Implement v2 API support - #194
Merged
Merged
Conversation
Generated from the live v2 API spec (2.0.0-beta, OpenAPI 3.1) with openapi-python-client 0.29.0 — attrs models + httpx, relative imports, sync and async variants of every endpoint. Generator selection rationale is recorded in fastfuels_sdk/v2/COMPARISON.md (following commit). generate_clients.sh is the reproducible regeneration recipe. It patches the spec before generation: the spec contains two schemas titled 'Feature' (the FastFuels feature resource and geojson-pydantic's GeoJSON Feature), and openapi-python-client names classes from titles and refuses duplicates — without the patch it silently drops every domain endpoint. The proper fix is re-titling the model in FastFuels-API-v2. The generated tree is excluded from pre-commit hooks like v1's, and from distributions via the fastfuels_sdk/v2 build exclude (PR #181) until the preview ships with its runtime deps (#176). Part of #176
The draft modules are the seeds for the real v2 wrappers (#177-#180): Domain at full v1 parity (from_id/from_geojson/from_geodataframe/ from_file/get/update/delete/list, to_geodataframe/to_json), and job-based Grid (LANDFIRE FBFM40, TreeMap/PIM, 3DEP topography), Feature (OSM road/water), and Inventory (PIM grid -> tree inventory) with a shared get/wait_until_completed/delete/list lifecycle that should be factored into a common base when promoted (#176). COMPARISON.md records the openapi-generator vs openapi-python-client evaluation that selected the latter: both generators were run against the same spec and the same wrapper surface was written twice across all four resources. Key findings: the spec is anyOf-saturated (38% of schemas, structural to FastAPI/pydantic), openapi-generator 7.22.0 emits broken code for the GeoJSON coordinate schemas (cannot deserialize any Domain) and wrapper-object ergonomics elsewhere, while ~68% of the wrapper code is generator-independent. Part of #176
generate_clients.sh (plural) was named for the generator comparison era when it produced both candidate clients. It is now the canonical regeneration tool for fastfuels_sdk/v2/client_library/, so: singular name, openapi-python-client pinned to 0.29.0 (regen diffs reflect API changes only; bump the pin deliberately), and a header documenting the regenerate-and-commit workflow. Verified: running the renamed script reproduces the committed client_library byte-for-byte (zero diff).
generate_client.sh now writes client_library/base_url.py so the script is the single source of truth for the deployment URL (openapi-python-client embeds no server URL and the spec has no servers entry). The regen also picked up a new endpoint added to the spec since the last generation: apply_modifications on tree inventories.
- exceptions.py: ApiException hierarchy (BadRequest/Unauthorized/ Forbidden/NotFound/UnprocessableEntity/Service) with expect() and raise_for_response() translating sync_detailed() responses at a single choke point; tolerant HTTPValidationError parsing for the API's string-detail 422s - api.py: set_api_key/get_client/ensure_client around one cached AuthenticatedClient (raise_on_unexpected_status=False so undocumented statuses reach the wrapper translation); base URL from the generated client_library.base_url with FASTFUELS_API_V2_URL override - remaining drafts repointed at the shared client (the draft-era _checked helper moves into draft_grids_opc until #178-#180)
Full v1-parity surface (from_id/from_geojson/from_geodataframe/from_file, get, update, delete, to_json, to_geodataframe, list_domains) built on the shared api/exceptions infrastructure, plus the v2-only endpoints: Domain.preview, Domain.get_lattice, and reproject_geojson. from_geojson accepts a single Feature and wraps it in a FeatureCollection; from_geodataframe forwards the GeoDataFrame CRS. Exported from fastfuels_sdk.v2. tests/v2/test_domains.py: 31 tests against the live v2 API (all passing), covering creation across formats and CRSs, the read/update/delete lifecycle, preview/reproject/lattice, and the typed exception paths (404 -> NotFoundException, 422 -> UnprocessableEntityException).
- mike powers a version selector in the header (mkdocs-material's extra.version.provider): v1 (current, aliased latest) and v2 (preview) deploy as independent site snapshots, so existing /v1/... deep links keep working - per-version configs (mkdocs.yml for v1, mkdocs-v2.yml for v2) inherit shared settings from mkdocs-base.yml; site_url set so switching versions keeps the reader on the equivalent page - docs.yml deploys both versions via mike on release (plus manual workflow_dispatch); first run should follow a one-time gh-pages wipe to drop the stale pre-restructure root-level pages - v2 docs: home, 'Migrating from v1' guide (per-resource what-changed sections seeded for grids/features/inventories), domains how-to guide, and an mkdocstrings reference for domains/api/exceptions - styled the version selector as an explicit pill control (extra.css) - mike added to the docs dependency group
One config now builds either version: DOCS_DIR selects the docs tree (default docs/v1; docs/v2 for v2 via mkdocs' !ENV substitution) and each tree carries its own nav in a SUMMARY.md (mkdocs-literate-nav). Replaces the mkdocs-base.yml/mkdocs-v2.yml INHERIT split. literate-nav is pinned to 0.6.2: 0.6.3 co-installs the properdocs mkdocs-fork and warns when run under mkdocs proper; revisit when the fork situation settles. check-yaml runs with --unsafe so it can parse mkdocs' !ENV tag.
…ervice mkdocs.yml, per the official mkdocs-material setup guides: automatic light/dark palette toggle, repo link, navigation features sized for a small site (sections/top/tracking/footer, toc.follow; instant nav deliberately skipped — it conflicts with mike's cross-version redirect), search suggest/highlight/share, code annotations, content tabs, and the curated baseline markdown extensions. mkdocs-material pinned to <10. Alignment with the FastFuels-Web documentation service (docs.fastfuels.silvxlabs.com), which both properties' Diataxis rules now codify in CLAUDE.md: version labels mirror the platform docs (v1 / 'v2 (Beta)', replacing 'preview'), the v2 pages cross-link to the platform docs for concept explanations instead of duplicating them, the domains guide opens with a Prerequisites section, and the migration guide's before/after snippets use v1/v2 content tabs. CLAUDE.md: documentation principles as shared context — the two-property division of responsibility, the four Diataxis kinds with their authoring rules, docstrings-are-user-facing-reference policy, and the versioned docs conventions (DOCS_DIR/SUMMARY.md/mike, duplicated extra.css, live-verified examples, frozen docs/v1).
Rewrite the draft on the domains.py pattern: every call goes sync_detailed() + exceptions.expect() (201 creates, 204 delete -- the draft fired the delete request and ignored the response entirely). New surface beyond the draft: create_layerset and create_layerset_from_geodataframe (projected-CRS fuelbed polygons, synchronous upload), partitioned data access (get_data_metadata, get_data_partition, get_data, to_geodataframe), and list_features grows an optional domain_id -- omitting it routes to the cross-domain endpoint (GET /domains/-/features) -- plus the type/product/tag filters. The job polling loop lands in _jobs.py as a plain shared helper (Grid/Inventory will delegate to the same function), not a mixin; wrappers keep their own small model-copy helpers.
34 tests against the live v2 API: OSM road/water creation, layerset upload (incl. single-Feature wrapping, GeoDataFrame path, and the geographic-CRS rejection), the wait/get/update/delete lifecycle, partitioned data access, and list filters incl. the cross-domain route. test_sorting is xfailed: every sort_by combination on the feature list endpoints returns 500 from the live API (missing composite indexes); the equivalent domain sorting works. create_default_layerset_geojson() builds the test layerset from blue_mtn_5070 (already projected) with the required fuelbed columns.
Features how-to guide (create from OSM, wait, layerset upload with the projected-CRS warning, partitioned data access, list filters incl. cross-domain, delete, error handling) with outputs verified against the live API; nav and reference entries; migration guide's Features section replaces the in-development placeholder with what-changed bullets and before/after tabs.
The v2 resources form a dependency chain (domain -> features; grids and inventories will hang off the same domain and can reference features as spatial masks), so expensive job resources should be built once per session, not once per module. conftest.py now owns test_domain, road_feature (completed), and layerset_feature at session scope; the grids/inventories modules will request them and add their own (e.g. a completed PIM grid). Convention: shared fixtures are READ-ONLY. The update test classes, which previously mutated the shared resources (safe only by in-file ordering), now mutate class-scoped throwaways. Test domains carry an sdk-test fingerprint tag, and a session-start sweeper deletes tagged domains older than two hours -- leftovers from crashed or interrupted runs, where teardown never fires. The age gate keeps concurrent runs (local + CI) from sweeping each other.
- The leak sweep is a plain function (tests/v2/utils.py sweep_leftover_domains), called in plain sight at the top of the test_domain fixture -- no more side-effect-as-fixture dependency injection, no isinstance paranoia around fields the live API always returns. - road_feature becomes completed_road_feature: fixtures that wait on a job carry a completed_ prefix (layersets are born complete, so layerset_feature stays unprefixed). - Tests that only need a feature to exist (from_id, get, to_json, cross-domain list) now use the cheap layerset_feature; completed_road_feature remains only where completion or road-ness is the point: the job-lifecycle test, the data-access tests, and the list-filter tests that need a road in the listing.
One entry point for both workflows:
./scripts/docs.sh build v1 + v2 snapshots and serve the full
site (version selector included) locally
./scripts/docs.sh deploy publish both versions to gh-pages
The docs workflow now calls the script, so versions, titles, and
aliases live in exactly one place.
The version selector was wired to the wrong axis. mike's model is frozen snapshots of one docs tree, one per package release; pydantic (which ships pydantic.v1 inside 2.x -- exactly our subpackage situation) keys its selector to releases (dev, 2.13..2.0, 1.10) and documents the legacy surface via a migration guide plus the frozen old-release snapshot. Selector entries keyed to API surfaces exist only in REST API portals (Stripe) -- the axis the platform docs correctly use for the two deployed HTTP services, but wrong for one package that ships both interfaces. Now: one docs/ tree -- v1 content at the root (restoring the original site URLs) and v2 as a 'v2 (Beta)' nav section -- with the nav inline in mkdocs.yml, and the workflow deploys <major.minor> + latest with mike on each GitHub release. Docs ship with the release that ships the code. Deletes scripts/docs.sh, the DOCS_DIR/!ENV switch, literate-nav and both SUMMARY.md navs, and the duplicate stylesheet. Also fixes the v1 create_tree_grid docstring (SAVR -> savr), which blocked --strict now that one build covers the v1 reference.
Seeing it rendered settled the question the other way: the SDK docs keep the v1/v2 version dropdown, deliberately mirroring the platform docs UX. The dual-tree mike setup is the accepted cost of that dropdown. Keeps the create_tree_grid docstring fix (SAVR -> savr) from the reverted commit.
) Implements the settled hybrid v2 API: creation is module-level functions (create_*_grid_from_*), and operations on a held resource are methods. grids.py (new, #178): - Grid record (subclasses the generated model) with refresh/wait/update/ delete and the transform methods resample, lookup_fuel_model_values, and export. - Creators: 3dep/landfire topography, landfire canopy, meta/naip CHM, landfire fbfm40/fccs, treemap (create_pim_grid_from_treemap), geotiff/ netcdf upload, and uniform. - Top-level list_grids/get_grid and check_3dep_coverage; shared _build_alignment helper translating output_resolution_m/align_to/align/ resampling into the alignment union. Wait model (_jobs.py): wait/wait_all polling refresh(), JobFailedError. features.py/domains.py: move creation to module-level functions; uniform lifecycle (get->refresh in place, wait, in-place update); Domain.delete gains force= for cascade. __init__.py: expose the grids/features submodules plus a flat top-level surface (Grid, list_grids, get_grid, wait_all, JobFailedError, ...). Remove the superseded drafts and COMPARISON.md. Add live tests in test_grids.py and update the feature/domain tests and shared fixtures for the function-based creators and the rename. v2_api_design.md captures the working design notes. Live-verified against the prod API: tests/v2/test_grids.py 50 passed, 2 skipped (deferred geotiff/netcdf upload paths).
Rename create_canopy_height_grid_from_naip -> create_canopy_height_grid_from_naip_chm (function, __all__, test class + call site, design-doc row) to match the NAIP-CHM product name. Rewrite the docstring from the upstream NAIP-CHM model docs: it produces a single continuous chm band (height in meters) at 0.6 m over CONUS, and is a surface model (nDSM) that retains buildings and infrastructure as well as vegetation — so vegetative-fuel-only workflows must mask built structures via modifications=. Add a References section citing the publication (Morford et al. 2025) and the dataset/model repository.
Build a GridModification (feature spatial condition + replace action) for a creator's modifications= list — the v2 replacement for v1 feature_masks. Lives in fastfuels_sdk/v2/modifications.py, exported as ff.mask; supports a single band or a list, a Feature or bare id, and the operator/buffer_m/target options. Covered by unit payload tests plus a live FBFM40-with-mask test.
Add workflow-first how-to guides for grids, split into Creating grids (by source, with an aligned-grid-set worked example) and Working with grids (lifecycle, inspection, resample, export). All examples are verified against the live API with real output — band catalogs and units, georeference, wait_all output, and the export record. Rewrite features.md for the functional creators and the wait/refresh/update lifecycle; fix the stale get()/in_place examples in domains.md; fill in the grids and features sections of migration.md; and add grids, modifications, and _jobs to the API reference plus a Grids group to the nav.
Inventory record with the uniform v2 lifecycle plus duplicate, apply_modifications, voxelize (3D tree fuel grid), export, and partitioned data access assembling to a pandas DataFrame. Creators: create_tree_inventory_from_pim_grid / _from_chm_grid / _from_file (signed-URL upload; the PUT must echo the signed x-goog-content-length-range header). list_inventories routes to the cross-domain endpoint when no domain is given. Live tests: 29 passed, 1 xfail — in-place apply_modifications fails server-side (FastFuels-API-v2#333).
New inventories how-to guide with live-captured examples; the creating-grids voxelization section gains real inventory.voxelize code; the migration guide's Inventories section is filled in (v1 exposed tree data only through exports); inventories added to the Reference and nav.
Export record with the uniform v2 lifecycle plus to_file (streams the signed URL; a directory target uses the export's default filename). create_quicfire_export bundles fuel grids into the QUIC-Fire archive, taking each role as a (grid, band) pair and the fire grid as horizontal/vertical resolution or align_to. list_exports/get_export address exports by id alone. grid.export and inventory.export now return the wrapped Export, so create -> wait -> to_file chains. Live tests: 21 passed, including the QUIC-Fire zip roundtrip.
New exports how-to guide with live-captured download and QUIC-Fire bundle examples; the grid and inventory export sections now show the working wait().to_file() flow; the migration guide gains an Exports section (export_roi becomes explicit creation plus the server-side bundle) and drops the stale grids-module note; exports added to the Reference and nav.
Promote httpx/attrs/python-dateutil from the dev group to runtime dependencies and replace the whole-tree fastfuels_sdk/v2 build exclude with fine-grained excludes, so the v2 package ships in the wheel as a beta preview. Drop the httpx<0.29.0 upper cap (inherited from the client generator) to avoid resolution conflicts for installers. Split the test workflow into test-v1 and test-v2 jobs. v1 and v2 are separate deployments with separate keys, so each job authenticates its own deployment via a per-job secret mapped to the shared FASTFUELS_API_KEY variable. test-v2 is intended as a non-required check while v2 is beta. api.py records the settled decision that both versions read FASTFUELS_API_KEY. Part of #176.
The regen added Band.summary, a continuous/categorical union the server populates per band when a grid completes. It was reachable through the raw grid.bands[i].summary, but looking a band up by key was clunky. Grid.band_summary(band) returns the summary for a named band (validating the key like to_numpy), normalizing the not-yet-computed case to None. The returned object is the generated ContinuousBandSummary (count/nodata_count/min_/max_/ mean/std) or CategoricalBandSummary (count/nodata_count/unique_count), discriminated by type_. Factored the band-key lookup + "no band" error out of to_numpy into a shared Grid._band helper (used by both), so the validation can't drift. Docs: a "Summarize a band without downloading it" section in working-with-grids. Tests (TDD): pure unit tests for the accessor (returns the summary, None when unset, raises on unknown band) plus a live continuous-summary check; the existing to_numpy tests guard the _band refactor. Verified the field is None while pending and populated after wait(). 3 unit + 3 live passed.
Consolidate the inventory modify/treat workflow into one focused How-To (guides/modify-treat-inventories.md): modifications (conditions + actions), treatments (ff.basal_area_treatment / ff.diameter_treatment), applying both at creation and to a held inventory (apply_modifications / apply_treatments), and the duplicate-to-branch pattern. Trim the overlap in inventories.md per the Diataxis split: the detailed "Modify trees at creation" / "Branch a scenario" sections become a short "Reshape the trees" pointer plus a "Duplicate an inventory" lifecycle section, both cross-linking the new guide. Add the guide to SUMMARY nav and retarget the two stale inventories.md#modify-trees-at-creation links (creating-grids, export tutorial) to it. The guide notes the one gap it surfaces: inventory modifications are still hand-built models while treatments/masks have builders (tracked as a follow-up for an ff.modify vocab). Validated with `mkdocs build --strict` (no broken links / nav errors).
Bring the scratch design note current after the client re-sync: update the module layout (point_clouds.py, treatments.py, _uploads.py, _jobs.py; drop the stale [later] tags on inventories/exports; add the new Grid methods) and add a "Post-regen additions (2026-06-12)" section recording the new resources/ endpoints and the design calls behind them (GDAM + fbfm40-lookup are functions not methods; treatments have builders but inventory modifications don't yet; the shared put_upload header fix). Also reframed the modifications/treatments vocab open item to match what shipped. Docs/exports/__init__ exports and reference+SUMMARY nav for the new modules were already updated as part of #14-#21; this closes out the housekeeping.
Close the vocab asymmetry the modify/treat guide surfaced: inventory
modifications were hand-built generated models while grids (ff.mask) and
inventory treatments (ff.basal_area_treatment) had builders.
Broaden modifications.py from grid-only to all modification primitives and add
four tree-scoped builders (exported top-level, string-coercing like mask):
- tree_attribute(attr, operator, value) -> InventoryModificationCondition;
operator accepts symbolic ("<") or named ("lt") forms.
- tree_within(feature, buffer_m=, operator=) -> InventoryFeatureSpatialCondition
(which treatments' conditions= also accept).
- remove_trees(*conditions) -> InventoryModification with RemoveAction.
- modify_trees(attr, modifier, value, *conditions) -> InventoryModification with
an attribute action.
Both assemblers require >=1 condition (the API 422s on an empty rule).
The modify/treat guide now uses the builders (dropped the hand-built example
and the "no ff.modify builder yet" note); design note updated to mark the vocab
settled.
Tests (TDD): 14 pure unit tests (coercion, condition ANDing, RemoveAction vs
modifier, >=1-condition guard) red->green. Live: a create-time modification
(remove_trees(tree_attribute("dbh","<",10)) at seed 42) completes with strictly
fewer trees than the unmodified fixture — verified via the create path because
in-place apply_modifications is #333-blocked. 14 unit + 1 live passed.
Complete v2 post-regen SDK backlog
…support # Conflicts: # mkdocs.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #176
What changed
fastfuels_sdk.v2, including the generated client and high-level Domain, Grid, Feature, Inventory, Export, and PointCloud APIs.fastfuels_sdknamespace on v1.main, including the latest v1 documentation corrections.Why
This is the integration branch for the v2 SDK preview. It brings the generated client, supported high-level interface, live tests, packaging, and user documentation together without changing the existing default v1 API.
User and developer impact
Existing
import fastfuels_sdkusers continue to receive the v1 interface. V2 is additive and explicitly selected with:The release adds runtime dependencies used by v2: attrs, httpx, python-dateutil, and xarray.
Validation
uv run pre-commit run --all-files: passeduv run pytest tests/v1 -q: 398 passed, 3 skippeduv run pytest tests/v2 -q -rxX: 320 passed, 1 skippedsdk-testdomains remained after the live suitesmainmerged cleanly after resolving the versionedmkdocs.ymlconfigurationRemaining release work
Issue #176 remains open for the stable v2 API domain decision and the 0.22 preview release.