diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2dffd2e..63aab77 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,14 +3,25 @@ name: build mkdocs on: release: types: [published] + workflow_dispatch: + +permissions: + contents: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v6 - - name: Install docs dependencies - run: uv sync --group docs - - run: uv run mkdocs gh-deploy --force --clean --verbose + - name: Configure git for mike + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + # docs/deploy.sh is the single source of truth for versions, titles, + # and aliases — the same script serves the local preview. + - name: Deploy versioned docs (v1 + v2) + run: ./docs/deploy.sh deploy diff --git a/.github/workflows/tests_main.yml b/.github/workflows/tests_main.yml index 4993cb3..18932e6 100644 --- a/.github/workflows/tests_main.yml +++ b/.github/workflows/tests_main.yml @@ -4,17 +4,47 @@ on: pull_request: branches: [ main ] -env: - FASTFUELS_API_KEY: ${{ secrets.FASTFUELS_API_KEY }} - +# v1 and v2 are separate live deployments with separate API keys, so each +# suite runs as its own job with its own secret mapped to FASTFUELS_API_KEY +# (the variable both SDKs read). While v2 is a beta preview, make only +# `test-v1` a required status check in branch protection — `test-v2` runs +# and reports on every PR but is intentionally NOT required, so v2-API +# flakiness does not block v1-only changes. jobs: - test: + test-v1: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + env: + FASTFUELS_API_KEY: ${{ secrets.FASTFUELS_API_KEY }} + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + - name: Install dependencies + run: uv sync + - name: Run v1 tests + env: + TEST_ENV: local + run: | + cd tests/ + uv run pytest v1 + + test-v2: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest] python-version: ["3.12"] + env: + FASTFUELS_API_KEY: ${{ secrets.FASTFUELS_API_KEY_V2 }} steps: - uses: actions/checkout@v4 - name: Install uv @@ -24,9 +54,9 @@ jobs: enable-cache: true - name: Install dependencies run: uv sync - - name: Run pytest + - name: Run v2 tests env: TEST_ENV: local run: | cd tests/ - uv run pytest + uv run pytest v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f87da45..47e115f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,12 @@ -exclude: ^fastfuels_sdk/v1/client_library/ +exclude: ^fastfuels_sdk/(v1|v2)/client_library/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-yaml + # mkdocs.yml uses mkdocs' !ENV tag, which plain yaml.safe_load + # cannot construct; --unsafe still checks syntax + args: [--unsafe] - id: end-of-file-fixer - id: trailing-whitespace - id: detect-private-key diff --git a/CLAUDE.md b/CLAUDE.md index b065830..cad21bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,3 +4,84 @@ Do NOT add "Co-Authored-By: Claude", "Generated with Claude Code", or any other AI-attribution lines to commit messages, PR descriptions, or GitHub issues. + +## Live API testing + +- Local live-test credentials are stored in the repository-root `.env`, which + is ignored by Git. The SDK authenticates with the `FASTFUELS_API_KEY` value + from that file. +- Before running live tests, load `.env` into the current shell without + printing its contents: `set -a; source .env; set +a`. +- Check and load `.env` before concluding that live API credentials are + unavailable or asking the user to provide them. +- Never print, stage, commit, or copy credential values from `.env` into code, + documentation, logs, commit messages, pull requests, or issues. + +## Documentation + +FastFuels has two documentation properties that must stay in concert: + +- **This repo (`docs/`)** — the Python SDK docs (mkdocs-material + mike, + published to silvxlabs.github.io/fastfuels-sdk-python). Scope: how to use + FastFuels **from Python** — SDK how-to guides, SDK tutorials, migration + guides, and the mkdocstrings API reference. +- **FastFuels-Web `documentation/`** (sibling repo) — the platform docs at + docs.fastfuels.silvxlabs.com (Astro/Starlight). Scope: the web + application, the HTTP API (language-agnostic: curl + raw requests), and + **all concept explanations**. + +Division of responsibility: platform concepts (what a domain is, how grids +work, why a design is the way it is) are explained once, in the platform +docs — the SDK docs link to docs.fastfuels.silvxlabs.com rather than +duplicating them. The SDK docs own Python idioms, signatures, and +SDK-specific behavior. Cross-link in both directions; never copy content +between the two. + +### Diátaxis + +Both properties follow the Diátaxis framework (reference copy: +`FastFuels-Web/documentation/diataxis.rst`). Every page is exactly ONE of +four kinds — decide which before writing; if a page wants to be two kinds, +split it: + +- **Tutorial** — learning by doing. First-person plural ("we'll create…"), + prerequisites up front, expected output shown at each step, reliable + end-to-end. No explanation digressions — link instead. +- **How-to guide** — a goal, for a competent user. Conditional imperatives + ("To create a domain from a file, …"). Action only: no teaching, no + background. Opens with a short Prerequisites section. +- **Reference** — neutral facts. In this repo it is generated from + docstrings via mkdocstrings; do not hand-write opinions into it. +- **Explanation** — the "why". Belongs in the platform docs unless it is + SDK-specific (e.g. the v1→v2 migration guide's what-changed sections). + +### Docstrings are user-facing reference documentation + +mkdocstrings renders docstrings directly into the published Reference +pages. A docstring describes what the object does, parameters, returns, +raises, and examples — nothing else. No editorial or comparative +commentary (no v1-vs-v2 asides, no GitHub issue references, no design +rationale). Migration notes belong in `docs/v2/guides/migration.md`; +rationale and issue references belong in `#` code comments. + +### Conventions + +- Version labels mirror the platform docs: **"v1"** (alias `latest`) and + **"v2 (Beta)"** — set as mike titles in `docs/deploy.sh`, the single + entry point for both the local preview (`./docs/deploy.sh`) and the + gh-pages deploy (`./docs/deploy.sh deploy`, run by the docs workflow). +- One `mkdocs.yml` builds both versions: `DOCS_DIR` selects `docs/v1` + (default) or `docs/v2`; each tree owns its nav in a `SUMMARY.md` + (literate-nav). Versions deploy as independent mike snapshots picked + from the header version selector. +- `docs/v1/stylesheets/extra.css` and `docs/v2/stylesheets/extra.css` are + duplicates by construction — edit both or the version snapshots diverge. +- Code examples must be real: verified against the live API (the live test + suite in `tests/` is the verification path), with realistic output — + never hand-written response shapes. +- Use admonitions (`!!! tip` / `warning` / `danger`) instead of bold + "Note:" prose; use content tabs (`=== "v1"` / `=== "v2"`) for + side-by-side variants; v2 pages open with the `!!! warning "Beta"` + admonition. +- `docs/v1/` is frozen alongside the v1 SDK (bugfix-level edits only); + new documentation lands in `docs/v2/`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d689a8a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,51 @@ +# FastFuels SDK documentation + +This site is built with [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) +and versioned with [mike](https://github.com/jimporter/mike). The header +has a version dropdown with two entries, mirroring the FastFuels +platform docs: **v1** (the current default SDK, alias `latest`) and +**v2 (Beta)** (the `fastfuels_sdk.v2` subpackage). + +## Layout + +| Path | What it is | +|---|---| +| `docs/v1/` | v1 SDK docs — frozen alongside the v1 SDK (bugfix-level edits only) | +| `docs/v2/` | v2 SDK docs — new documentation lands here | +| `docs/v1/SUMMARY.md`, `docs/v2/SUMMARY.md` | Each version's nav (mkdocs-literate-nav) | +| `docs/v1/stylesheets/extra.css`, `docs/v2/stylesheets/extra.css` | Duplicates by construction — edit both | +| `mkdocs.yml` (repo root) | One config for both versions: `DOCS_DIR` picks the tree (default `docs/v1`) | +| `docs/deploy.sh` | The docs entry point: build, serve, deploy | + +## Launch the docs server + +The full site, version dropdown included, at : + +```bash +./docs/deploy.sh +``` + +This builds both versions onto your **local** `gh-pages` branch and +serves it with mike (`git branch -D gh-pages` discards the preview). + +For quick edits to one version, plain mkdocs gives live reload: + +```bash +uv run mkdocs serve # v1 +DOCS_DIR=docs/v2 uv run mkdocs serve # v2 +``` + +> [!NOTE] +> The version dropdown only exists under mike — plain `mkdocs serve` +> shows a single version and logs a `versions.json` 404. That's +> expected, not a bug. + +## Deploy + +```bash +./docs/deploy.sh deploy +``` + +Publishes both versions to `gh-pages` on origin. The docs workflow +(`.github/workflows/docs.yml`) runs the same command on each GitHub +release (or manually via workflow_dispatch). diff --git a/docs/deploy.sh b/docs/deploy.sh new file mode 100755 index 0000000..c42aeb6 --- /dev/null +++ b/docs/deploy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Build the versioned docs — v1 and v2 mike snapshots — and serve or publish. +# +# ./docs/deploy.sh build both versions, then serve the full site +# (version selector included) at localhost:8000 +# ./docs/deploy.sh deploy publish both versions to gh-pages on origin +# +# mike snapshots each version onto the gh-pages branch and maintains the +# versions.json that powers the header version selector; plain +# `mkdocs serve` can only ever show one version (and 404s versions.json). +# Without `deploy`, the snapshots are committed to your local gh-pages +# branch only — `git branch -D gh-pages` discards them. +# +# Version labels mirror the FastFuels-Web docs (docs.fastfuels.silvxlabs.com). + +set -euo pipefail +cd "$(dirname "$0")/.." + +push="" +case "${1:-serve}" in + serve) ;; + deploy) push="--push" ;; + *) + echo "usage: $0 [deploy]" >&2 + exit 1 + ;; +esac + +uv run --group docs mike deploy $push --update-aliases v1 latest --title "v1" +DOCS_DIR=docs/v2 uv run --group docs mike deploy $push v2 --title "v2 (Beta)" +uv run --group docs mike set-default $push latest + +if [[ -z "$push" ]]; then + uv run --group docs mike serve +fi diff --git a/docs/v1/SUMMARY.md b/docs/v1/SUMMARY.md new file mode 100644 index 0000000..3b98b9b --- /dev/null +++ b/docs/v1/SUMMARY.md @@ -0,0 +1,12 @@ +* [Home](index.md) +* How-To Guides + * [Authentication](guides/authentication.md) + * [Domains](guides/domains.md) + * [Inventories](guides/inventories.md) + * [Point Clouds](guides/point_clouds.md) + * [Features](guides/features.md) + * [Grids](guides/grids.md) +* Tutorials + * [Export to QUIC-Fire](tutorials/export_to_quicfire.md) + * [ALS Point Cloud](tutorials/point_cloud_example.md) +* [Reference](reference.md) diff --git a/docs/index.md b/docs/v1/index.md similarity index 100% rename from docs/index.md rename to docs/v1/index.md diff --git a/docs/v1/stylesheets/extra.css b/docs/v1/stylesheets/extra.css new file mode 100644 index 0000000..c76b44b --- /dev/null +++ b/docs/v1/stylesheets/extra.css @@ -0,0 +1,55 @@ +/* Version selector (mike + mkdocs-material). The stock theme renders the + selector as bare header text; style it as an explicit control. + (Duplicated in docs/v1 and docs/v2 — each version's site builds from its + own docs_dir.) */ +.md-version { + margin-left: 0.8rem; +} + +.md-version__current { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background-color: rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 2rem; + padding: 0.25rem 0.8rem; + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + transition: background-color 125ms, border-color 125ms; +} + +.md-version__current:hover, +.md-version__current:focus { + background-color: rgba(0, 0, 0, 0.32); + border-color: rgba(255, 255, 255, 0.6); +} + +.md-version__list { + margin-top: 0.5rem; + border-radius: 0.3rem; + box-shadow: var(--md-shadow-z3); + overflow: hidden; + min-width: 8rem; +} + +.md-version__item { + line-height: 1; +} + +.md-version__link { + display: block; + width: 100%; + padding: 0.6rem 1rem; + font-size: 0.7rem; + color: var(--md-default-fg-color); + transition: background-color 125ms, color 125ms; +} + +.md-version__link:hover, +.md-version__link:focus { + background-color: var(--md-default-fg-color--lightest); + color: var(--md-typeset-a-color); +} diff --git a/docs/v2/SUMMARY.md b/docs/v2/SUMMARY.md new file mode 100644 index 0000000..601ae6c --- /dev/null +++ b/docs/v2/SUMMARY.md @@ -0,0 +1,17 @@ +* [Home](index.md) +* [Migrating from v1](guides/migration.md) +* Tutorials + * [Export QUIC-Fire inputs](tutorials/export_to_quicfire.md) +* How-To Guides + * [Domains](guides/domains.md) + * [Features](guides/features.md) + * Grids + * [Creating grids](guides/creating-grids.md) + * [Composing grids](guides/composing-grids.md) + * [Working with grids](guides/working-with-grids.md) + * [Inventories](guides/inventories.md) + * [Modify and treat inventories](guides/modify-treat-inventories.md) + * [Point clouds](guides/point-clouds.md) + * [Exports](guides/exports.md) + * [Quotas and usage](guides/quotas.md) +* [Reference](reference.md) diff --git a/docs/v2/guides/composing-grids.md b/docs/v2/guides/composing-grids.md new file mode 100644 index 0000000..aa45e0d --- /dev/null +++ b/docs/v2/guides/composing-grids.md @@ -0,0 +1,65 @@ +# How to Compose Grids + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +Use grid composition to copy and calculate bands from completed grids on the +same two-dimensional lattice. For creating the source grids, see +[Creating grids](creating-grids.md); for alignment options, see +[Align grids to each other](creating-grids.md#align-grids-to-each-other). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- One or more completed 2D grids in the same domain, with the same CRS, + transform, and shape +- For the example below, a completed FBFM40 grid assigned to `fbfm40` + +## Select and calculate output bands + +Give each input grid an alias. Use that alias when referring to its bands in +select and compute operations: + +```python +import fastfuels_sdk.v2 as ff + +fuel_grid = ff.grids.create_fuel_grid_from_fbfm40_lookup( + fbfm40, + bands=["fuel_load.1hr", "fuel_depth"], +) +fuel_grid.wait() + +composed = ff.grids.create_grid_from_compose( + {"fuels": fuel_grid}, + select=[ + ff.compose.select("fuel_depth", "fuels.fuel_depth"), + ], + compute=[ + ff.compose.compute( + "fuel_load.1hr", + "multiply", + ["fuels.fuel_load.1hr", 0.5], + conditions=[ + ff.compose.condition("fuels.fuel_load.1hr", "gt", 0), + ], + else_=ff.compose.literal(0, unit="kg/m**2"), + ), + ], +) +composed.wait() +``` + +The alias mapping can contain more than one grid. Every band reference uses +the form `alias.band_key`; output names must be unique across the `select` and +`compute` lists. + +The arithmetic operators are `add`, `subtract`, `multiply`, `divide`, `min`, +`max`, and `average`. Conditions are ANDed together and require an `else_` +fallback. A fallback may be another band, a number, a typed literal, a fuel +model label, or an `ff.compose.inline_compute(...)` result. + +The API derives output units from compute operands. Pass `unit=` to +`ff.compose.compute` only to request a dimensionally compatible output unit. diff --git a/docs/v2/guides/creating-grids.md b/docs/v2/guides/creating-grids.md new file mode 100644 index 0000000..34bc29a --- /dev/null +++ b/docs/v2/guides/creating-grids.md @@ -0,0 +1,461 @@ +# How to Create Grids in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +A grid is a raster data product within a domain — topography, surface fuel +models, canopy fuels, and more — generated from a data source. This guide +shows how to create grids and assemble them into an aligned dataset for fire +modeling. For waiting on, inspecting, exporting, and managing grids you +already hold, see [Working with grids](working-with-grids.md); for what grids +*are*, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). Coming from +the v1 SDK? Start with the [migration guide](migration.md#grids). + +Creation is functional: call a `create__grid_from_` function on +a domain. Each returns a [`Grid`](working-with-grids.md) whose generation runs +as a background job, so it starts `"pending"`. + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- An existing [domain](domains.md) — every creator's first argument is a + `Domain` (or a bare domain id string) + +## Build an aligned grid set + +The typical goal is several grids covering one domain on the *same* lattice, +so they stack cell-for-cell. Because each creator can resample to the domain +lattice, giving them all the same `output_resolution_m` is enough — here, a +30 m terrain + surface fuel + canopy set: + +```python +import fastfuels_sdk.v2 as ff + +topography = ff.grids.create_topography_grid_from_3dep( + domain, source_resolution_m=10, output_resolution_m=30, + bands=["elevation", "slope", "aspect"], +) +surface_fuel = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + domain, output_resolution_m=30, +) +canopy_fuel = ff.grids.create_canopy_fuel_grid_from_landfire( + domain, output_resolution_m=30, +) + +# Jobs run server-side in parallel — create first, then join. +ff.wait_all([topography, surface_fuel, canopy_fuel]) +``` + +Each grid reports the bands it carries and, once complete, its +georeference: + +```python +>>> [b.key for b in topography.bands] +['elevation', 'slope', 'aspect'] +>>> topography.georeference.crs +'EPSG:32611' +>>> topography.georeference.shape # [rows, cols] on the domain lattice +[47, 58] +``` + +Because all three share `output_resolution_m=30` on the same domain, they +share a CRS, transform, and shape — they overlay exactly. The sections below +cover each source in detail; [Working with grids](working-with-grids.md) +covers inspecting, resampling, exporting, and managing the results. + +## Topography grids + +From the USGS 3D Elevation Program (3DEP), which carries `elevation` (m), +`slope` (deg), and `aspect` (deg): + +```python +grid = ff.grids.create_topography_grid_from_3dep( + domain, + source_resolution_m=10, # 3DEP native resolution: 1, 10, or 30 + output_resolution_m=30, + bands=["elevation", "slope", "aspect"], +) +``` + +3DEP is the higher-resolution source where it has coverage. To check before +creating the grid: + +```python +>>> coverage = ff.grids.check_3dep_coverage(domain, resolution_m=10) +>>> coverage.available +True +>>> coverage.tile_count +1 +``` + +Where 3DEP lacks coverage, source topography from LANDFIRE (30 m, CONUS) +instead: + +```python +grid = ff.grids.create_topography_grid_from_landfire(domain, output_resolution_m=30) +``` + +## Surface fuel model grids + +LANDFIRE's 40 Scott & Burgan fire behavior fuel models (FBFM40) is the +standard surface fuels source. The grid carries a single categorical `fbfm` +band of fuel model *codes* (e.g. `GR1`, `TL3`, `SH5`): + +```python +grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + domain, output_resolution_m=30 +) +``` + +To replace non-burnable codes (urban, water, agriculture, …) with no-data, +pass them to `remove_non_burnable`: + +```python +grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + domain, output_resolution_m=30, remove_non_burnable=["NB1", "NB2"] +) +``` + +### Look up fuel parameters from FBFM40 codes + +The `fbfm` band holds codes, not the quantities a fire model consumes. To +turn the codes into fuel parameters — loadings by size class, fuel-bed depth, +surface-area-to-volume ratios — pass the completed grid to +`ff.grids.create_fuel_grid_from_fbfm40_lookup`. It returns a new grid whose +bands are the requested parameters: + +```python +grid.wait() + +fuels = ff.grids.create_fuel_grid_from_fbfm40_lookup( + grid, bands=["fuel_load.1hr", "fuel_load.10hr", "fuel_depth"] +) +fuels.wait() +``` + +```python +>>> [(b.key, b.unit) for b in fuels.bands] +[('fuel_load.1hr', 'kg/m**2'), ('fuel_load.10hr', 'kg/m**2'), ('fuel_depth', 'm')] +``` + +### Use Anderson 13 fuel models + +To create the Anderson 13 model set instead, select a LANDFIRE version and +use the FBFM13 creator: + +```python +grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm13( + domain, + version="2024", + remove_non_burnable=["NB1", "NB2"], + output_resolution_m=30, +) +grid.wait() +``` + +Its categorical source band is `fbfm13`. Convert it to any of the nine +FBFM13 parameter bands with the matching lookup: + +```python +fuels = ff.grids.create_fuel_grid_from_fbfm13_lookup( + grid, + bands=["fuel_load.1hr", "fuel_load.live_foliage", "fuel_depth"], +) +fuels.wait() +``` + +### From FCCS instead + +To use Fuel Characteristic Classification System (FCCS) fuelbeds, create the +categorical source grid and wait for it to complete: + +```python +fccs = ff.grids.create_fuel_model_grid_from_landfire_fccs( + domain, remove_bare_ground=True, output_resolution_m=30 +) +fccs.wait() +``` + +Then look up any of the 12 available FCCS fuel-parameter bands, including +duff and live components: + +```python +fuels = ff.grids.create_fuel_grid_from_fccs_lookup( + fccs, + bands=[ + "fuel_load.litter", + "fuel_load.duff", + "duff_depth", + "fuel_load.live_shrub", + ], +) +fuels.wait() +``` + +FCCS takes the same alignment arguments as the other source grids +(`output_resolution_m`, `align_to`, `align`, `resampling`) — see +[Align grids to each other](#align-grids-to-each-other). + +To select and calculate bands across one or more completed grids, see +[Compose grids](composing-grids.md). + +## Canopy grids + +For crown-fire inputs, `create_canopy_fuel_grid_from_landfire` produces the +full LANDFIRE canopy set (30 m, CONUS) — canopy height, bulk density, base +height, and cover: + +```python +grid = ff.grids.create_canopy_fuel_grid_from_landfire(domain, output_resolution_m=30) +``` + +```python +>>> [(b.key, b.unit) for b in grid.bands] +[('chm', 'm'), ('cbd', 'kg/m**3'), ('cbh', 'm'), ('cc', '%')] +``` + +When you only need canopy *height* at higher resolution, use a dedicated +canopy height model — the Meta model (≈1 m, global) or NAIP-CHM (0.6 m, +CONUS) — each producing a single `chm` band: + +```python +meta = ff.grids.create_canopy_height_grid_from_meta(domain, output_resolution_m=1) +naip = ff.grids.create_canopy_height_grid_from_naip_chm(domain, output_resolution_m=1) +``` + +To rasterize a completed airborne point cloud into the same `chm` band, pass +the point cloud directly. The output defaults to 1 m cells: + +```python +chm = ff.grids.create_canopy_height_grid_from_point_cloud(point_cloud) +chm.wait() +``` + +Use `output_resolution_m` or `align_to` to choose a different lattice. See the +[Point clouds guide](point-clouds.md#create-a-point-cloud-from-usgs-3dep) to +create an airborne point cloud from USGS 3DEP. + +!!! tip "NAIP-CHM is a surface model" + NAIP-CHM is a digital surface model and retains buildings and other + infrastructure. To keep only vegetation, mask out built-up areas — see + [Mask out features](#mask-out-features). + +## 3D tree fuel grids (voxelization) + +The 3D canopy fuel grid — per-voxel bulk density, the input 3D fire models +consume — is built in three steps: + +**1. Create a Plot Imputation Map (PIM) grid** that maps each cell to a +TreeMap forest inventory plot: + +```python +pim = ff.grids.create_pim_grid_from_treemap( + domain, output_resolution_m=30, resampling="nearest" +) +pim.wait() +``` + +By default the grid carries the TreeMap id band (`tm_id`); request the plot +control number as well with `bands=["tm_id", "plt_cn"]`. + +**2. Generate a tree inventory** from the PIM grid — a table of individual +trees imputed from the matched plots (see the +[Inventories guide](inventories.md)): + +```python +inventory = ff.inventories.create_tree_inventory_from_pim_grid( + domain, pim, seed=42 +) +inventory.wait() +``` + +**3. Voxelize the inventory**, discretizing each tree's crown onto a 3D +lattice and computing per-voxel fuel properties: + +```python +voxels = inventory.voxelize( + horizontal_resolution_m=2.0, vertical_resolution_m=1.0 +) +voxels.wait() +``` + +```python +>>> voxels.georeference.shape # [layers, rows, cols] +[37, 703, 863] +``` + +Because it is a 3D product, a voxel grid supports neither resampling nor +post-hoc modifications — apply any tree modifications on the inventory +before voxelizing (see +[Modify and treat tree inventories](modify-treat-inventories.md)). + +For the concepts behind plot imputation and voxelization, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). + +### Derive surface fuels with DUET + +To create a two-dimensional DUET surface-fuel grid, include the three DUET +input bands when voxelizing the inventory: + +```python +voxels = inventory.voxelize( + horizontal_resolution_m=2, + vertical_resolution_m=1, + bands=[ + "bulk_density.foliage.live", + "spcd", + "fuel_moisture.live", + ], +) +voxels.wait() +``` + +Build calibration targets from ordinary mappings, then pass them with the +output bands and time since fire: + +```python +calibration = ff.duet_calibration( + fuel_load={ + "grass": {"mean": 0.5, "sd": 0.25}, + "litter": {"max": 5, "min": 0}, + }, + fuel_depth={ + "grass": {"value": 0.3}, + "litter": {"value": 0.06}, + }, +) + +surface = ff.grids.create_surface_fuel_grid_from_duet( + voxels, + years_since_burn=25, + bands=[ + "fuel_load.grass", + "fuel_load.litter", + "fuel_depth.grass", + "fuel_depth.litter", + ], + calibration=calibration, +) +surface.wait() +``` + +Use a `value` target to set every occupied cell to a constant, `max` with an +optional `min` to scale by extrema, or `mean` and `sd` to scale by moments. +Omit `calibration` only when you want the raw DUET values. + +## Uniform grids + +To fill the whole domain with constant band values at a chosen resolution — +useful for testing or for holding a fuel parameter constant: + +```python +grid = ff.grids.create_uniform_grid( + domain, + resolution_m=2.0, + bands={"fuel_load": 0.5, "fuel_moisture": 15.0}, +) +``` + +## Grids from your own raster files + +To upload a local NetCDF whose CRS matches the domain CRS: + +```python +grid = ff.grids.create_grid_from_netcdf(domain, "fuels.nc") +grid.wait() +``` + +GeoTIFF uploads work the same way but additionally take a `bands` list of +`UploadBandDefinition`s mapping 1:1 to the raster's bands in order: + +```python +from fastfuels_sdk.v2.client_library.models import UploadBandDefinition + +grid = ff.grids.create_grid_from_geotiff( + domain, + "elevation.tif", + bands=[UploadBandDefinition(key="elevation", interpolation="bilinear")], +) +``` + +The SDK creates the grid, uploads the file to the returned signed URL, and +hands back a pending grid. + +## Align grids to each other + +The worked example above gave every grid the same `output_resolution_m` to +land them on the domain lattice. Most creators (everything except FCCS and +uploads) accept four arguments that control the output lattice; they are +mutually exclusive — passing more than one raises `ValueError`: + +- **`output_resolution_m`** — resample to this cell size on the **domain** + lattice. The common case, and what makes grids of equal resolution stack. +- **`align="native"`** — keep the source's native resolution and lattice. +- **`align_to=`** — match another grid's lattice exactly (pass a + `Grid` or its id), so the two stack even at the source's native + resolution. +- **`resampling`** — the method used when changing resolution: `nearest`, + `bilinear`, `cubic`, `average`, `mode`, `min`, `max`, `median`, and more. + An unrecognized value raises `ValueError`. + +```python +# Stack a canopy grid onto an existing topography grid's lattice exactly +canopy = ff.grids.create_canopy_fuel_grid_from_landfire( + domain, align_to=topography, resampling="nearest" +) +``` + +## Mask out features + +To overwrite grid cells that fall within a feature — the v2 replacement for +v1's `feature_masks` — build a mask with `ff.mask` and pass it in a creator's +`modifications` list. For example, to set the cells under roads to a +non-burnable fuel model code (FBFM 91): + +```python +roads = ff.features.create_road_feature_from_osm(domain) +roads.wait() + +grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + domain, + output_resolution_m=30, + modifications=[ff.mask(roads, "fbfm", 91, buffer_m=5)], +) +``` + +The feature must be `completed` and in the same domain as the grid. Roads +and other linestrings usually need a `buffer_m` (or `target="cell"`) so the +thin geometry covers whole cells. See `ff.mask` in the +[Reference](../reference.md) for masking multiple bands and the `operator` +and `target` options. + +The same masks apply to a grid you already hold via +[`grid.apply_modifications`](working-with-grids.md#apply-modifications-to-a-grid), +which re-derives the grid in place. + +## Error handling + +Creators raise typed exceptions from `fastfuels_sdk.v2.exceptions` on invalid +input: + +```python +from fastfuels_sdk.v2.exceptions import UnprocessableEntityException + +try: + grid = ff.grids.create_uniform_grid(domain, resolution_m=2.0, bands={"bad": 1}) +except UnprocessableEntityException as exc: + print(exc.detail) +``` + +See [Working with grids](working-with-grids.md#error-handling) for the full +exception model. + +## Next steps + +A freshly created grid is still a running job. [Working with +grids](working-with-grids.md) covers waiting on it, inspecting its bands and +georeference, resampling, exporting, and managing it. diff --git a/docs/v2/guides/domains.md b/docs/v2/guides/domains.md new file mode 100644 index 0000000..7a40a0b --- /dev/null +++ b/docs/v2/guides/domains.md @@ -0,0 +1,203 @@ +# How to Work with Domains in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +A domain is the spatial container every other FastFuels resource lives in. +This guide covers working with domains from Python; for what domains *are* +and how the platform treats them, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). Coming +from the v1 SDK? Start with the [migration guide](migration.md#domains). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key + +The v2 SDK reads your API key from the `FASTFUELS_API_KEY` environment +variable, the same variable the v1 SDK uses: + +```bash +export FASTFUELS_API_KEY="your-api-key" +``` + +Or set it programmatically: + +```python +from fastfuels_sdk.v2 import set_api_key + +set_api_key("your-api-key") +``` + +## Create a Domain from GeoJSON + +To create a domain from a GeoJSON file: + +```python +import json +from fastfuels_sdk.v2.domains import Domain + +with open("area.geojson") as f: + geojson = json.load(f) + +domain = Domain.from_geojson( + geojson=geojson, + name="My Domain", + description="Forest area for analysis", + pad_to_resolution=2.0, +) +``` + +The v2 API accepts a GeoJSON FeatureCollection; the SDK automatically +wraps a single Feature for you. + +## Create a Domain from a GeoDataFrame or a File + +If your spatial data is in a format supported by GeoPandas (Shapefile, +KML, GeoPackage, etc.): + +```python +import geopandas as gpd +from fastfuels_sdk.v2.domains import Domain + +gdf = gpd.read_file("forest_area.shp") + +domain = Domain.from_geodataframe( + geodataframe=gdf, + name="Forest Domain", + description="Imported from shapefile", +) + +# Or skip the GeoPandas step entirely: +domain = Domain.from_file("forest_area.shp", name="Forest Domain") +``` + +## Preview a Domain Before Creating It + +`Domain.preview` runs the same validation and projection pipeline as +creation but persists nothing — useful for showing a user the projected, +padded bounding box before committing: + +```python +previewed = Domain.preview(geojson, pad_to_resolution=2.0) + +print(previewed.id) # always "preview" — not a real identifier +print(previewed.bbox) # the projected, padded bounding box +``` + +## Retrieve an Existing Domain + +To fetch a domain using its ID: + +```python +domain = Domain.from_id("abc123") +``` + +## Update Domain Properties + +To modify a domain's name, description, or tags: + +```python +domain.update( + name="New Name", + description="Updated description", + tags=["forest", "analysis"], +) +``` + +`update` changes the domain in place and returns it, so calls chain. Only +the fields you pass are sent; passing none makes no API call. + +## Refresh Domain Data + +To reload a domain's latest state from the API in place: + +```python +domain.refresh() +``` + +`refresh` updates the domain in place and returns it. To fetch a separate +copy by ID instead, use `Domain.from_id(domain.id)`. + +## Get the Pixel Lattice for a Domain + +To align a raster with the grids FastFuels will produce for a domain, +fetch its pixel lattice (affine transform + shape) at a resolution: + +```python +lattice = domain.get_lattice(resolution=2.0) + +print(lattice.crs) # e.g. "EPSG:32611" +print(lattice.transform) # affine coefficients [a, b, c, d, e, f] +print(lattice.shape) # [height, width] in pixels + +# Expand the lattice by N cells on each side, mirroring the +# extent_buffer_cells semantics of the grid creation endpoints +buffered = domain.get_lattice(resolution=2.0, num_buffer_cells=5) +``` + +## Reproject GeoJSON + +A stateless utility — no resource is created: + +```python +from fastfuels_sdk.v2.domains import reproject_geojson + +projected = reproject_geojson(geojson, target_epsg=5070) +print(projected["crs"]["properties"]["name"]) # "EPSG:5070" +``` + +## List Available Domains + +To list domains with pagination: + +```python +from fastfuels_sdk.v2.domains import list_domains + +# Get first page with default size (100) +domains = list_domains() + +# Custom page and size +domains = list_domains( + page=2, + size=50, + sort_by="name", + sort_order="ascending", +) + +for domain in domains: + print(f"{domain.id}: {domain.name}") +``` + +## Delete a Domain + +To permanently delete a domain and all resources associated with it: + +```python +domain.delete() +``` + +## Error Handling + +Wrapper methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +from fastfuels_sdk.v2.domains import Domain +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +try: + domain = Domain.from_id("does-not-exist") +except NotFoundException: + print("No such domain (or you don't have access to it)") + +try: + domain = Domain.from_geojson(too_big_geojson) +except UnprocessableEntityException as exc: + print(f"Invalid domain: {exc.detail}") +``` diff --git a/docs/v2/guides/exports.md b/docs/v2/guides/exports.md new file mode 100644 index 0000000..7017c96 --- /dev/null +++ b/docs/v2/guides/exports.md @@ -0,0 +1,208 @@ +# How to Export Data from FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +An export packages a resource's data into a downloadable file: a +[grid](working-with-grids.md) as GeoTIFF/NetCDF/zarr, an +[inventory](inventories.md) as Parquet/CSV/GeoJSON/GeoPackage, or several +grids assembled into a fire-behavior landscape or QUIC-Fire-loadable archive. +Exports run as background jobs and expose a signed download URL on completion. + +The pattern is the same everywhere: exporting a resource you **hold** is a +method on it (`grid.export(...)`, `inventory.export(...)`); the QUIC-Fire +bundle and landscape are **assembled from many** grids, so they are +module-level functions (`ff.exports.create_quicfire_export(...)` and +`ff.exports.create_landscape_export(...)`). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- A completed resource to export — a [grid](creating-grids.md) or an + [inventory](inventories.md) + +## Export and download a grid + +To export a completed grid and save the file, chain create → `wait` → +`to_file`: + +```python +import fastfuels_sdk.v2 as ff + +export = grid.export(format="geotiff") # or "netcdf", "zarr" +export.wait().to_file("elevation.tif") +``` + +`grid.export` returns a pending [`Export`](#work-with-an-export-you-hold); +`wait()` blocks until the file is packaged, and `to_file` streams it to +disk. GeoTIFF applies to 2D grids; export 3D voxel grids as `"netcdf"` or +`"zarr"`. Pass `bands=` to export a subset of the grid's bands. + +If `to_file` is given an existing directory, the file lands inside it +under the export's default filename: + +```python +>>> export.wait().to_file("outputs/") +PosixPath('outputs/export.tif') +``` + +## Export and download an inventory + +The same flow exports an inventory's tree records — `"parquet"` (zipped, +default), `"csv"`, `"geojson"`, or `"geopackage"`, with `columns=` for a +column subset: + +```python +export = inventory.export(format="csv") +path = export.wait().to_file("trees.csv") +``` + +```python +>>> path.read_text().splitlines()[0] +'x,y,fia_species_code,fia_status_code,dbh,height,crown_ratio' +``` + +## Create a fire-behavior landscape + +To create an 8-band LANDFIRE-style GeoTIFF for FlamMap, IFTDSS, or WFDSS, +assign the terrain, fuel-model, and canopy roles to completed 30 m grids: + +```python +export = ff.exports.create_landscape_export( + domain, + fire_behavior_fuel_model="fbfm40", + elevation=(topography, "elevation"), + slope=(topography, "slope"), + aspect=(topography, "aspect"), + fuel_model=(fuel_models, "fbfm"), + canopy_cover=(canopy, "cc"), + canopy_height=(canopy, "chm"), + canopy_base_height=(canopy, "cbh"), + canopy_bulk_density=(canopy, "cbd"), + name="Landscape", +) +export.wait().to_file("landscape.tif") +``` + +Use `fire_behavior_fuel_model="fbfm13"` with an FBFM13 source. To export a +different domain-anchored cell size, pass `resolution_m=` after building all +role grids at that resolution. To preserve an existing lattice instead, pass +`align_to=`. The exporter can crop oversized inputs but does not +resample or reproject them; use the grid alignment options before exporting +if the API reports an alignment error. + +## Bundle grids for QUIC-Fire + +`create_quicfire_export` packages surface fuel, canopy fuel, and +(optionally) topography grids into a zip archive that QUIC-Fire loads +directly. Each role is a `(grid, band)` pair; the five required roles +produce `treesrhof.dat`, `treesmoist.dat`, and `treesfueldepth.dat`: + +```python +export = ff.exports.create_quicfire_export( + domain, + canopy_bulk_density=(voxels, "bulk_density.foliage.live"), + canopy_moisture=(voxels, "fuel_moisture.live"), + surface_fuel_load=(surface, "fuel_load.1hr"), + surface_fuel_depth=(surface, "fuel_depth"), + surface_moisture=(surface, "fuel_moisture.1hr"), + name="QUIC-Fire bundle", +) +export.wait().to_file("outputs/") +``` + +```python +>>> import zipfile +>>> sorted(zipfile.ZipFile("outputs/QUIC-Fire_bundle.zip").namelist()) +['domain.geojson', 'metadata.json', 'treesfueldepth.dat', 'treesmoist.dat', 'treesrhof.dat'] +``` + +Here `voxels` is a [3D tree fuel grid](inventories.md#voxelize-into-a-3d-fuel-grid) +carrying bulk density and moisture bands, and `surface` is any 2D grid +carrying the surface roles (the example above pairs naturally with a +[uniform grid](creating-grids.md#uniform-grids) or an +[FBFM40 lookup grid](creating-grids.md#surface-fuel-model-grids)). For an +end-to-end walkthrough that builds all of these grids from real data sources +and bundles them, follow the +[Export QUIC-Fire inputs tutorial](../tutorials/export_to_quicfire.md). + +Two optional roles extend the bundle: `topography=(grid, "elevation")` +adds `topo.dat`, and the SAVR pair (`canopy_savr=` + `surface_savr=`, +both or neither) adds `treesss.dat`. + +The fire grid — the lattice everything is sliced onto — defaults to the +domain bounding box at 2 m horizontal / 1 m vertical. Change it with +`horizontal_resolution_m=` / `vertical_resolution_m=`, or define it from +an existing grid's lattice with `align_to=`. Every role grid must be +lattice-aligned with the fire grid and cover its full extent — the +exporter crops oversized grids by integer slicing but never resamples, so +build the roles on the fire grid's lattice (see +[Align grids to each other](creating-grids.md#align-grids-to-each-other)). + +## Work with an export you hold + +An `Export` is a job resource like any other: `wait(timeout=, verbose=)` +polls it to a terminal status (raising `JobFailedError` on failure), +`refresh()` reloads it in place, `update(name=, description=, tags=)` +edits its metadata, and `delete()` removes it along with the packaged +file. + +The signed download URL fills in on completion and expires after +`expiration_days` (max 7, the default): + +```python +>>> export.signed_url[:50] +'https://storage.googleapis.com/silvx-fastfuels-exp' +>>> export.expires_on +datetime.datetime(2026, 6, 17, 15, 5, 46, 885537, tzinfo=datetime.timezone.utc) +``` + +Downloading before the job completes raises a `ValueError`: + +```python +>>> pending_export.to_file("outputs/") +ValueError: Cannot download an export with status 'pending'. Call .wait() until it completes first. +``` + +Exports are addressed by their ID alone — no domain in the path: + +```python +export = ff.get_export("4a56bae0cd5e481aa1617cb894a9a7f3") +``` + +## List exports + +To list your exports, optionally narrowed to a domain, a source name +(the format, `"landscape"`, or `"quicfire"`), or a tag: + +```python +exports = ff.list_exports(domain) +landscapes = ff.list_exports(source="landscape") +bundles = ff.list_exports(source="quicfire") +tagged = ff.list_exports(tag="run-42") +``` + +## Error handling + +Wrapper functions and methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import NotFoundException + +try: + export = ff.get_export("does-not-exist") +except NotFoundException: + print("No such export (or you don't have access to it)") +``` + +## Next steps + +- Build the grids that feed an export — see + [Creating grids](creating-grids.md) and + [Inventories](inventories.md) +- Full signatures — see the [Reference](../reference.md) diff --git a/docs/v2/guides/features.md b/docs/v2/guides/features.md new file mode 100644 index 0000000..ed41077 --- /dev/null +++ b/docs/v2/guides/features.md @@ -0,0 +1,289 @@ +# How to Work with Features in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +Features are geographic data within a domain: roads and water bodies +extracted from OpenStreetMap, or custom layersets of fuelbed polygons you +upload yourself. This guide covers working with features from Python; for +what features *are* and how the platform treats them, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). Coming +from the v1 SDK? Start with the [migration guide](migration.md#features). + +The v2 surface is functional: you **create** a feature by calling a +`create_..._feature_from_...` function on a domain, and everything you do +with a feature you already hold is a **method** on it. + +```python +import fastfuels_sdk.v2 as ff + +feature = ff.features.create_road_feature_from_osm(domain) +feature.wait() +``` + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- An existing [domain](domains.md) to create features in + +## Create a Road Feature from OpenStreetMap + +To extract the road network within a domain's extent: + +```python +import fastfuels_sdk.v2 as ff + +feature = ff.features.create_road_feature_from_osm( + domain, + name="My Roads", + description="OSM road network", +) + +print(feature.status) # pending +``` + +Feature generation runs as a background job — the returned feature starts +in `"pending"` status. To include roads just outside the domain, buffer +the query extent by up to 100 meters with `extent_buffer_m`: + +```python +feature = ff.features.create_road_feature_from_osm(domain, extent_buffer_m=50) +``` + +The first argument accepts either a `Domain` or a bare domain id string. + +## Create a Water Feature from OpenStreetMap + +To extract water bodies, use the same calling convention: + +```python +feature = ff.features.create_water_feature_from_osm(domain, name="My Water") +``` + +## Wait for a Feature to Complete + +To block until a feature job finishes: + +```python +feature.wait(verbose=True) +``` + +```text +Feature 36573c0205d147a08011693b7894c0fb: pending (5s) +Feature 36573c0205d147a08011693b7894c0fb: running (15s) +Feature 36573c0205d147a08011693b7894c0fb: completed (20s) +``` + +`wait` polls until the job reaches a terminal status and returns the +feature (so calls chain). By default it waits indefinitely; pass `timeout` +(seconds) to bound the wait, which raises `TimeoutError` if exceeded. A +failed job raises `JobFailedError`, carrying the API's `code`, `message`, +and `suggestion`. To wait on several jobs at once, use `ff.wait_all`: + +```python +roads = ff.features.create_road_feature_from_osm(domain) +water = ff.features.create_water_feature_from_osm(domain) + +ff.wait_all([roads, water]) # jobs run server-side in parallel +``` + +Once completed, the feature's `georeference` reports the CRS and bounds of +the generated data: + +```python +print(feature.georeference.crs) # 'EPSG:32611' +print(feature.georeference.bounds) # [720192.0, 5189446.0, 721918.0, 5190852.0] +``` + +## Upload a Custom Layerset + +A layerset is a FeatureCollection of fuelbed polygons you supply +yourself. Unlike OSM features, the upload is synchronous — the returned +feature is already `"completed"`. + +!!! warning "Projected CRS required" + The FeatureCollection's `crs` member must declare a **projected** CRS + (e.g. EPSG:5070 or a UTM zone). Geographic coordinates (EPSG:4326) + are rejected, because rasterization requires cell sizes in meters. + +Each polygon's `properties` must carry the fuelbed input columns +`fuel_type`, `fuel_loading`, `fuel_height`, `percent_cover`, and +`distribution` (`"homogeneous"`, `"random_clusters"`, or +`"uniform_random"`): + +```python +import json +import fastfuels_sdk.v2 as ff + +with open("fuelbeds.geojson") as f: # projected CRS + fuelbed properties + geojson = json.load(f) + +feature = ff.features.create_layerset_feature_from_geojson( + domain, geojson, name="My Fuelbeds" +) + +print(feature.status) # completed +``` + +To upload from a GeoPandas GeoDataFrame instead, reproject first if +needed — the GeoDataFrame's CRS is forwarded as-is: + +```python +import geopandas as gpd + +gdf = gpd.read_file("fuelbeds.shp").to_crs(epsg=5070) +feature = ff.features.create_layerset_feature_from_geodataframe(domain, gdf) +``` + +## Rasterize a Layerset into a Grid + +A completed layerset feature carries vector fuelbed polygons; to burn them +onto a raster grid, call `rasterize`. It returns a pending +[`Grid`](working-with-grids.md): + +```python +grid = feature.rasterize(output_resolution_m=2.0) +grid.wait() +``` + +`rasterize` takes the same alignment arguments as the grid creators +(`output_resolution_m`, `align_to`, `align`, `resampling`) plus an +`overlap_method` controlling how overlapping polygons resolve. See +[Align grids to each other](creating-grids.md#align-grids-to-each-other) +for the alignment model. + +## Retrieve an Existing Feature + +To fetch a feature using its domain and feature IDs: + +```python +feature = ff.get_feature(domain, "36573c0205d147a08011693b7894c0fb") +``` + +## Refresh Feature Data + +To reload a feature's latest state from the API (for example, to check job +progress yourself) in place: + +```python +feature.refresh() +print(feature.status) +``` + +`refresh` updates the feature in place and returns it. To fetch a separate +copy by ID instead, use `ff.get_feature(domain, feature.id)`. + +## Access Feature Data + +The generated geodata is served in partitions. For most workflows, +retrieve everything at once as a GeoDataFrame: + +```python +gdf = feature.to_geodataframe() + +print(len(gdf)) # 43 +print(gdf.crs) # EPSG:32611 +``` + +Or as a single GeoJSON FeatureCollection: + +```python +data = feature.get_data() +print(len(data["features"])) # 43 +``` + +To control retrieval partition by partition (useful for large features), +read the partition layout first: + +```python +metadata = feature.get_data_metadata() +print(metadata.total_features) # 43 +print(metadata.partition_count) # 1 + +for index in range(metadata.partition_count): + partition = feature.get_data_partition(index) # GeoJSON FeatureCollection +``` + +Data is only available once the feature is `"completed"` — earlier calls +raise `UnprocessableEntityException`. + +## Update Feature Properties + +To modify a feature's name, description, or tags: + +```python +feature.update(name="New Name", tags=["roads", "osm"]) +``` + +`update` changes the feature in place and returns it. Only the fields you +pass are sent; passing none makes no API call. + +## List Features + +To list features in a domain: + +```python +features = ff.list_features(domain) +``` + +To list features across all your domains, omit the domain: + +```python +all_features = ff.list_features() +``` + +Narrow the results with filters: + +```python +roads = ff.list_features(domain, feature_type="road") +osm_features = ff.list_features(domain, product="osm") +tagged = ff.list_features(domain, tag="roads") +``` + +Sort a page by name, creation time, or modification time: + +```python +newest_first = ff.list_features( + domain, + sort_by="created_on", + sort_order="descending", +) +``` + +## Delete a Feature + +To permanently delete a feature and its generated data: + +```python +feature.delete() +``` + +Deleting a domain also deletes all of its features. + +## Error Handling + +Wrapper functions and methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +try: + feature = ff.get_feature(domain, "does-not-exist") +except NotFoundException: + print("No such feature (or you don't have access to it)") + +try: + metadata = pending_feature.get_data_metadata() +except UnprocessableEntityException as exc: + print(exc.detail) + # features/36573c0205d147a08011693b7894c0fb status is 'pending', + # expected 'completed'. +``` diff --git a/docs/v2/guides/inventories.md b/docs/v2/guides/inventories.md new file mode 100644 index 0000000..51c6a91 --- /dev/null +++ b/docs/v2/guides/inventories.md @@ -0,0 +1,419 @@ +# How to Work with Tree Inventories in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +An inventory is a table of individual trees within a domain — one row per +tree, with its coordinates, species, diameter, height, and crown ratio. +Inventories sit between grids: they are generated *from* a grid (a PIM grid +or a canopy height model) or from your own data, and they are voxelized +*into* the 3D canopy fuel grid that 3D fire models consume. This guide +covers working with inventories from Python; for what inventories *are*, +see the [FastFuels documentation](https://docs.fastfuels.silvxlabs.com). +Coming from the v1 SDK? Start with the +[migration guide](migration.md#inventories). + +The v2 surface is functional: you **create** an inventory by calling a +`create_tree_inventory_from_...` function on a domain, and everything you +do with an inventory you already hold is a **method** on it. + +```python +import fastfuels_sdk.v2 as ff + +inventory = ff.inventories.create_tree_inventory_from_pim_grid(domain, pim) +trees = inventory.wait().to_dataframe() +``` + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- An existing [domain](domains.md) — every creator's first argument is a + `Domain` (or a bare domain id string) + +## From TreeMap to trees + +The most common workflow generates a tree inventory for anywhere in the +conterminous US: create a [PIM grid](creating-grids.md#3d-tree-fuel-grids-voxelization) +that matches each cell to a TreeMap forest inventory plot, then expand the +matched plots into individual trees. Pass a `seed` to make the expansion +reproducible: + +```python +import fastfuels_sdk.v2 as ff + +pim = ff.grids.create_pim_grid_from_treemap( + domain, output_resolution_m=30, resampling="nearest" +) +pim.wait() + +inventory = ff.inventories.create_tree_inventory_from_pim_grid( + domain, pim, seed=42, name="Blue Mountain trees" +) +inventory.wait() +``` + +Expansion runs as a background job — the returned inventory starts in +`"pending"` status and `wait()` blocks until it is ready. Once completed, +load the trees as a pandas DataFrame: + +```python +>>> trees = inventory.to_dataframe() +>>> len(trees) +71619 +>>> trees.head() + x y fia_species_code ... dbh height crown_ratio +0 721167.447037 5.190627e+06 122 ... 10.160 6.7056 0.10 +1 721169.886263 5.190628e+06 122 ... 22.606 10.3632 0.80 +2 721172.889178 5.190630e+06 122 ... 10.160 6.7056 0.10 +3 721167.558322 5.190629e+06 122 ... 8.128 4.5720 0.08 +4 721177.067389 5.190639e+06 122 ... 8.128 4.5720 0.08 +``` + +Coordinates are in the domain CRS in meters, `dbh` is in centimeters, and +`height` is in meters; the inventory's `columns` attribute records each +column's type and unit. + +## Create an inventory from a canopy height model + +To detect individual trees in a completed +[canopy height model grid](creating-grids.md#canopy-grids) instead — useful +where you want trees derived from remotely sensed canopy structure: + +```python +chm = ff.grids.create_canopy_height_grid_from_meta(domain, output_resolution_m=1) +chm.wait() + +inventory = ff.inventories.create_tree_inventory_from_chm_grid(domain, chm) +inventory.wait() +``` + +Stem isolation defaults to local maximum filtering with a 2 m minimum +height. To tune the algorithm, pass a `StemIsolationLmf` or +`StemIsolationVwf` (variable window filtering) model as `algorithm=`. A +CHM-derived inventory carries only what the canopy surface reveals — `x`, +`y`, and `height` columns — so it cannot be voxelized directly (voxelization +needs the per-tree measurements a PIM expansion or an upload provides). + +## Upload your own tree records + +To create an inventory from your own measurements, upload a `.csv`, +`.geojson`, or `.gpkg` file. Coordinates must be in the domain's CRS: + +```python +inventory = ff.inventories.create_tree_inventory_from_file( + domain, "plot_trees.csv", name="Field plot" +) +inventory.wait() +``` + +The standard column roles are `x`, `y`, `height` (m), `dbh` (cm), +`crown_ratio`, `fia_species_code`, and `fia_status_code`. Columns whose +names already match need no mapping; otherwise map your file's column names +onto the roles: + +```python +inventory = ff.inventories.create_tree_inventory_from_file( + domain, + "plot_trees.csv", + columns={"x": "X_UTM", "y": "Y_UTM", "dbh": "DBH_CM", "height": "HT_M"}, +) +``` + +The upload itself is synchronous, but processing the file runs as a +background job — `wait()` before reading data back: + +```python +>>> inventory.wait().to_dataframe() + x y height ... crown_ratio fia_species_code fia_status_code +0 721055.0 5190149.0 12.0 ... 0.40 122 1 +1 721065.0 5190159.0 8.5 ... 0.50 122 1 +2 721075.0 5190169.0 15.2 ... 0.35 202 1 +``` + +## Impute missing morphology with GDAM + +An uploaded inventory may carry only some columns — coordinates and heights, +say, without diameters or species. `create_tree_inventory_from_gdam` fills in +the missing morphology (`dbh`, `crown_ratio`, `fia_species_code`) for a +completed inventory using GDAM allometry, producing a new inventory: + +```python +sparse = ff.inventories.create_tree_inventory_from_file(domain, "stems.csv") +sparse.wait() + +full = ff.inventories.create_tree_inventory_from_gdam(domain, sparse) +full.wait() +``` + +Existing values are preserved — only missing cells are imputed. Pass +`impute_columns=["fia_species_code"]` to impute just a subset. + +## Wait for an inventory to finish + +`wait` polls until the job reaches a terminal status and returns the +inventory (so calls chain). By default it waits indefinitely; pass +`timeout` (seconds) to bound the wait, which raises `TimeoutError` if +exceeded. A failed job raises `JobFailedError`. To join several jobs at +once, use `ff.wait_all`: + +```python +ff.wait_all([pim, inventory], verbose=True) +``` + +```text +Inventory 7b02df6f583a418da3ec9929037d876d: completed (5s) +``` + +## Access the tree records + +For most workflows, `to_dataframe()` (shown above) is all you need — it +retrieves each partition through the CSV endpoint, parses it with pandas, and +assembles one DataFrame. Pass `columns=` to retrieve a subset. + +The records are served in fixed-size partitions; to control retrieval +partition by partition (useful for large inventories), read the partition +layout first: + +```python +>>> metadata = inventory.get_data_metadata() +>>> metadata.total_rows +71619 +>>> metadata.num_partitions +4 +>>> partition = inventory.get_data_partition(0) +>>> partition.num_rows +34831 +``` + +`get_data_partition` uses the compact `"split"` JSON layout by default, where +each row in `partition.data` follows `partition.columns`. To receive +self-describing row mappings instead, request the `"records"` layout: + +```python +partition = inventory.get_data_partition( + 0, columns=["x", "y", "height"], json_orientation="records" +) +first_tree = partition.data[0] +``` + +Data is only available once the inventory is `"completed"` — earlier calls +raise `UnprocessableEntityException`. + +## Summarize a column without downloading records + +Once an inventory completes, inspect a column's server-computed summary by +passing its key to `column_summary`: + +```python +dbh = inventory.column_summary("dbh") +mean_dbh = dbh.mean +minimum_dbh = dbh.min_ +maximum_dbh = dbh.max_ + +species = inventory.column_summary("fia_species_code") +species_count = species.unique_count +``` + +A continuous column reports `count`, `null_count`, `min_`, `max_`, `mean`, and +`std`; a categorical column reports `count`, `null_count`, and `unique_count`. +The `type_` field identifies the shape. `column_summary` returns `None` when +the requested column exists but its summary is unavailable; an unknown column +key raises `ValueError`. + +## Inspect stand-level forestry metrics + +Once a tree inventory completes, read its server-computed forestry metrics +without downloading the tree records: + +```python +metrics = inventory.forestry_metrics + +tree_count = metrics.tree_count +basal_area_per_acre = metrics.basal_area_per_area +trees_per_acre = metrics.tree_density +quadratic_mean_diameter_inches = metrics.quadratic_mean_diameter + +dominant_groups = [ + (group.spgrpcd, group.name, group.basal_area_share) + for group in metrics.dominant_species_groups +] +``` + +`dominant_species_groups` is ordered by decreasing basal-area share and +contains the leading FIA species groups. Only the leading groups are returned, +so their shares may sum to less than one. `forestry_metrics` is `None` when +metrics are unavailable, including before processing completes. + +## Reshape the trees + +Every creator accepts `modifications=` (rules that filter trees by conditions +and act on them) and `treatments=` (silvicultural thinning to a target), and +you can reshape an inventory you already hold with `apply_modifications` / +`apply_treatments`. See +[Modify and treat tree inventories](modify-treat-inventories.md) for the full +workflow. + +## Duplicate an inventory + +`duplicate` makes an independent copy under a new ID, byte-copying the finished +data rather than re-deriving it, so the copy starts identical to the source: + +```python +>>> copy = inventory.duplicate(name="Scenario A") +>>> copy.wait() +>>> copy.checksum == inventory.checksum +True +``` + +The `checksum` is a version marker for an inventory's content: it changes each +time the data is rebuilt and is unaffected by metadata-only edits, so an +identical checksum means identical trees. Duplicate before reshaping to keep +the original untouched (see +[Branch a scenario](modify-treat-inventories.md#branch-a-scenario)). + +## Voxelize into a 3D fuel grid + +A completed PIM-expanded or uploaded inventory voxelizes into the +[3D tree fuel grid](creating-grids.md#3d-tree-fuel-grids-voxelization) — +per-voxel canopy bulk density on a 3D lattice: + +```python +voxels = inventory.voxelize( + horizontal_resolution_m=2.0, vertical_resolution_m=1.0 +) +voxels.wait() +``` + +```python +>>> [b.key for b in voxels.bands] +['bulk_density.foliage.live'] +>>> voxels.georeference.crs +'EPSG:32611' +>>> voxels.georeference.shape # [layers, rows, cols] +[37, 703, 863] +``` + +By default each tree's foliage biomass is distributed with the Purves +crown profile model using NSVB allometry. Request more bands with +`bands=` (e.g. `"bulk_density.branchwood.live"`, `"fuel_moisture.dead"`, +`"spcd"`, `"tree_id"`), switch the crown shape with +`crown_profile_model="beta"`, and pass a `seed` for reproducibility. +Because the result is a 3D product, it supports neither resampling nor +grid modifications — modify the trees on the inventory instead, before +voxelizing. + +## Export an inventory + +To export the tree records to a downloadable file and save it, call +`export` with a format — `"parquet"` (zipped, default), `"csv"`, +`"geojson"`, or `"geopackage"` — and chain the export job's `wait` into +`to_file`: + +```python +export = inventory.export(format="csv") +export.wait().to_file("trees.csv") +``` + +Pass `columns=` to export a column subset. For managing exports, see the +[Exports guide](exports.md). + +## Retrieve an existing inventory + +To fetch an inventory using its domain and inventory IDs: + +```python +inventory = ff.get_inventory(domain, "7b02df6f583a418da3ec9929037d876d") +``` + +## Refresh and update + +To reload an inventory's latest state from the API in place: + +```python +inventory.refresh() +print(inventory.status) +``` + +To modify an inventory's name, description, or tags: + +```python +inventory.update(name="New Name", tags=["thinned"]) +``` + +Both update the inventory in place and return it. `update` sends only the +fields you pass; passing none makes no API call. + +## List inventories + +To list inventories in a domain: + +```python +inventories = ff.list_inventories(domain) +``` + +To list inventories across all your domains, omit the domain: + +```python +all_inventories = ff.list_inventories() +``` + +Narrow the results with filters: + +```python +pim_inventories = ff.list_inventories(domain, source="pim") +tagged = ff.list_inventories(domain, tag="thinned") +``` + +## Delete an inventory + +To permanently delete an inventory and its tree records: + +```python +inventory.delete() +``` + +Deleting a domain also deletes all of its inventories. + +## Error handling + +Wrapper functions and methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +try: + inventory = ff.get_inventory(domain, "does-not-exist") +except NotFoundException: + print("No such inventory (or you don't have access to it)") + +try: + trees = running_inventory.to_dataframe() +except UnprocessableEntityException as exc: + print(exc.detail) + # inventories/a6840e0c7cd64de896954d95afc6aea0 status is 'running', + # expected 'completed'. +``` + +Deriving from an inventory that is not completed raises a `ValueError` +before any API call: + +```python +>>> pending_inventory.voxelize(horizontal_resolution_m=2.0, vertical_resolution_m=1.0) +ValueError: Cannot voxelize an inventory with status 'pending'. Call .wait() until it completes first. +``` + +## Next steps + +- Voxelize alongside aligned 2D grids — see + [Creating grids](creating-grids.md) +- Wait on, inspect, and manage the voxelized grid — see + [Working with grids](working-with-grids.md) +- Full signatures — see the [Reference](../reference.md) diff --git a/docs/v2/guides/migration.md b/docs/v2/guides/migration.md new file mode 100644 index 0000000..dfb3426 --- /dev/null +++ b/docs/v2/guides/migration.md @@ -0,0 +1,318 @@ +# Migrating from v1 to v2 + +The FastFuels v1 and v2 APIs are separate live services. Resources created +in v1 are not visible to v2 (and vice versa) — migrating a workflow means +creating new resources through v2. The SDK ships both interfaces as +versioned subpackages, so you can keep v1 working while you migrate to v2 +one workflow at a time: + +```python +from fastfuels_sdk import Domain # v1 (the default today) +from fastfuels_sdk.v1 import Domain # v1, explicit +from fastfuels_sdk.v2 import Domain # v2 +``` + +Both subpackages read the same `FASTFUELS_API_KEY` environment variable. +v1 and v2 are separate deployments that issue **different keys**, though, so +a v1 key will not authenticate against v2 — set `FASTFUELS_API_KEY` to the +key for the version you are calling. + +!!! warning "One key per process" + Because both subpackages read the single `FASTFUELS_API_KEY` variable, + using v1 and v2 at the same time *in one process* with different keys is + not supported. Migrate one workflow at a time, pointing + `FASTFUELS_API_KEY` at the appropriate key for each run. + +## At a glance + +| v1 | v2 | What changed | +|---|---|---| +| `Domain.from_geojson(..., horizontal_resolution=2.0, vertical_resolution=1.0)` | `Domain.from_geojson(..., pad_to_resolution=2.0)` | Resolution belongs to grids now; domains only pad their extent for grid alignment | +| `domain.export()` | grid exports | No domain-level export in v2; data exports hang off grids | +| — | `Domain.preview`, `Domain.get_lattice`, `reproject_geojson` | New domain capabilities | +| `Grids`, `SurfaceGrid`, `TreeGrid`, `TopographyGrid`, `FeatureGrid` + builders | unified `Grid` resource | One job-based resource per grid, distinguished by data source | +| `Features`, `RoadFeature`, `WaterFeature` | unified `Feature` resource | One job-based resource for OSM roads, OSM water, and custom layersets | +| `feature.get_data()`, `feature.get_all_data()` | `feature.get_data_metadata()`, `feature.get_data_partition()`, `feature.get_data()`, `feature.to_geodataframe()` | Page-based data retrieval becomes partition-based | +| `Inventories`, `TreeInventory` | unified `Inventory` resource | One job-based resource, created from a PIM grid, a canopy height model, or an upload | + +## Domains + +Available now — see the [Domains guide](domains.md) and the +[Reference](../reference.md). + +### What changed from v1 + +- **Resolution moved to grids.** Domains no longer take + `horizontal_resolution`/`vertical_resolution`. Use the optional + `pad_to_resolution` argument to pad the domain bounding box outward so + grids of that resolution align with it. +- **Responses carry the working extent.** A v2 domain contains one named + GeoJSON feature, `"domain"`: the projected bounding box used by child + resources. Keep the original input geometry separately if you need it. +- **GeoDataFrame CRS is honored.** `from_geodataframe` forwards the + GeoDataFrame's CRS to the API, so projected inputs (e.g. EPSG:5070) + are interpreted correctly. The v1 SDK assumed EPSG:4326. +- **No `Domain.export()`.** The v2 API has no domain export endpoint; + data is exported through [grid and inventory exports](exports.md) + instead. +- **New endpoints.** `Domain.preview` validates and projects a domain + without creating it, `Domain.get_lattice` returns the pixel lattice + for grid alignment, and `reproject_geojson` is a stateless + reprojection utility. +- **Typed exceptions.** Errors raise `fastfuels_sdk.v2.exceptions` + classes (`NotFoundException`, `UnprocessableEntityException`, ...) + carrying the HTTP status code and API error detail. + +### Before and after + +=== "v1" + + ```python + from fastfuels_sdk import Domain + + domain = Domain.from_geojson( + geojson, + name="My Domain", + horizontal_resolution=2.0, + vertical_resolution=1.0, + ) + ``` + +=== "v2" + + ```python + from fastfuels_sdk.v2 import Domain + + domain = Domain.from_geojson( + geojson, + name="My Domain", + pad_to_resolution=2.0, + ) + ``` + +## Grids + +Available now — see [Creating grids](creating-grids.md), +[Working with grids](working-with-grids.md), and the +[Reference](../reference.md). + +### What changed from v1 + +- **One unified `Grid` resource.** v1's per-type resources + (`SurfaceGrid`, `TreeGrid`, `TopographyGrid`, `FeatureGrid`) and their + builders collapse into a single job-based `Grid` distinguished by its + data source. The `Grids.from_domain_id(...)` container is replaced by + the module-level `list_grids(domain)`. +- **Creation is a function per source.** Instead of a builder, call a + `create__grid_from_` function on the domain — e.g. + `ff.grids.create_topography_grid_from_3dep(...)` or + `ff.grids.create_fuel_model_grid_from_landfire_fbfm40(...)`. +- **Alignment is explicit.** Resolution and lattice are set per grid with + `output_resolution_m`, `align="native"`, `align_to=`, and + `resampling` (see + [Align grids to each other](creating-grids.md#align-grids-to-each-other)). +- **`feature_masks` becomes `modifications`.** v1's + `feature_masks=["road", "water"]` becomes + `modifications=[ff.mask(feature, band, value)]`, which overwrites the + cells a feature covers. +- **Universal transforms are methods; type-specific ones are functions.** + Transforms that apply to any grid you hold are methods — + `grid.resample(...)`, `grid.export(...)`. Deriving a fuel-parameter grid + from FBFM40 codes only applies to FBFM40 grids, so it is a function: + `ff.grids.create_fuel_grid_from_fbfm40_lookup(fbfm_grid, ...)`. + +### Before and after + +=== "v1" + + ```python + from fastfuels_sdk import Grids + + grids = Grids.from_domain_id(domain.id) + topo = grids.create_topography_grid( + attributes=["elevation", "slope", "aspect"] + ) + topo.wait_until_completed() + ``` + +=== "v2" + + ```python + import fastfuels_sdk.v2 as ff + + topo = ff.grids.create_topography_grid_from_3dep( + domain, output_resolution_m=10, bands=["elevation", "slope", "aspect"] + ) + topo.wait() + ``` + +## Features + +Available now — see the [Features guide](features.md) and the +[Reference](../reference.md). + +### What changed from v1 + +- **Road and water unify into one resource.** v1's `Features` container + with `.road`/`.water` sub-resources becomes a single job-based + `Feature` distinguished by its type ("road", "water", or "layerset"). + The `Features.from_domain_id(...)` container is replaced by the + module-level `list_features(domain_id)`. +- **Creation is a function per source.** The v1 + `features.create_road_feature_from_osm()` becomes the module-level + `ff.features.create_road_feature_from_osm(domain)`, and likewise + `create_water_feature_from_osm` for water. The new `extent_buffer_m` + argument buffers the OSM query extent by up to 100 meters. +- **User-supplied geometry becomes layersets.** v1's road-from-GeoJSON + path is gone. Instead, v2 accepts custom *layersets* — fuelbed + polygons carrying rasterizer properties — via + `ff.features.create_layerset_feature_from_geojson` and + `create_layerset_feature_from_geodataframe`. Layersets require a + projected CRS and upload synchronously, and a completed layerset can be + burned onto a grid with `feature.rasterize(...)`. +- **Data access is partitioned.** v1's `get_data(page, size)` / + `get_all_data()` become `get_data_metadata()` (partition layout), + `get_data_partition(index)` (one partition), and `get_data()` (all + partitions assembled) — plus `to_geodataframe()` for the common case + of loading everything into GeoPandas. +- **Cross-domain listing.** `list_features()` without a domain ID lists + features across all your domains, with `feature_type`, `product`, and + `tag` filters. +- **Typed exceptions.** Errors raise `fastfuels_sdk.v2.exceptions` + classes, the same as domains. + +### Before and after + +=== "v1" + + ```python + from fastfuels_sdk import Features + + features = Features.from_domain_id(domain.id) + road = features.create_road_feature_from_osm() + road.wait_until_completed(verbose=True) + + data = road.get_all_data() + ``` + +=== "v2" + + ```python + import fastfuels_sdk.v2 as ff + + road = ff.features.create_road_feature_from_osm(domain) + road.wait(verbose=True) + + roads = road.to_geodataframe() + ``` + +## Inventories + +Available now — see the [Inventories guide](inventories.md) and the +[Reference](../reference.md). + +### What changed from v1 + +- **TreeMap generation is a two-resource workflow.** v1's + `create_tree_inventory_from_treemap()` did the plot matching and the + tree expansion in one call. v2 splits them: create a PIM grid + (`ff.grids.create_pim_grid_from_treemap`), wait for it, then expand it + with `ff.inventories.create_tree_inventory_from_pim_grid(domain, pim)`. + The intermediate PIM grid is reusable — expand it several times with + different seeds or modifications without re-matching plots. +- **Creation is a function per source.** The `Inventories.from_domain_id` + container is gone; call `create_tree_inventory_from_pim_grid`, + `create_tree_inventory_from_chm_grid` (new — stem isolation on a canopy + height model), or `create_tree_inventory_from_file` on the domain. +- **Upload columns changed.** v1 uploads required `TREE_ID`, `SPCD`, + `STATUSCD`, `DIA`, `HT` columns. v2 uses the roles `x`, `y`, `height`, + `dbh`, `crown_ratio`, `fia_species_code`, `fia_status_code`, and a + `columns={role: your_name}` mapping replaces renaming your file. +- **Modifications and treatments are typed models.** v1's dict syntax + (`{"attribute": "HT", ...}`) becomes the generated + `InventoryModification` / treatment models, passed to `modifications=` / + `treatments=` at creation or to `inventory.apply_modifications(...)` + in place. v1's `feature_masks=["road", "water"]` becomes a modification + whose condition is a feature spatial condition + (`InventoryFeatureSpatialCondition`) and whose action is `RemoveAction`. +- **Data access is direct.** v1 exposed tree records only through file + exports (`create_export` → `to_file`). v2 streams them from the API: + `get_data_metadata()` / `get_data_partition(index)` — plus + `to_dataframe()` for the common case of loading every tree into pandas. + File exports remain available via `inventory.export(...)`. +- **Scenario branching is first-class.** `inventory.duplicate()` clones a + completed inventory, and `checksum` marks the data version, so derived + resources can detect a stale source. +- **Voxelization is a method.** The v1 tree grid built from an inventory + becomes `inventory.voxelize(horizontal_resolution_m=..., + vertical_resolution_m=...)`, returning a 3D `Grid`. + +## Exports + +Available now — see the [Exports guide](exports.md) and the +[Reference](../reference.md). + +### What changed from v1 + +- **Exports hang off grids and inventories.** v1's `domain.export()` and + per-resource `create_export`/`get_export` pairs become + `grid.export(format=...)` and `inventory.export(format=...)`, each + returning a job-based `Export`; chain `export.wait().to_file(path)` to + download. The lifecycle renames match the other resources + (`wait_until_completed` → `wait`). +- **`export_roi` becomes explicit creation + the QUIC-Fire bundle.** The + v1 convenience built every resource from an ROI and exported in one + call. In v2 you create the domain, grids, and inventory explicitly + (see the other guides), then bundle them with + `ff.exports.create_quicfire_export(domain, ...)` — naming the exact + grid and band filling each QUIC-Fire role (`canopy_bulk_density`, + `surface_fuel_load`, ...). The API packages the `.dat` archive + server-side, replacing v1's client-side zarr-to-QUIC-Fire conversion. +- **Exports are cross-domain resources.** `ff.get_export(export_id)` and + `ff.list_exports(...)` address exports by ID alone, with domain, + source, and tag filters. + +### Before and after + +=== "v1" + + ```python + export = tree_inventory.create_export("csv") + export = export.wait_until_completed() + export.to_file("trees.csv") + ``` + +=== "v2" + + ```python + inventory.export(format="csv").wait().to_file("trees.csv") + ``` + +### Before and after + +=== "v1" + + ```python + from fastfuels_sdk import Inventories + + inventories = Inventories.from_domain_id(domain.id) + trees = inventories.create_tree_inventory_from_treemap(seed=42) + trees.wait_until_completed() + + export = trees.create_export("csv") + export.wait_until_completed().to_file("trees.csv") # read the file back + ``` + +=== "v2" + + ```python + import fastfuels_sdk.v2 as ff + + pim = ff.grids.create_pim_grid_from_treemap(domain, output_resolution_m=30) + pim.wait() + + trees = ff.inventories.create_tree_inventory_from_pim_grid(domain, pim, seed=42) + trees.wait() + + data = trees.to_dataframe() + ``` diff --git a/docs/v2/guides/modify-treat-inventories.md b/docs/v2/guides/modify-treat-inventories.md new file mode 100644 index 0000000..b5898cb --- /dev/null +++ b/docs/v2/guides/modify-treat-inventories.md @@ -0,0 +1,135 @@ +# How to Modify and Treat Tree Inventories + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +Two tools reshape a tree inventory's stems: + +- **Modifications** — general rules that filter trees by *conditions* and + apply *actions* (remove, multiply, replace, …) to the matching rows. +- **Treatments** — silvicultural thinning prescriptions that remove stems + until the stand reaches a target basal area or diameter. + +Both can be applied two ways: at **creation**, via a creator's +`modifications=` / `treatments=` argument, or to an inventory you already +**hold**, via `apply_modifications` / `apply_treatments`. This guide covers +both. For creating and reading inventories in the first place, see +[Inventories](inventories.md). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- An [inventory](inventories.md) (held or being created) + +## Modifications + +A modification is a rule with two parts: `conditions` (all ANDed — a tree must +satisfy every one) and `actions` (applied to the matching trees). Build +conditions with `ff.tree_attribute` (a per-tree attribute test) or +`ff.tree_within` (trees inside a feature), then assemble the rule with +`ff.remove_trees` (drop the matching trees) or `ff.modify_trees` (change an +attribute on them): + +```python +import fastfuels_sdk.v2 as ff + +# Remove every tree under 10 cm DBH +ff.remove_trees(ff.tree_attribute("dbh", "<", 10)) + +# Shrink crowns on the largest trees inside a stand boundary +ff.modify_trees( + "crown_ratio", "multiply", 0.8, + ff.tree_attribute("dbh", ">", 40), + ff.tree_within(stand), +) +``` + +`tree_attribute` takes an attribute (`"dbh"`, `"height"`, `"crown_ratio"`, +`"fia_species_code"`) and a comparison (`"<"`, `"<="`, `">"`, `">="`, `"=="`, +`"!="`); pass several conditions to AND them. `modify_trees`'s modifier is +`"replace"`, `"add"`, `"subtract"`, `"multiply"`, or `"divide"`. + +## Treatments + +A treatment thins to a target. Build one with `ff.basal_area_treatment` +(residual basal area) or `ff.diameter_treatment` (diameter limit) rather than +hand-building the model: + +```python +import fastfuels_sdk.v2 as ff + +# Thin from below to a residual basal area of 25 m**2/ha +treatment = ff.basal_area_treatment("from_below", 25.0) + +# Or remove every stem under 10 cm dbh +treatment = ff.diameter_treatment("from_below", 10.0) +``` + +`method` is `"from_below"` (smallest first), `"from_above"` (largest first), +or — for basal area only — `"proportional"` (across all size classes). + +## Apply at creation + +Every `create_tree_inventory_from_*` function accepts `modifications=` and +`treatments=`; modifications run first, then treatments, while the inventory is +derived: + +```python +thinned = ff.inventories.create_tree_inventory_from_pim_grid( + domain, + pim, + seed=42, + modifications=[ff.remove_trees(ff.tree_attribute("dbh", "<", 10))], + treatments=[ff.basal_area_treatment("from_below", 25.0)], + name="Thinned", +) +thinned.wait() +``` + +## Apply to an inventory you hold + +`apply_modifications` and `apply_treatments` reshape an inventory **in place**: +its ID is kept and the data is re-derived as a background job. The submitted +rules are queued while the inventory is `"pending"`, then appended to the +cumulative `modifications` / `treatments` list when processing completes. Call +`wait()` before using the inventory again or reading those ledgers. + +```python +inventory.apply_treatments([ff.basal_area_treatment("from_below", 25.0)]) +inventory.wait() + +inventory.apply_modifications([ff.remove_trees(ff.tree_attribute("dbh", "<", 10))]) +inventory.wait() +``` + +Both must be called on a `completed` inventory. Re-deriving overwrites the +inventory's data, so to keep the original, [duplicate](#branch-a-scenario) it +first and reshape the copy. + +## Branch a scenario + +To compare scenarios — a thinned stand against the original — keep the +original untouched and work on copies. `duplicate` makes an independent copy +under a new ID, byte-copying the finished data rather than re-deriving it, so +the copy starts identical (same `checksum`): + +```python +copy = inventory.duplicate(name="Thinning scenario") +copy.wait() +copy.apply_treatments([ff.basal_area_treatment("from_below", 25.0)]) +copy.wait() +``` + +The `checksum` is a version marker for an inventory's content: it changes each +time the data is rebuilt and is unaffected by metadata-only edits, so an +identical checksum means identical trees. + +## Next steps + +- Create and read inventories — see [Inventories](inventories.md) +- Voxelize a reshaped inventory into a 3D fuel grid — see + [Inventories](inventories.md#voxelize-into-a-3d-fuel-grid) +- Full signatures — see the [Reference](../reference.md) diff --git a/docs/v2/guides/point-clouds.md b/docs/v2/guides/point-clouds.md new file mode 100644 index 0000000..2b45853 --- /dev/null +++ b/docs/v2/guides/point-clouds.md @@ -0,0 +1,146 @@ +# How to Work with Point Clouds in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +A point cloud is a 3D LiDAR dataset within a domain, fetched from USGS 3DEP or +uploaded from your own airborne (ALS) or terrestrial (TLS) scan. For what +point clouds *are* and how they fit the platform, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com); this guide +covers creating and managing them from Python. + +The v2 surface is functional: you **create** a point cloud from a domain and +data source, and everything you do with one you already hold is a **method** +on it. + +```python +import fastfuels_sdk.v2 as ff + +pc = ff.point_clouds.create_point_cloud_from_file( + domain, "scan.laz", point_cloud_type="als" +) +pc.wait() +``` + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- An existing [domain](domains.md) — the first argument is a `Domain` (or a + bare domain id string) +- A local LiDAR file (`.las`/`.laz`) when uploading your own scan + +## Create a point cloud from USGS 3DEP + +Check coverage and the per-fetch point budget before starting the background +job: + +```python +coverage = ff.point_clouds.check_3dep_coverage(domain) + +if not coverage.available: + raise RuntimeError("No 3DEP LiDAR covers this domain") +if coverage.exceeds_point_budget: + raise RuntimeError("Shrink the domain before fetching 3DEP LiDAR") +``` + +Create the point cloud with automatic acquisition selection: + +```python +pc = ff.point_clouds.create_point_cloud_from_3dep( + domain, + name="USGS 3DEP", +) +pc.wait() +``` + +To pin specific acquisitions, pass names returned by the coverage check in +priority order: + +```python +pc = ff.point_clouds.create_point_cloud_from_3dep( + domain, + datasets=[coverage.datasets[0].name], +) +``` + +The returned point cloud is always airborne (`type_ == "als"`). + +## Upload a point cloud + +`create_point_cloud_from_file` creates the resource, uploads the file to a +signed URL, and returns the (pending) point cloud. Set `point_cloud_type` to +`"als"` for an airborne scan or `"tls"` for a terrestrial one: + +```python +pc = ff.point_clouds.create_point_cloud_from_file( + domain, + "scan.laz", + point_cloud_type="als", + name="North stand ALS", +) +``` + +Processing runs as a background job — the returned point cloud starts in +`"pending"` status and `wait()` blocks until it is ready: + +```python +pc.wait(verbose=True) +``` + +Once completed, the point cloud's `georeference` (CRS and bounds) and +`summary` (point statistics) are populated. + +## Work with a point cloud you hold + +A point cloud is a job resource like any other: `wait(timeout=, verbose=)` +polls it to a terminal status (raising `JobFailedError` on failure), +`refresh()` reloads it in place, `update(name=, description=, tags=)` edits its +metadata, and `delete()` removes it along with its data. + +```python +pc.update(name="North stand (2024)", tags=["als", "2024"]) +pc.delete() +``` + +Point clouds are addressed by their domain and id: + +```python +pc = ff.get_point_cloud(domain, "4a56bae0cd5e481aa1617cb894a9a7f3") +``` + +## List point clouds + +To list your point clouds, optionally narrowed to a domain, a scan type, a +source, or a tag: + +```python +clouds = ff.list_point_clouds(domain) +als_only = ff.list_point_clouds(domain, point_cloud_type="als") +tagged = ff.list_point_clouds(tag="2024") +``` + +Omit `domain` to list point clouds across all of your domains. + +## Error handling + +Wrapper functions and methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import NotFoundException + +try: + pc = ff.get_point_cloud(domain, "does-not-exist") +except NotFoundException: + print("No such point cloud (or you don't have access to it)") +``` + +## Next steps + +- Turn a completed airborne point cloud into a canopy-height grid with + [`create_canopy_height_grid_from_point_cloud`](creating-grids.md#canopy-grids) +- Full signatures — see the [Reference](../reference.md) diff --git a/docs/v2/guides/quotas.md b/docs/v2/guides/quotas.md new file mode 100644 index 0000000..9d57642 --- /dev/null +++ b/docs/v2/guides/quotas.md @@ -0,0 +1,83 @@ +# How to Check Usage and Handle Quota Rejections + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable + +Use this guide to inspect the current owner's limits and usage and to handle +quota rejections from SDK resource creators. + +## Check your limits + +Call `get_quotas` to retrieve the limits resolved for the owner authenticated +by the current API key: + +```python +import fastfuels_sdk.v2 as ff + +quotas = ff.get_quotas() + +print(quotas.max_active_grids) +print(quotas.max_weekly_grid_dispatches) +print(quotas.max_grid_storage_bytes) +``` + +## Check your current usage + +Call `get_usage` to compare current usage with the corresponding limits: + +```python +usage = ff.get_usage() + +print(usage.grids.active.usage, usage.grids.active.limit) +print(usage.grids.total.usage, usage.grids.total.limit) +print(usage.grids.storage.usage_bytes, usage.grids.storage.limit_bytes) +``` + +Count-only resources are available through `usage.domains`, +`usage.applications`, and `usage.api_keys`. The active, total, and storage +fields are also available for exports, inventories, features, and point +clouds. + +Inspect `usage.lifecycle` for the resource-retention policy currently applied +to the owner: + +```python +print(usage.lifecycle.resource_ttl_days) +print(usage.lifecycle.failed_resource_ttl_days) +print(usage.lifecycle.next_expiry_on) +``` + +## Handle a quota rejection + +Catch `QuotaExceededException` around any operation that creates or re-derives +a resource: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import QuotaExceededException + +try: + grid = ff.grids.create_topography_grid_from_3dep( + domain, + output_resolution_m=30, + ) +except QuotaExceededException as exc: + print(exc.quota, exc.current, exc.limit) + print(exc.message) + if exc.retry_after is not None: + print(f"Retry in {exc.retry_after} seconds") +``` + +For an active-job limit, `retry_after` contains the number of seconds the API +recommends waiting before another attempt. + +For a weekly dispatch limit, `window_reset_on` contains the reset time. Count +and storage limits provide neither retry field; delete unneeded resources or +request a higher limit before retrying. diff --git a/docs/v2/guides/working-with-grids.md b/docs/v2/guides/working-with-grids.md new file mode 100644 index 0000000..34a858f --- /dev/null +++ b/docs/v2/guides/working-with-grids.md @@ -0,0 +1,291 @@ +# How to Work with Grids in FastFuels SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +This guide covers what you do with a grid you already hold: wait on its job, +inspect its bands and georeference, resample it, export it, and list or +delete grids. To create grids in the first place, see +[Creating grids](creating-grids.md). For what grids *are*, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). + +You get a grid handle either from a creator (see +[Creating grids](creating-grids.md)) or by fetching one by id: + +```python +import fastfuels_sdk.v2 as ff + +grid = ff.get_grid(domain, "0eeed67e33df450f943a528fb1447dab") +``` + +Operations on a grid you hold are **methods** on it (`grid.wait()`, +`grid.resample(...)`, `grid.delete()`); listing and fetching are top-level +functions (`ff.list_grids`, `ff.get_grid`). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- A grid — created per [Creating grids](creating-grids.md), or fetched by id + +## Wait for a grid to finish + +Because creators return a pending grid, block on the job before using its +data: + +```python +grid.wait() +``` + +```python +>>> grid.status + +``` + +`wait` polls until the job reaches a terminal status and returns the grid, +so calls chain. It waits indefinitely by default; pass `timeout` (seconds) +to bound it, which raises `TimeoutError`. A failed job raises +`JobFailedError`, carrying the API's `code`, `message`, and `suggestion`. + +Jobs run server-side in parallel, so create everything first and join with +`ff.wait_all`. With `verbose=True` it prints a line per poll for each grid +still running when it is reached: + +```python +ff.wait_all([topography, surface_fuel, canopy_fuel], verbose=True) +``` + +```text +Grid 0eeed67e33df450f943a528fb1447dab: running (5s) +Grid 0eeed67e33df450f943a528fb1447dab: completed (10s) +``` + +(Small domains finish fast — grids already complete when `wait_all` reaches +them print nothing.) + +## Inspect a grid's bands and georeference + +A grid reports its bands as soon as it is created — their keys, names, and +units — so you know what data it holds: + +```python +>>> [b.key for b in topography.bands] +['elevation', 'slope', 'aspect'] +>>> topography.bands[1].name, topography.bands[1].unit +('Slope', 'deg') +``` + +Its georeference is populated once the job completes (it is `None` while +pending), reporting the CRS, affine transform, and pixel shape of the +output: + +```python +>>> topography.georeference.crs +'EPSG:32611' +>>> topography.georeference.transform # affine [a, b, c, d, e, f] +[30.0, 0.0, 720192.0, 0.0, -30.0, 5190856.0] +>>> topography.georeference.shape # [rows, cols] +[47, 58] +``` + +Two grids built on the same domain at the same resolution share this +georeference, which is what lets them overlay cell-for-cell. + +## Summarize a band without downloading it + +Once a grid completes, the server attaches summary statistics to each band, so +you can get a quick overview without fetching the cells (which is what +[`to_numpy`](#read-grid-data-into-python) does). Pass a band key to +`band_summary`: + +```python +summary = topography.band_summary("elevation") +``` + +A continuous band reports `count`, `nodata_count`, `min_`, `max_`, `mean`, and +`std`; a categorical band reports `count`, `nodata_count`, and `unique_count`. +The `type_` field (`"continuous"` or `"categorical"`) tells you which. +`band_summary` returns `None` until the grid completes — call +[`wait`](#wait-for-a-grid-to-finish) first. + +## Retrieve a grid by id + +To fetch an existing grid using its domain and grid IDs: + +```python +grid = ff.get_grid(domain, "0eeed67e33df450f943a528fb1447dab") +``` + +## Refresh and update + +To reload a grid's latest state in place (for example, to check job progress +yourself): + +```python +grid.refresh() +``` + +To change a grid's name, description, or tags: + +```python +>>> grid.update(name="Topography (30 m)", tags=["terrain"]) +>>> grid.name, grid.tags +('Topography (30 m)', ['terrain']) +``` + +`refresh` and `update` both change the grid in place and return it. `update` +sends only the fields you pass; passing none makes no API call. + +## Resample a grid + +To resample a completed grid onto a new lattice, producing a new grid: + +```python +coarser = grid.resample(output_resolution_m=90, resampling="average") +``` + +`resample` takes the same alignment arguments as the creators (see +[Align grids to each other](creating-grids.md#align-grids-to-each-other)). +The source grid must be `completed`. + +## Duplicate a grid + +To branch from a completed grid, `duplicate` makes an independent copy under a +new ID. The finished data is byte-copied rather than re-derived, so the copy +carries the source's `checksum` verbatim — only its `id` and timestamps differ: + +```python +copy = grid.duplicate(name="Scenario A") +copy.wait() +``` + +## Apply modifications to a grid + +To modify a grid you already hold — rather than at creation — call +`apply_modifications` with the same `ff.mask(...)` rules a creator's +`modifications=` argument accepts. The rules are appended to the grid's +cumulative modifications and the data is re-derived **in place**: the grid +keeps its ID and returns to `"pending"` while it rebuilds. + +```python +grid.apply_modifications([ff.mask(roads, "fbfm", 91, buffer_m=5)]) +grid.wait() +``` + +The grid must be `completed` first. Re-deriving overwrites the grid's data, so +to keep the original, [`duplicate`](#duplicate-a-grid) it first and modify the +copy. See [Mask out features](creating-grids.md#mask-out-features) for the +masking model. + +## Read grid data into Python + +To pull a single band's values into a NumPy array, pass the band key to +`to_numpy`: + +```python +elevation = topography.to_numpy("elevation") +``` + +```python +>>> elevation.shape # matches grid.georeference.shape +(47, 58) +>>> elevation.dtype +dtype('float32') +``` + +The array is shaped like the grid — `(rows, cols)` for a 2D raster, or +`(z, rows, cols)` for a 3D voxel grid. Cells with no data hold the band's +`nodata` value, or `NaN` when the band defines none. + +To read every band at once into an `xarray.Dataset` — one variable per band, +with `x`/`y` (and `z`) coordinates derived from the grid's affine transform +and the CRS on `.attrs` — call `to_xarray`: + +```python +>>> topography.to_xarray() + Size: 34kB +Dimensions: (y: 47, x: 58) +Coordinates: + * y (y) float64 376B 5.191e+06 5.191e+06 ... 5.189e+06 5.189e+06 + * x (x) float64 464B 7.202e+05 7.202e+05 ... 7.219e+05 7.219e+05 +Data variables: + elevation (y, x) float32 11kB 1.015e+03 1.013e+03 ... 1.02e+03 1.019e+03 + slope (y, x) float32 11kB 7.587 8.345 8.092 7.41 ... 3.42 3.008 3.094 + aspect (y, x) float32 11kB 25.29 28.79 40.35 51.47 ... 126.0 135.2 138.1 +Attributes: + crs: EPSG:32611 +``` + +Both methods download the grid's data and require a `completed` grid. To +write the data to a file on disk instead of loading it into memory, see +[Export a grid](#export-a-grid). + +## Export a grid + +To export a completed grid to a downloadable file and save it, chain the +export job's `wait` into `to_file`: + +```python +export = grid.export(format="geotiff") # or "netcdf", "zarr" +export.wait().to_file("elevation.tif") +``` + +The export runs as its own background job; the signed download URL fills +in once it completes and stays valid for seven days. To load a grid's +values into memory instead of a file, see +[Read grid data into Python](#read-grid-data-into-python). For exporting +band subsets, the multi-grid QUIC-Fire bundle, and managing exports, see the +[Exports guide](exports.md). + +## List grids + +To list grids in a domain, or across all your domains by omitting it: + +```python +grids = ff.list_grids(domain) +all_grids = ff.list_grids() +``` + +Narrow the results by source, source product (requires `source`), or tag — +the source and product names are the ones a grid reports in `grid.source`: + +```python +topo_grids = ff.list_grids(domain, source="3dep") +fbfm_grids = ff.list_grids(domain, source="landfire", product="fbfm40") +tagged = ff.list_grids(domain, tag="terrain") +``` + +## Delete a grid + +To permanently delete a grid and its generated data: + +```python +grid.delete() +``` + +Deleting a domain also deletes all of its grids. + +## Error handling + +Functions and methods raise typed exceptions from +`fastfuels_sdk.v2.exceptions`: + +```python +import fastfuels_sdk.v2 as ff +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +try: + grid = ff.get_grid(domain, "does-not-exist") +except NotFoundException: + print("No such grid (or you don't have access to it)") + +try: + grid.resample(output_resolution_m=30) # source not completed yet +except UnprocessableEntityException as exc: + print(exc.detail) +``` diff --git a/docs/v2/index.md b/docs/v2/index.md new file mode 100644 index 0000000..f87da7b --- /dev/null +++ b/docs/v2/index.md @@ -0,0 +1,51 @@ +# Welcome to the FastFuels Python SDK Documentation! + +!!! warning "Beta" + You are reading the documentation for the **v2 SDK**, which targets + the FastFuels v2 API and is under active development. The v1 SDK + remains the default — switch versions with the selector in the + header. Import v2 explicitly from `fastfuels_sdk.v2`. + +## What is FastFuels? + +FastFuels is a cloud-based platform for generating forest inventory data. It +uses a combination of satellite imagery and machine learning to generate +tabular tree data and voxelized 3D fuel models. These data products can be used +to support wildfire risk assessment, fire behavior modeling, and other +applications. + +The [FastFuels documentation](https://docs.fastfuels.silvxlabs.com) covers +the platform itself: the web application, the HTTP API, and explanations of +the core concepts (domains, grids, inventories, features). This site covers +how to work with FastFuels from Python. + +## What is the FastFuels Python SDK? + +The FastFuels Python SDK is a Python package that provides a convenient +interface for interacting with the FastFuels API. It can be used to create and +manage FastFuels resources. It can also be used to download and process +generated data products. + +## Installation + +The FastFuels Python SDK can be installed using `pip`: + +```bash +pip install fastfuels-sdk +``` + +## Using the v2 SDK + +The v1 and v2 APIs are separate live services, and the SDK ships both as +versioned subpackages, so you can use them side by side during migration: + +```python +from fastfuels_sdk.v1 import Domain # v1 (current default) +from fastfuels_sdk.v2 import Domain # v2 (Beta) +``` + +Coming from v1? Start with [Migrating from v1](guides/migration.md). + +The SDK authenticates with an existing API key. Creating applications and +creating, rotating, or revoking keys are account-management operations handled +outside the high-level Python SDK through the FastFuels console. diff --git a/docs/v2/reference.md b/docs/v2/reference.md new file mode 100644 index 0000000..0500363 --- /dev/null +++ b/docs/v2/reference.md @@ -0,0 +1,25 @@ +::: fastfuels_sdk.v2.domains + +::: fastfuels_sdk.v2.features + +::: fastfuels_sdk.v2.grids + +::: fastfuels_sdk.v2.inventories + +::: fastfuels_sdk.v2.point_clouds + +::: fastfuels_sdk.v2.exports + +::: fastfuels_sdk.v2.modifications + +::: fastfuels_sdk.v2.treatments + +::: fastfuels_sdk.v2.calibrations + +::: fastfuels_sdk.v2.compose + +::: fastfuels_sdk.v2._jobs + +::: fastfuels_sdk.v2.api + +::: fastfuels_sdk.v2.exceptions diff --git a/docs/v2/stylesheets/extra.css b/docs/v2/stylesheets/extra.css new file mode 100644 index 0000000..c76b44b --- /dev/null +++ b/docs/v2/stylesheets/extra.css @@ -0,0 +1,55 @@ +/* Version selector (mike + mkdocs-material). The stock theme renders the + selector as bare header text; style it as an explicit control. + (Duplicated in docs/v1 and docs/v2 — each version's site builds from its + own docs_dir.) */ +.md-version { + margin-left: 0.8rem; +} + +.md-version__current { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background-color: rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 2rem; + padding: 0.25rem 0.8rem; + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + transition: background-color 125ms, border-color 125ms; +} + +.md-version__current:hover, +.md-version__current:focus { + background-color: rgba(0, 0, 0, 0.32); + border-color: rgba(255, 255, 255, 0.6); +} + +.md-version__list { + margin-top: 0.5rem; + border-radius: 0.3rem; + box-shadow: var(--md-shadow-z3); + overflow: hidden; + min-width: 8rem; +} + +.md-version__item { + line-height: 1; +} + +.md-version__link { + display: block; + width: 100%; + padding: 0.6rem 1rem; + font-size: 0.7rem; + color: var(--md-default-fg-color); + transition: background-color 125ms, color 125ms; +} + +.md-version__link:hover, +.md-version__link:focus { + background-color: var(--md-default-fg-color--lightest); + color: var(--md-typeset-a-color); +} diff --git a/docs/v2/tutorials/export_to_quicfire.md b/docs/v2/tutorials/export_to_quicfire.md new file mode 100644 index 0000000..c848c66 --- /dev/null +++ b/docs/v2/tutorials/export_to_quicfire.md @@ -0,0 +1,341 @@ +# Tutorial: Export QUIC-Fire Inputs with the v2 SDK + +!!! warning "Beta" + The v2 SDK targets the FastFuels v2 API and is under active + development. The v1 SDK remains the default — import v2 explicitly + from `fastfuels_sdk.v2`. + +In this tutorial we'll build a complete set of QUIC-Fire fuel inputs for a +small region in the Blue Mountain Recreation Area and package them into a +QUIC-Fire-loadable archive. Along the way we'll touch +every kind of v2 resource — a domain, OpenStreetMap features, raster grids, a +tree inventory, and a 3D voxel grid — and assemble them with a single +QUIC-Fire export. + +We'll follow the v2 SDK's functional style throughout: we **create** a +resource by calling a `create_…` function on a domain, and everything we do +with a resource we already hold (waiting, looking up values, voxelizing, +exporting) is a **method** on it. Because every creation runs as a background +job, we'll create work in batches and join each batch with `ff.wait_all`, +letting the server do the jobs in parallel. + +## What we'll build + +A QUIC-Fire export reads five required fuel fields — plus optional +topography, which we'll include. We'll build each from a real data source: + +| Field | Source we'll use | v2 resource | +| --- | --- | --- | +| Canopy bulk density | TreeMap → tree inventory → voxels | 3D voxel grid | +| Canopy fuel moisture | voxelization moisture model | 3D voxel grid | +| Surface fuel load | LANDFIRE FBFM40 lookup | 2D grid | +| Surface fuel depth | LANDFIRE FBFM40 lookup | 2D grid | +| Surface fuel moisture | a uniform value | 2D grid | +| Topography (elevation) | USGS 3DEP | 2D grid | + +The exporter slices these into the QUIC-Fire `.dat` arrays we'll inspect in +[Step 9](#step-9-inspect-the-export). + +## Prerequisites + +- The FastFuels SDK installed: `pip install fastfuels-sdk` +- A FastFuels API key in the `FASTFUELS_API_KEY` environment variable +- Basic familiarity with Python and GeoPandas + +For background on what each of these resources *is*, see the +[FastFuels documentation](https://docs.fastfuels.silvxlabs.com). Coming from +the v1 SDK? The [migration guide](../guides/migration.md) maps the v1 builder +classes to the v2 functions used here. + +## Step 1: Authenticate + +The v2 SDK reads your API key from the `FASTFUELS_API_KEY` environment +variable: + +```bash +export FASTFUELS_API_KEY="your-api-key" +``` + +Then import the package under the conventional `ff` alias — everything in this +tutorial hangs off it: + +```python +import fastfuels_sdk.v2 as ff +``` + +If you'd rather set the key in code, call `ff.set_api_key("your-api-key")` +before anything else. + +## Step 2: Define a region of interest + +We'll describe our area as a GeoDataFrame holding a single polygon in WGS 84 +(EPSG:4326): + +```python +import geopandas as gpd +from shapely.geometry import Polygon + +coordinates = [ + [-114.09957018646286, 46.82933208815811], + [-114.10141707482919, 46.828370407248826], + [-114.10010954324228, 46.82690548814563], + [-114.09560673134018, 46.8271123684554], + [-114.09592544216444, 46.829058122675065], + [-114.09957018646286, 46.82933208815811], +] + +roi = gpd.GeoDataFrame(geometry=[Polygon(coordinates)], crs="EPSG:4326") +``` + +## Step 3: Create a domain + +A domain is the spatial container every other resource lives in. We'll create +one from the GeoDataFrame, padding its bounding box out to a whole number of +2 m cells so the grids we build later tile it exactly: + +```python +domain = ff.Domain.from_geodataframe( + geodataframe=roi, + name="Blue Mountain QUIC-Fire", + description="Tutorial region in the Blue Mountain Recreation Area", + pad_to_resolution=2.0, +) +``` + +```python +>>> domain.id +'0d77a5b1525b497c829fceeadba5f958' +``` + +The domain reprojects our lat/lon polygon into a local metric CRS. We can see +the 2 m pixel lattice every grid will share: + +```python +>>> lattice = domain.get_lattice(resolution=2.0) +>>> lattice.crs +'EPSG:32611' +>>> lattice.shape # [rows, cols] +[136, 225] +``` + +## Step 4: Add roads and water from OpenStreetMap + +Roads and open water aren't fuel — we'll pull them from OpenStreetMap now so +we can carve them out of the fuels later. Both extractions are background +jobs, so we create them together and join with `ff.wait_all`: + +```python +roads = ff.features.create_road_feature_from_osm(domain, name="Roads") +water = ff.features.create_water_feature_from_osm(domain, name="Water") + +ff.wait_all([roads, water], verbose=True) +``` + +```text +Feature 07e6407aedd44d7e8a09fbc5e48c3f3e: pending (5s) +Feature 07e6407aedd44d7e8a09fbc5e48c3f3e: pending (15s) +Feature 07e6407aedd44d7e8a09fbc5e48c3f3e: completed (20s) +``` + +(Only one feature prints here — the other finished before `wait_all` +reached it. Small regions extract fast.) + +!!! note "No separate feature grid in v2" + In the v1 SDK you created a standalone *feature grid* to mask trees and + fuels. In v2 there's no such resource: you mask a feature directly into + each grid by passing `ff.mask(feature, …)` in that creator's + `modifications=` list, as we do for the surface fuels in + [Step 6](#step-6-build-the-surface-fuel-grids). See + [Mask out features](../guides/creating-grids.md#mask-out-features) for the + full masking model. + +## Step 5: Build the topography grid + +Elevation comes from the USGS 3D Elevation Program (3DEP). We resample its +10 m source onto our 2 m domain lattice: + +```python +topography = ff.grids.create_topography_grid_from_3dep( + domain, + source_resolution_m=10, + output_resolution_m=2, + bands=["elevation"], +) +``` + +We won't wait on it yet — it's a pending job we'll join with the other raster +grids in the next step. + +## Step 6: Build the surface fuel grids + +QUIC-Fire's surface needs fuel load, fuel depth, and fuel moisture. Load and +depth come from LANDFIRE's 40 Scott & Burgan fire behavior fuel models +(FBFM40). First we build the FBFM40 grid, masking the road network to a +non-burnable code (FBFM 91) as we go — this is the v2 replacement for v1's +`feature_masks`: + +```python +fbfm = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + domain, + output_resolution_m=2, + modifications=[ff.mask(roads, "fbfm", 91, buffer_m=5)], +) +``` + +The FBFM40 grid holds categorical fuel-model *codes*. We also start the +PIM grid we'll need for trees in [Step 7](#step-7-build-the-canopy-fuel-grid), +then join all three pending raster jobs: + +```python +pim = ff.grids.create_pim_grid_from_treemap( + domain, output_resolution_m=2, resampling="nearest" +) + +ff.wait_all([topography, fbfm, pim], verbose=True) +``` + +With the FBFM40 codes in hand, we look up the actual fuel quantities QUIC-Fire +consumes — 1-hour fuel load and fuel-bed depth — which produces a new grid: + +```python +surface = ff.grids.create_fuel_grid_from_fbfm40_lookup( + fbfm, bands=["fuel_load.1hr", "fuel_depth"] +) +``` + +```python +>>> [(b.key, b.unit) for b in surface.bands] +[('fuel_load.1hr', 'kg/m**2'), ('fuel_depth', 'm')] +``` + +LANDFIRE doesn't carry fuel moisture (it's a weather-driven scenario input), +so we set a uniform 15% surface moisture on a matching 2 m grid: + +```python +moisture = ff.grids.create_uniform_grid( + domain, resolution_m=2.0, bands={"fuel_moisture.1hr": 15.0} +) +``` + +## Step 7: Build the canopy fuel grid + +Canopy fuel takes three moves: match each cell to a TreeMap forest plot (the +PIM grid we already started), expand those plots into individual trees, then +voxelize the trees into a 3D bulk-density grid. We expand the trees from the +completed PIM grid, using a fixed `seed` so the result is reproducible: + +```python +inventory = ff.inventories.create_tree_inventory_from_pim_grid( + domain, pim, seed=42 +) + +ff.wait_all([surface, moisture, inventory], verbose=True) +``` + +```python +>>> len(inventory.to_dataframe()) +2068 +``` + +Now we voxelize the inventory onto the same 2 m horizontal lattice, with 1 m +vertical layers, asking for both the live foliage bulk density and the live +fuel moisture QUIC-Fire needs: + +```python +voxels = inventory.voxelize( + horizontal_resolution_m=2.0, + vertical_resolution_m=1.0, + bands=["bulk_density.foliage.live", "fuel_moisture.live"], +) +voxels.wait(verbose=True) +``` + +```python +>>> [b.key for b in voxels.bands] +['bulk_density.foliage.live', 'fuel_moisture.live'] +>>> voxels.georeference.shape # [layers, rows, cols] +[27, 136, 225] +``` + +## Step 8: Export to QUIC-Fire + +Everything is now on the same 2 m lattice. `create_quicfire_export` bundles +the fields into a QUIC-Fire archive — each role is a `(grid, band)` pair +naming the grid and the band to read from it: + +```python +export = ff.exports.create_quicfire_export( + domain, + canopy_bulk_density=(voxels, "bulk_density.foliage.live"), + canopy_moisture=(voxels, "fuel_moisture.live"), + surface_fuel_load=(surface, "fuel_load.1hr"), + surface_fuel_depth=(surface, "fuel_depth"), + surface_moisture=(moisture, "fuel_moisture.1hr"), + topography=(topography, "elevation"), + name="Blue Mountain QUIC-Fire", +) + +export.wait(verbose=True) +``` + +```text +Export 67af943ef13e49a1b71c97c90c3fa4d9: pending (5s) +Export 67af943ef13e49a1b71c97c90c3fa4d9: running (20s) +Export 67af943ef13e49a1b71c97c90c3fa4d9: completed (25s) +``` + +The export is its own background job; once it completes, a signed download URL +is filled in. We stream the archive to a local directory: + +```python +path = export.to_file("quicfire_export/") +``` + +```python +>>> path +PosixPath('quicfire_export/Blue_Mountain_QUIC-Fire.zip') +``` + +## Step 9: Inspect the export + +The archive is a zip that QUIC-Fire loads directly. Let's confirm the inputs +are all there: + +```python +>>> import zipfile +>>> sorted(zipfile.ZipFile(path).namelist()) +['domain.geojson', 'metadata.json', 'topo.dat', 'treesfueldepth.dat', 'treesmoist.dat', 'treesrhof.dat'] +``` + +The `.dat` files are the QUIC-Fire fuel arrays: + +- `treesrhof.dat` — canopy bulk density +- `treesmoist.dat` — canopy fuel moisture +- `treesfueldepth.dat` — surface fuel-bed depth +- `topo.dat` — elevation + +alongside `metadata.json` (the grid geometry and band provenance) and +`domain.geojson` (the domain footprint). + +## Recap + +In one pass we created a domain, pulled road and water features, built +topography from 3DEP and surface fuels from LANDFIRE FBFM40, expanded a +TreeMap tree inventory into a 3D canopy fuel grid, and bundled it all into a +QUIC-Fire archive — masking roads out of the fuels along the way. + +## Next steps + +- Swap in your own region by changing the polygon in + [Step 2](#step-2-define-a-region-of-interest). +- Mask water (and other features) into the fuels the same way we masked + roads — see [Mask out features](../guides/creating-grids.md#mask-out-features). +- Tune the tree inventory before voxelizing — thinning, treatments, and + modifications — see + [Modify and treat tree inventories](../guides/modify-treat-inventories.md). +- Define the fire grid from an existing grid's lattice, or change its + resolution, with the export's alignment options — see + [Bundle grids for QUIC-Fire](../guides/exports.md#bundle-grids-for-quic-fire). +- Full signatures for every function used here are in the + [Reference](../reference.md). +``` diff --git a/fastfuels_sdk/v1/grids/grids.py b/fastfuels_sdk/v1/grids/grids.py index 3487389..494f4b8 100644 --- a/fastfuels_sdk/v1/grids/grids.py +++ b/fastfuels_sdk/v1/grids/grids.py @@ -472,7 +472,7 @@ def create_tree_grid( "value": float # moisture content in % } - SAVR: dict, optional + savr : dict, optional Configuration for surface area to volume ratio. Sources available: - Inventory: { diff --git a/fastfuels_sdk/v2/__init__.py b/fastfuels_sdk/v2/__init__.py new file mode 100644 index 0000000..3105410 --- /dev/null +++ b/fastfuels_sdk/v2/__init__.py @@ -0,0 +1,98 @@ +""" +FastFuels SDK v2. + +The settled access pattern is a single import reaching everything through the +package namespace:: + + import fastfuels_sdk.v2 as ff + + domain = ff.Domain.from_file("aoi.geojson") + grid = ff.grids.create_topography_grid_from_3dep(domain, output_resolution_m=10) + grid.wait() + +- Resource creators are module-qualified functions: ``ff.grids.create_*``, + ``ff.features.create_*``. +- Operations on a resource you hold are methods: ``grid.wait()``, + ``grid.resample(...)``, ``grid.delete()``. +- Cross-cutting helpers are top-level: ``ff.Domain``, ``ff.get_grid``, + ``ff.list_grids``, ``ff.wait_all``, ``ff.set_api_key``. +""" + +from fastfuels_sdk.v2 import ( + compose, + exports, + features, + grids, + inventories, + point_clouds, +) +from fastfuels_sdk.v2._jobs import JobFailedError, wait_all +from fastfuels_sdk.v2.api import get_quotas, get_usage, set_api_key +from fastfuels_sdk.v2.calibrations import duet_calibration +from fastfuels_sdk.v2.domains import Domain, list_domains, reproject_geojson +from fastfuels_sdk.v2.exports import Export, get_export, list_exports +from fastfuels_sdk.v2.features import Feature, get_feature, list_features +from fastfuels_sdk.v2.grids import Grid, get_grid, list_grids +from fastfuels_sdk.v2.inventories import Inventory, get_inventory, list_inventories +from fastfuels_sdk.v2.modifications import ( + mask, + modify_trees, + remove_trees, + tree_attribute, + tree_within, +) +from fastfuels_sdk.v2.point_clouds import ( + PointCloud, + get_point_cloud, + list_point_clouds, +) +from fastfuels_sdk.v2.treatments import basal_area_treatment, diameter_treatment + +__all__ = [ + # Submodules (resource creators live here: ff.grids.create_*, ff.features.create_*) + "compose", + "exports", + "features", + "grids", + "inventories", + "point_clouds", + # Configuration + "set_api_key", + # Quotas and usage + "get_quotas", + "get_usage", + # Calibrations + "duet_calibration", + # Records + "Domain", + "Export", + "Feature", + "Grid", + "Inventory", + "PointCloud", + # Fetch / list helpers + "list_domains", + "list_features", + "get_feature", + "list_grids", + "get_grid", + "list_inventories", + "get_inventory", + "list_exports", + "get_export", + "list_point_clouds", + "get_point_cloud", + "reproject_geojson", + # Modifications + "mask", + "tree_attribute", + "tree_within", + "remove_trees", + "modify_trees", + # Treatments + "basal_area_treatment", + "diameter_treatment", + # Jobs + "wait_all", + "JobFailedError", +] diff --git a/fastfuels_sdk/v2/_jobs.py b/fastfuels_sdk/v2/_jobs.py new file mode 100644 index 0000000..5384ab8 --- /dev/null +++ b/fastfuels_sdk/v2/_jobs.py @@ -0,0 +1,130 @@ +""" +fastfuels_sdk/v2/_jobs.py + +Shared polling helpers for job-based v2 resources (Feature, Grid, +Inventory). Private module: each resource exposes its own documented +``wait`` method that delegates to :func:`wait` here. :func:`wait_all` and +:class:`JobFailedError` are re-exported at the package top level. +""" + +import time + +from fastfuels_sdk.v2.client_library.models import JobStatus +from fastfuels_sdk.v2.client_library.types import Unset + +_TERMINAL_STATUSES = (JobStatus.COMPLETED, JobStatus.FAILED) + +# Server-side poll interval. Not user-tunable: the job runs server-side +# regardless of how often we poll, so the interval is an implementation +# detail rather than part of the wait contract. +_POLL_SECONDS = 5 + + +class JobFailedError(Exception): + """Raised when a job-based resource reaches the ``failed`` status. + + Attributes + ---------- + code : str or None + Machine-readable error code reported by the API (``None`` when the + API returned no error detail). + message : str or None + Human-readable error message reported by the API. + suggestion : str or None + Actionable advice reported by the API, when present. + """ + + def __init__(self, display, *, code=None, message=None, suggestion=None): + self.code = code + self.message = message + self.suggestion = suggestion + super().__init__(display) + + +def _job_failed_error(resource, error) -> JobFailedError: + """Build a :class:`JobFailedError` from a generated ``JobError``.""" + label = f"{type(resource).__name__} {resource.id}" + if error is None or isinstance(error, Unset): + return JobFailedError(f"{label} failed (no error detail returned).") + suggestion = getattr(error, "suggestion", None) + if isinstance(suggestion, Unset): + suggestion = None + display = f"{label} failed [{error.code}]: {error.message}" + if suggestion: + display += f" Suggestion: {suggestion}" + return JobFailedError( + display, code=error.code, message=error.message, suggestion=suggestion + ) + + +def wait(resource, timeout=None, verbose=False): + """Poll a job resource until it reaches a terminal status. + + Duck-typed on the resource wrapper: requires ``refresh()``, ``status``, + ``error``, and ``id``. Updates the resource in place on every poll and + returns it, so calls chain. + + Parameters + ---------- + resource + A job-based resource wrapper (Feature, Grid, ...). + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + Print status updates while polling. + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapsed before a terminal status. + JobFailedError + If the job reaches the ``failed`` status. + """ + elapsed = 0.0 + resource.refresh() + while resource.status not in _TERMINAL_STATUSES: + if timeout is not None and elapsed >= timeout: + raise TimeoutError( + f"{type(resource).__name__} {resource.id} did not complete " + f"within {timeout} seconds" + ) + time.sleep(_POLL_SECONDS) + elapsed += _POLL_SECONDS + resource.refresh() + if verbose: + print( + f"{type(resource).__name__} {resource.id}: " + f"{resource.status} ({elapsed:.0f}s)" + ) + if resource.status == JobStatus.FAILED: + raise _job_failed_error(resource, resource.error) + return resource + + +def wait_all(resources, timeout=None, verbose=False): + """Wait for several job resources to reach a terminal status. + + Jobs run server-side in parallel, so creating resources without waiting + and then joining them here runs the work concurrently. Resources are + joined in iteration order; the first one to be found ``failed`` raises + its :class:`JobFailedError`. + + Parameters + ---------- + resources : iterable + Job-based resource wrappers to wait on. + timeout : float, optional + Per-resource timeout in seconds (``None`` waits indefinitely). + verbose : bool, optional + Print status updates while polling. + + Returns + ------- + list + The resources, each updated in place to its terminal state. + """ + resources = list(resources) + for resource in resources: + wait(resource, timeout=timeout, verbose=verbose) + return resources diff --git a/fastfuels_sdk/v2/_uploads.py b/fastfuels_sdk/v2/_uploads.py new file mode 100644 index 0000000..1dc4e6a --- /dev/null +++ b/fastfuels_sdk/v2/_uploads.py @@ -0,0 +1,38 @@ +""" +fastfuels_sdk/v2/_uploads.py + +Shared helper for uploading a local file to a signed upload URL. Used by every +upload creator (grids, inventories, point clouds) so the signed-header contract +lives in exactly one place. +""" + +# External imports +import requests + + +def put_upload(spec, path: str) -> None: + """Upload a local file to a signed upload URL with HTTP PUT. + + The API signs the upload URL against a specific set of headers -- the + ``Content-Type`` and a GCS ``x-goog-content-length-range`` -- and returns + them in ``spec.headers``. The PUT must echo those headers *exactly* (no + more, no less) or GCS rejects it with 403, so this sends the server-provided + set verbatim rather than reconstructing it. + + Parameters + ---------- + spec : upload spec + An upload spec carrying ``url`` and ``headers`` (a + ``*UploadSpecHeaders`` model), as returned by the create endpoints. + path : str + Path to the local file to upload. + + Raises + ------ + requests.HTTPError + If the signed PUT returns a non-2xx status. + """ + headers = dict(spec.headers.to_dict()) + with open(path, "rb") as file_obj: + response = requests.put(spec.url, data=file_obj, headers=headers) + response.raise_for_status() diff --git a/fastfuels_sdk/v2/api.py b/fastfuels_sdk/v2/api.py new file mode 100644 index 0000000..70c9f90 --- /dev/null +++ b/fastfuels_sdk/v2/api.py @@ -0,0 +1,140 @@ +""" +fastfuels_sdk/v2/api.py + +Client configuration for the FastFuels v2 API: API key management and the +shared HTTP client used by the wrapper modules. +""" + +import os +from typing import Optional + +# DEFAULT_BASE_URL is recorded by generate_client.sh alongside the generated +# client (openapi-python-client embeds no server URL). It is the Cloud Run +# deployment for now — a stable domain should front it before GA (tracked in +# #176); until then FASTFUELS_API_V2_URL overrides it without an SDK upgrade. +from fastfuels_sdk.v2.client_library.base_url import DEFAULT_BASE_URL +from fastfuels_sdk.v2.client_library.api.users import get_me, get_me_usage +from fastfuels_sdk.v2.client_library.client import AuthenticatedClient +from fastfuels_sdk.v2.client_library.models import Quotas, Usage +from fastfuels_sdk.v2.exceptions import expect + +_client: Optional[AuthenticatedClient] = None + + +def set_api_key(api_key: str) -> None: + """Set the API key for the FastFuels v2 SDK. + + This invalidates the cached client, ensuring that subsequent API calls + use the new credentials. + + Parameters + ---------- + api_key : str + The API key to use for authentication. + """ + global _client + _client = None + os.environ["FASTFUELS_API_KEY"] = api_key + + +def get_client() -> Optional[AuthenticatedClient]: + """Get the current API client, creating one if necessary. + + The API key is read from: + + 1. The cached client, if :func:`set_api_key` was called + 2. The ``FASTFUELS_API_KEY`` environment variable + + Returns + ------- + Optional[AuthenticatedClient] + The client instance, or None if no API key is configured. + """ + global _client + + if _client is not None: + return _client + + # v1 and v2 are separate deployments with separate keys, but both read + # FASTFUELS_API_KEY: running both versions in one process with different + # keys is not a supported use case, so a single variable suffices. + api_key = os.getenv("FASTFUELS_API_KEY") + if not api_key: + return None + + _client = AuthenticatedClient( + base_url=os.getenv("FASTFUELS_API_V2_URL", DEFAULT_BASE_URL), + token=api_key, + prefix="", # raw key, not "Bearer " + auth_header_name="api-key", + # Error translation happens in one place — exceptions.expect() on + # sync_detailed() responses — so the generated client must hand back + # undocumented statuses (e.g. 404) instead of raising its own + # UnexpectedStatus. + raise_on_unexpected_status=False, + ) + + return _client + + +def ensure_client() -> AuthenticatedClient: + """Ensure an API client is configured and return it. + + Returns + ------- + AuthenticatedClient + The client instance. + + Raises + ------ + RuntimeError + If no API key is configured. + """ + client = get_client() + if client is None: + raise RuntimeError( + "FastFuels API key not configured. Please either:\n" + " 1. Set the FASTFUELS_API_KEY environment variable, or\n" + " 2. Call fastfuels_sdk.v2.api.set_api_key('your-api-key') " + "before making API calls" + ) + return client + + +def get_quotas() -> Quotas: + """Return the authenticated owner's resolved quotas. + + Returns + ------- + Quotas + Count, concurrency, storage, dispatch, and retention limits for the + owner authenticated by the current API key. + + Raises + ------ + RuntimeError + If no API key is configured. + ApiException + If the API request fails. + """ + owner = expect(get_me.sync_detailed(client=ensure_client())) + return owner.quotas + + +def get_usage() -> Usage: + """Return the authenticated owner's current usage and limits. + + Returns + ------- + Usage + Usage for job resources, count-only resources, storage, and the + owner's resource-retention policy. + + Raises + ------ + RuntimeError + If no API key is configured. + ApiException + If the API request fails. + """ + return expect(get_me_usage.sync_detailed(client=ensure_client())) diff --git a/fastfuels_sdk/v2/calibrations.py b/fastfuels_sdk/v2/calibrations.py new file mode 100644 index 0000000..4add3f6 --- /dev/null +++ b/fastfuels_sdk/v2/calibrations.py @@ -0,0 +1,196 @@ +"""Calibration builders for generated v2 request models.""" + +from collections.abc import Mapping +from numbers import Real + +from fastfuels_sdk.v2.client_library.models import ( + DuetCalibration, + DuetConstantCalibrationTarget, + DuetMaxMinCalibrationTarget, + DuetMeanSdCalibrationTarget, + DuetParameterCalibration, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +__all__ = ["duet_calibration"] + +_DUET_FUEL_TYPES = {"grass", "coniferous", "deciduous", "litter", "all"} +_DUET_TARGET_TYPES = ( + DuetConstantCalibrationTarget, + DuetMaxMinCalibrationTarget, + DuetMeanSdCalibrationTarget, +) + + +def duet_calibration( + *, + fuel_load=None, + fuel_depth=None, + fuel_moisture=None, +) -> DuetCalibration: + """Build calibration targets for a DUET surface-fuel grid. + + Each argument maps fuel types (``"grass"``, ``"coniferous"``, + ``"deciduous"``, ``"litter"``, or ``"all"``) to a target. Target fields + select the calibration method: ``value`` for constant, ``max`` and + optional ``min`` for max-min, or ``mean`` and ``sd`` for mean-standard + deviation. An explicit ``method`` field is also accepted. + + Parameters + ---------- + fuel_load : mapping, optional + Per-fuel-type load targets. + fuel_depth : mapping, optional + Per-fuel-type depth targets. + fuel_moisture : mapping, optional + Per-fuel-type moisture targets. + + Returns + ------- + DuetCalibration + Calibration for + :func:`fastfuels_sdk.v2.grids.create_surface_fuel_grid_from_duet`. + + Raises + ------ + TypeError + If a parameter or target is not a mapping or generated target model. + ValueError + If no parameter is provided, a target is invalid, or mutually + exclusive fuel types are combined. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> calibration = ff.duet_calibration( + ... fuel_load={ + ... "grass": {"mean": 0.5, "sd": 0.25}, + ... "litter": {"max": 5.0, "min": 0.0}, + ... }, + ... fuel_depth={ + ... "grass": {"value": 0.3}, + ... "litter": {"value": 0.06}, + ... }, + ... ) + """ + parameters = { + "fuel_load": fuel_load, + "fuel_depth": fuel_depth, + "fuel_moisture": fuel_moisture, + } + if not any(value is not None for value in parameters.values()): + raise ValueError( + "duet_calibration requires at least one of fuel_load, fuel_depth, " + "or fuel_moisture." + ) + + return DuetCalibration( + **{ + name: _duet_parameter(value, name) if value is not None else UNSET + for name, value in parameters.items() + } + ) + + +def _duet_parameter(value, parameter: str) -> DuetParameterCalibration: + if isinstance(value, DuetParameterCalibration): + value = value.to_dict() + if not isinstance(value, Mapping): + raise TypeError(f"{parameter} must be a mapping of fuel types to targets.") + if not value: + raise ValueError(f"{parameter} requires at least one fuel-type target.") + + unknown = set(value) - _DUET_FUEL_TYPES + if unknown: + raise ValueError( + f"Unknown {parameter} fuel types: {sorted(unknown)}. Use one of " + f"{sorted(_DUET_FUEL_TYPES)}." + ) + if "all" in value and len(value) > 1: + raise ValueError( + f"{parameter} 'all' cannot be combined with per-fuel-type targets." + ) + if "litter" in value and ({"coniferous", "deciduous"} & set(value)): + raise ValueError( + f"{parameter} 'litter' cannot be combined with 'coniferous' or " + "'deciduous'." + ) + + targets = { + name: _duet_target(target, f"{parameter}.{name}") + for name, target in value.items() + } + if "all" in targets: + targets["all_"] = targets.pop("all") + return DuetParameterCalibration(**targets) + + +def _duet_target(value, path: str): + if isinstance(value, _DUET_TARGET_TYPES): + value = value.to_dict() + if not isinstance(value, Mapping): + raise TypeError(f"{path} must be a calibration-target mapping.") + + data = dict(value) + method = data.pop("method", None) + if method is None: + method = _infer_duet_method(data, path) + + if method == "constant": + _require_fields(data, path, required={"value"}) + return DuetConstantCalibrationTarget(value=_nonnegative(data["value"], path)) + if method == "maxmin": + _require_fields(data, path, required={"max"}, optional={"min"}) + maximum = _nonnegative(data["max"], f"{path}.max") + minimum = _nonnegative(data.get("min", 0.0), f"{path}.min") + if maximum < minimum: + raise ValueError(f"{path}.max must be greater than or equal to min.") + return DuetMaxMinCalibrationTarget(max_=maximum, min_=minimum) + if method == "meansd": + _require_fields(data, path, required={"mean", "sd"}) + return DuetMeanSdCalibrationTarget( + mean=_nonnegative(data["mean"], f"{path}.mean"), + sd=_nonnegative(data["sd"], f"{path}.sd"), + ) + raise ValueError( + f"Unknown calibration method {method!r} for {path}. Use 'constant', " + "'maxmin', or 'meansd'." + ) + + +def _infer_duet_method(data: Mapping, path: str) -> str: + fields = set(data) + if "value" in fields: + return "constant" + if fields & {"max", "min"}: + return "maxmin" + if fields & {"mean", "sd"}: + return "meansd" + raise ValueError( + f"Cannot infer a calibration method for {path}; provide value, max, " + "or mean and sd." + ) + + +def _require_fields( + data: Mapping, + path: str, + *, + required: set[str], + optional: set[str] | None = None, +) -> None: + optional = optional or set() + missing = required - set(data) + if missing: + raise ValueError(f"{path} is missing required fields: {sorted(missing)}.") + extra = set(data) - required - optional + if extra: + raise ValueError(f"{path} has fields not used by this method: {sorted(extra)}.") + + +def _nonnegative(value, path: str) -> float: + if isinstance(value, bool) or not isinstance(value, Real): + raise TypeError(f"{path} must be a number.") + if value < 0: + raise ValueError(f"{path} must be nonnegative.") + return float(value) diff --git a/fastfuels_sdk/v2/client_library/__init__.py b/fastfuels_sdk/v2/client_library/__init__.py new file mode 100644 index 0000000..174eaa3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing FastFuels API""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/fastfuels_sdk/v2/client_library/api/__init__.py b/fastfuels_sdk/v2/client_library/api/__init__.py new file mode 100644 index 0000000..81f9fa2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/applications/__init__.py b/fastfuels_sdk/v2/client_library/api/applications/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/applications/create_application.py b/fastfuels_sdk/v2/client_library/api/applications/create_application.py new file mode 100644 index 0000000..e0e847b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/create_application.py @@ -0,0 +1,180 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.application import Application +from ...models.create_application_request import CreateApplicationRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + *, + body: CreateApplicationRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/applications", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Application | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Application.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Application | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: CreateApplicationRequest, +) -> Response[Application | HTTPValidationError | QuotaExceededDetail]: + """Create an application + + Create a new application. Only personal-access users can create applications. + + Args: + body (CreateApplicationRequest): Request body for creating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: CreateApplicationRequest, +) -> Application | HTTPValidationError | QuotaExceededDetail | None: + """Create an application + + Create a new application. Only personal-access users can create applications. + + Args: + body (CreateApplicationRequest): Request body for creating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: CreateApplicationRequest, +) -> Response[Application | HTTPValidationError | QuotaExceededDetail]: + """Create an application + + Create a new application. Only personal-access users can create applications. + + Args: + body (CreateApplicationRequest): Request body for creating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: CreateApplicationRequest, +) -> Application | HTTPValidationError | QuotaExceededDetail | None: + """Create an application + + Create a new application. Only personal-access users can create applications. + + Args: + body (CreateApplicationRequest): Request body for creating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/applications/delete_application.py b/fastfuels_sdk/v2/client_library/api/applications/delete_application.py new file mode 100644 index 0000000..a75f856 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/delete_application.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + application_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/applications/{application_id}".format( + application_id=quote(str(application_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an application + + Delete an application and all its API keys. Validates ownership first. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + application_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an application + + Delete an application and all its API keys. Validates ownership first. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + application_id=application_id, + client=client, + ).parsed + + +async def asyncio_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an application + + Delete an application and all its API keys. Validates ownership first. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + application_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an application + + Delete an application and all its API keys. Validates ownership first. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + application_id=application_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/applications/get_application.py b/fastfuels_sdk/v2/client_library/api/applications/get_application.py new file mode 100644 index 0000000..c848e28 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/get_application.py @@ -0,0 +1,169 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.application import Application +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + application_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/applications/{application_id}".format( + application_id=quote(str(application_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Application | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Application.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Application | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[Application | HTTPValidationError]: + """Get an application by ID + + Get an application by ID with ownership check. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + application_id: str, + *, + client: AuthenticatedClient, +) -> Application | HTTPValidationError | None: + """Get an application by ID + + Get an application by ID with ownership check. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError + """ + + return sync_detailed( + application_id=application_id, + client=client, + ).parsed + + +async def asyncio_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[Application | HTTPValidationError]: + """Get an application by ID + + Get an application by ID with ownership check. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + application_id: str, + *, + client: AuthenticatedClient, +) -> Application | HTTPValidationError | None: + """Get an application by ID + + Get an application by ID with ownership check. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError + """ + + return ( + await asyncio_detailed( + application_id=application_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/applications/get_application_usage.py b/fastfuels_sdk/v2/client_library/api/applications/get_application_usage.py new file mode 100644 index 0000000..1c9ae31 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/get_application_usage.py @@ -0,0 +1,181 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.usage import Usage +from ...types import Response + + +def _get_kwargs( + application_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/applications/{application_id}/usage".format( + application_id=quote(str(application_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Usage | None: + if response.status_code == 200: + response_200 = Usage.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Usage]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Usage]: + """Get an application's usage + + Get an owned application's usage against its resolved limits, per resource type. + + Same shape as `GET /users/me/usage`, for an application the caller owns — + so a user can read an application's usage without authenticating as it. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Usage] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + application_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Usage | None: + """Get an application's usage + + Get an owned application's usage against its resolved limits, per resource type. + + Same shape as `GET /users/me/usage`, for an application the caller owns — + so a user can read an application's usage without authenticating as it. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Usage + """ + + return sync_detailed( + application_id=application_id, + client=client, + ).parsed + + +async def asyncio_detailed( + application_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Usage]: + """Get an application's usage + + Get an owned application's usage against its resolved limits, per resource type. + + Same shape as `GET /users/me/usage`, for an application the caller owns — + so a user can read an application's usage without authenticating as it. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Usage] + """ + + kwargs = _get_kwargs( + application_id=application_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + application_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Usage | None: + """Get an application's usage + + Get an owned application's usage against its resolved limits, per resource type. + + Same shape as `GET /users/me/usage`, for an application the caller owns — + so a user can read an application's usage without authenticating as it. + + Args: + application_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Usage + """ + + return ( + await asyncio_detailed( + application_id=application_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/applications/list_applications.py b/fastfuels_sdk/v2/client_library/api/applications/list_applications.py new file mode 100644 index 0000000..d7effbd --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/list_applications.py @@ -0,0 +1,189 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.list_applications_response import ListApplicationsResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/applications", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListApplicationsResponse | None: + if response.status_code == 200: + response_200 = ListApplicationsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListApplicationsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> Response[HTTPValidationError | ListApplicationsResponse]: + """List applications + + List applications owned by the authenticated user. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListApplicationsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> HTTPValidationError | ListApplicationsResponse | None: + """List applications + + List applications owned by the authenticated user. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListApplicationsResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> Response[HTTPValidationError | ListApplicationsResponse]: + """List applications + + List applications owned by the authenticated user. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListApplicationsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> HTTPValidationError | ListApplicationsResponse | None: + """List applications + + List applications owned by the authenticated user. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListApplicationsResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/applications/update_application.py b/fastfuels_sdk/v2/client_library/api/applications/update_application.py new file mode 100644 index 0000000..b2fb453 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/applications/update_application.py @@ -0,0 +1,190 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.application import Application +from ...models.http_validation_error import HTTPValidationError +from ...models.update_application_request import UpdateApplicationRequest +from ...types import Response + + +def _get_kwargs( + application_id: str, + *, + body: UpdateApplicationRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/applications/{application_id}".format( + application_id=quote(str(application_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Application | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Application.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Application | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + application_id: str, + *, + client: AuthenticatedClient, + body: UpdateApplicationRequest, +) -> Response[Application | HTTPValidationError]: + """Update an application + + Update an application's name or description. + + Args: + application_id (str): + body (UpdateApplicationRequest): Request body for updating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + application_id: str, + *, + client: AuthenticatedClient, + body: UpdateApplicationRequest, +) -> Application | HTTPValidationError | None: + """Update an application + + Update an application's name or description. + + Args: + application_id (str): + body (UpdateApplicationRequest): Request body for updating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError + """ + + return sync_detailed( + application_id=application_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + application_id: str, + *, + client: AuthenticatedClient, + body: UpdateApplicationRequest, +) -> Response[Application | HTTPValidationError]: + """Update an application + + Update an application's name or description. + + Args: + application_id (str): + body (UpdateApplicationRequest): Request body for updating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Application | HTTPValidationError] + """ + + kwargs = _get_kwargs( + application_id=application_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + application_id: str, + *, + client: AuthenticatedClient, + body: UpdateApplicationRequest, +) -> Application | HTTPValidationError | None: + """Update an application + + Update an application's name or description. + + Args: + application_id (str): + body (UpdateApplicationRequest): Request body for updating an application. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Application | HTTPValidationError + """ + + return ( + await asyncio_detailed( + application_id=application_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/__init__.py b/fastfuels_sdk/v2/client_library/api/domains/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/domains/create_domain.py b/fastfuels_sdk/v2/client_library/api/domains/create_domain.py new file mode 100644 index 0000000..be87d55 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/create_domain.py @@ -0,0 +1,636 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain import Domain +from ...models.geo_json_feature_collection import GeoJsonFeatureCollection +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + *, + body: GeoJsonFeatureCollection, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Domain | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Domain.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Domain | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Response[Domain | HTTPValidationError | QuotaExceededDetail]: + r"""Create a new domain + + # Create Domain Endpoint + + This endpoint creates a new domain resource based on a spatial extent and + additional details provided by the user. The domain resource acts as the + spatial container for all other resources that create data within the system. + + ## What is a Domain Resource? + + A domain resource is a spatial container that represents a specific geographical + area. It includes metadata such as the name, description, creation date, and the + spatial extent defined by geographic coordinates. Domains are used to organize + and manage spatial data and operations within a defined area. + + ## Request Body + + The request body must be a GeoJSON FeatureCollection as defined by the + [GeoJSON specification (RFC 7946)](https://datatracker.ietf.org/doc/html/rfc7946). + + ### Required Fields + + - **type**: (string) Must be \"FeatureCollection\". + - **features**: (array) An array of Feature objects. Each Feature must have: + - **type**: (string) Must be \"Feature\". + - **geometry**: (GeoJSON Geometry) A geometry object (typically Polygon). + - **type**: (string) Must be a valid GeoJSON type, e.g., \"Polygon\". + - **coordinates**: (array) An array of coordinates defining the geometry. + + ### Optional Fields + + - **name**: (string) The name of the domain. Default: empty string. + - **description**: (string) A brief description of the domain. Default: empty string. + - **tags**: (array of strings) Tags for organizing and filtering domains. + - **crs**: (object) The coordinate reference system. Default: EPSG:4326 (WGS84). + - **type**: (string) Must be \"name\". + - **properties**: (object) Contains the CRS details. + - **name**: (string) The CRS identifier, e.g., \"EPSG:4326\", \"EPSG:5070\", + or URN format \"urn:ogc:def:crs:EPSG::32611\". + - **pad_to_resolution**: (number) Optional resolution in meters to snap the + domain bounding box to. When set, the bounding box (the \"domain\" feature) + is snapped outward to the nearest multiple of this value. Grids whose + resolutions divide evenly into this value will produce identical, aligned + footprints on this domain. Useful for compositional workflows where + multiple grids at different resolutions need to share an extent. + - **style**: (object) Optional visual style for rendering the domain on a + map. Sub-fields: `stroke_color`, `stroke_opacity` (0-1), `stroke_width` + (>= 0), `fill_color`, `fill_opacity` (0-1). Color strings accept any + format the renderer understands (hex, named, `rgb()`, ...) and are + capped at 64 characters. + + ## Response + + On successful creation, returns the domain resource with: + + - **id**: (string) A unique 32-character hex identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) A single feature named `\"domain\"` — a polygon + covering the working extent (bounding box of the input, possibly + padded). This is what griddle, standgen, and exporter use as the + authoritative spatial extent. + - **bbox**: (array) Standard GeoJSON bbox `[minx, miny, maxx, maxy]` in the + domain's projected CRS. Equals the bounds of the \"domain\" feature. + - **pad_to_resolution**: (number, optional) The padding value, if set. + + ## CRS Handling + + The API handles coordinate reference systems as follows: + + 1. **Geographic CRS (e.g., EPSG:4326)**: Automatically projected to the + appropriate UTM zone based on the geometry's centroid. The response CRS + will be the UTM zone (e.g., EPSG:32611 for UTM Zone 11N). + + 2. **Projected CRS (e.g., EPSG:5070, EPSG:32611)**: Used as-is without + reprojection. The response CRS will match the input CRS. + + ## Validation + + The following validations are performed: + + 1. **CRS Validation**: Must be a valid EPSG code or URN format. + 2. **Area Validation**: Geometry must have non-zero area (no points or lines). + 3. **Location**: Geometry must be entirely within CONUS (Continental US). + Validated against the original input polygon (not the padded bbox). + 4. **Size Limit**: The working extent (possibly padded bbox) must be less + than 16 square kilometers. + + ## Important Notes + + 1. **FeatureCollection Only**: Unlike v1, this endpoint only accepts + FeatureCollection input, not individual Feature objects. Wrap single + features in a FeatureCollection. + + 2. **Working-Extent Output**: The created domain stores a single \"domain\" + feature — the bounding box of the input geometry, which is the working + extent used by all downstream services. The submitted geometry itself + is not stored. + + 3. **Projection**: Geographic coordinates are always projected to a suitable + UTM zone for accurate area calculations and grid operations. + + 4. **Maximum Area**: The 16 sq km limit ensures reasonable processing times. + Contact support if you need larger domains. + + ## Error Responses + + - **422 Unprocessable Entity**: + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Domain | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a new domain + + # Create Domain Endpoint + + This endpoint creates a new domain resource based on a spatial extent and + additional details provided by the user. The domain resource acts as the + spatial container for all other resources that create data within the system. + + ## What is a Domain Resource? + + A domain resource is a spatial container that represents a specific geographical + area. It includes metadata such as the name, description, creation date, and the + spatial extent defined by geographic coordinates. Domains are used to organize + and manage spatial data and operations within a defined area. + + ## Request Body + + The request body must be a GeoJSON FeatureCollection as defined by the + [GeoJSON specification (RFC 7946)](https://datatracker.ietf.org/doc/html/rfc7946). + + ### Required Fields + + - **type**: (string) Must be \"FeatureCollection\". + - **features**: (array) An array of Feature objects. Each Feature must have: + - **type**: (string) Must be \"Feature\". + - **geometry**: (GeoJSON Geometry) A geometry object (typically Polygon). + - **type**: (string) Must be a valid GeoJSON type, e.g., \"Polygon\". + - **coordinates**: (array) An array of coordinates defining the geometry. + + ### Optional Fields + + - **name**: (string) The name of the domain. Default: empty string. + - **description**: (string) A brief description of the domain. Default: empty string. + - **tags**: (array of strings) Tags for organizing and filtering domains. + - **crs**: (object) The coordinate reference system. Default: EPSG:4326 (WGS84). + - **type**: (string) Must be \"name\". + - **properties**: (object) Contains the CRS details. + - **name**: (string) The CRS identifier, e.g., \"EPSG:4326\", \"EPSG:5070\", + or URN format \"urn:ogc:def:crs:EPSG::32611\". + - **pad_to_resolution**: (number) Optional resolution in meters to snap the + domain bounding box to. When set, the bounding box (the \"domain\" feature) + is snapped outward to the nearest multiple of this value. Grids whose + resolutions divide evenly into this value will produce identical, aligned + footprints on this domain. Useful for compositional workflows where + multiple grids at different resolutions need to share an extent. + - **style**: (object) Optional visual style for rendering the domain on a + map. Sub-fields: `stroke_color`, `stroke_opacity` (0-1), `stroke_width` + (>= 0), `fill_color`, `fill_opacity` (0-1). Color strings accept any + format the renderer understands (hex, named, `rgb()`, ...) and are + capped at 64 characters. + + ## Response + + On successful creation, returns the domain resource with: + + - **id**: (string) A unique 32-character hex identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) A single feature named `\"domain\"` — a polygon + covering the working extent (bounding box of the input, possibly + padded). This is what griddle, standgen, and exporter use as the + authoritative spatial extent. + - **bbox**: (array) Standard GeoJSON bbox `[minx, miny, maxx, maxy]` in the + domain's projected CRS. Equals the bounds of the \"domain\" feature. + - **pad_to_resolution**: (number, optional) The padding value, if set. + + ## CRS Handling + + The API handles coordinate reference systems as follows: + + 1. **Geographic CRS (e.g., EPSG:4326)**: Automatically projected to the + appropriate UTM zone based on the geometry's centroid. The response CRS + will be the UTM zone (e.g., EPSG:32611 for UTM Zone 11N). + + 2. **Projected CRS (e.g., EPSG:5070, EPSG:32611)**: Used as-is without + reprojection. The response CRS will match the input CRS. + + ## Validation + + The following validations are performed: + + 1. **CRS Validation**: Must be a valid EPSG code or URN format. + 2. **Area Validation**: Geometry must have non-zero area (no points or lines). + 3. **Location**: Geometry must be entirely within CONUS (Continental US). + Validated against the original input polygon (not the padded bbox). + 4. **Size Limit**: The working extent (possibly padded bbox) must be less + than 16 square kilometers. + + ## Important Notes + + 1. **FeatureCollection Only**: Unlike v1, this endpoint only accepts + FeatureCollection input, not individual Feature objects. Wrap single + features in a FeatureCollection. + + 2. **Working-Extent Output**: The created domain stores a single \"domain\" + feature — the bounding box of the input geometry, which is the working + extent used by all downstream services. The submitted geometry itself + is not stored. + + 3. **Projection**: Geographic coordinates are always projected to a suitable + UTM zone for accurate area calculations and grid operations. + + 4. **Maximum Area**: The 16 sq km limit ensures reasonable processing times. + Contact support if you need larger domains. + + ## Error Responses + + - **422 Unprocessable Entity**: + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Response[Domain | HTTPValidationError | QuotaExceededDetail]: + r"""Create a new domain + + # Create Domain Endpoint + + This endpoint creates a new domain resource based on a spatial extent and + additional details provided by the user. The domain resource acts as the + spatial container for all other resources that create data within the system. + + ## What is a Domain Resource? + + A domain resource is a spatial container that represents a specific geographical + area. It includes metadata such as the name, description, creation date, and the + spatial extent defined by geographic coordinates. Domains are used to organize + and manage spatial data and operations within a defined area. + + ## Request Body + + The request body must be a GeoJSON FeatureCollection as defined by the + [GeoJSON specification (RFC 7946)](https://datatracker.ietf.org/doc/html/rfc7946). + + ### Required Fields + + - **type**: (string) Must be \"FeatureCollection\". + - **features**: (array) An array of Feature objects. Each Feature must have: + - **type**: (string) Must be \"Feature\". + - **geometry**: (GeoJSON Geometry) A geometry object (typically Polygon). + - **type**: (string) Must be a valid GeoJSON type, e.g., \"Polygon\". + - **coordinates**: (array) An array of coordinates defining the geometry. + + ### Optional Fields + + - **name**: (string) The name of the domain. Default: empty string. + - **description**: (string) A brief description of the domain. Default: empty string. + - **tags**: (array of strings) Tags for organizing and filtering domains. + - **crs**: (object) The coordinate reference system. Default: EPSG:4326 (WGS84). + - **type**: (string) Must be \"name\". + - **properties**: (object) Contains the CRS details. + - **name**: (string) The CRS identifier, e.g., \"EPSG:4326\", \"EPSG:5070\", + or URN format \"urn:ogc:def:crs:EPSG::32611\". + - **pad_to_resolution**: (number) Optional resolution in meters to snap the + domain bounding box to. When set, the bounding box (the \"domain\" feature) + is snapped outward to the nearest multiple of this value. Grids whose + resolutions divide evenly into this value will produce identical, aligned + footprints on this domain. Useful for compositional workflows where + multiple grids at different resolutions need to share an extent. + - **style**: (object) Optional visual style for rendering the domain on a + map. Sub-fields: `stroke_color`, `stroke_opacity` (0-1), `stroke_width` + (>= 0), `fill_color`, `fill_opacity` (0-1). Color strings accept any + format the renderer understands (hex, named, `rgb()`, ...) and are + capped at 64 characters. + + ## Response + + On successful creation, returns the domain resource with: + + - **id**: (string) A unique 32-character hex identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) A single feature named `\"domain\"` — a polygon + covering the working extent (bounding box of the input, possibly + padded). This is what griddle, standgen, and exporter use as the + authoritative spatial extent. + - **bbox**: (array) Standard GeoJSON bbox `[minx, miny, maxx, maxy]` in the + domain's projected CRS. Equals the bounds of the \"domain\" feature. + - **pad_to_resolution**: (number, optional) The padding value, if set. + + ## CRS Handling + + The API handles coordinate reference systems as follows: + + 1. **Geographic CRS (e.g., EPSG:4326)**: Automatically projected to the + appropriate UTM zone based on the geometry's centroid. The response CRS + will be the UTM zone (e.g., EPSG:32611 for UTM Zone 11N). + + 2. **Projected CRS (e.g., EPSG:5070, EPSG:32611)**: Used as-is without + reprojection. The response CRS will match the input CRS. + + ## Validation + + The following validations are performed: + + 1. **CRS Validation**: Must be a valid EPSG code or URN format. + 2. **Area Validation**: Geometry must have non-zero area (no points or lines). + 3. **Location**: Geometry must be entirely within CONUS (Continental US). + Validated against the original input polygon (not the padded bbox). + 4. **Size Limit**: The working extent (possibly padded bbox) must be less + than 16 square kilometers. + + ## Important Notes + + 1. **FeatureCollection Only**: Unlike v1, this endpoint only accepts + FeatureCollection input, not individual Feature objects. Wrap single + features in a FeatureCollection. + + 2. **Working-Extent Output**: The created domain stores a single \"domain\" + feature — the bounding box of the input geometry, which is the working + extent used by all downstream services. The submitted geometry itself + is not stored. + + 3. **Projection**: Geographic coordinates are always projected to a suitable + UTM zone for accurate area calculations and grid operations. + + 4. **Maximum Area**: The 16 sq km limit ensures reasonable processing times. + Contact support if you need larger domains. + + ## Error Responses + + - **422 Unprocessable Entity**: + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Domain | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a new domain + + # Create Domain Endpoint + + This endpoint creates a new domain resource based on a spatial extent and + additional details provided by the user. The domain resource acts as the + spatial container for all other resources that create data within the system. + + ## What is a Domain Resource? + + A domain resource is a spatial container that represents a specific geographical + area. It includes metadata such as the name, description, creation date, and the + spatial extent defined by geographic coordinates. Domains are used to organize + and manage spatial data and operations within a defined area. + + ## Request Body + + The request body must be a GeoJSON FeatureCollection as defined by the + [GeoJSON specification (RFC 7946)](https://datatracker.ietf.org/doc/html/rfc7946). + + ### Required Fields + + - **type**: (string) Must be \"FeatureCollection\". + - **features**: (array) An array of Feature objects. Each Feature must have: + - **type**: (string) Must be \"Feature\". + - **geometry**: (GeoJSON Geometry) A geometry object (typically Polygon). + - **type**: (string) Must be a valid GeoJSON type, e.g., \"Polygon\". + - **coordinates**: (array) An array of coordinates defining the geometry. + + ### Optional Fields + + - **name**: (string) The name of the domain. Default: empty string. + - **description**: (string) A brief description of the domain. Default: empty string. + - **tags**: (array of strings) Tags for organizing and filtering domains. + - **crs**: (object) The coordinate reference system. Default: EPSG:4326 (WGS84). + - **type**: (string) Must be \"name\". + - **properties**: (object) Contains the CRS details. + - **name**: (string) The CRS identifier, e.g., \"EPSG:4326\", \"EPSG:5070\", + or URN format \"urn:ogc:def:crs:EPSG::32611\". + - **pad_to_resolution**: (number) Optional resolution in meters to snap the + domain bounding box to. When set, the bounding box (the \"domain\" feature) + is snapped outward to the nearest multiple of this value. Grids whose + resolutions divide evenly into this value will produce identical, aligned + footprints on this domain. Useful for compositional workflows where + multiple grids at different resolutions need to share an extent. + - **style**: (object) Optional visual style for rendering the domain on a + map. Sub-fields: `stroke_color`, `stroke_opacity` (0-1), `stroke_width` + (>= 0), `fill_color`, `fill_opacity` (0-1). Color strings accept any + format the renderer understands (hex, named, `rgb()`, ...) and are + capped at 64 characters. + + ## Response + + On successful creation, returns the domain resource with: + + - **id**: (string) A unique 32-character hex identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) A single feature named `\"domain\"` — a polygon + covering the working extent (bounding box of the input, possibly + padded). This is what griddle, standgen, and exporter use as the + authoritative spatial extent. + - **bbox**: (array) Standard GeoJSON bbox `[minx, miny, maxx, maxy]` in the + domain's projected CRS. Equals the bounds of the \"domain\" feature. + - **pad_to_resolution**: (number, optional) The padding value, if set. + + ## CRS Handling + + The API handles coordinate reference systems as follows: + + 1. **Geographic CRS (e.g., EPSG:4326)**: Automatically projected to the + appropriate UTM zone based on the geometry's centroid. The response CRS + will be the UTM zone (e.g., EPSG:32611 for UTM Zone 11N). + + 2. **Projected CRS (e.g., EPSG:5070, EPSG:32611)**: Used as-is without + reprojection. The response CRS will match the input CRS. + + ## Validation + + The following validations are performed: + + 1. **CRS Validation**: Must be a valid EPSG code or URN format. + 2. **Area Validation**: Geometry must have non-zero area (no points or lines). + 3. **Location**: Geometry must be entirely within CONUS (Continental US). + Validated against the original input polygon (not the padded bbox). + 4. **Size Limit**: The working extent (possibly padded bbox) must be less + than 16 square kilometers. + + ## Important Notes + + 1. **FeatureCollection Only**: Unlike v1, this endpoint only accepts + FeatureCollection input, not individual Feature objects. Wrap single + features in a FeatureCollection. + + 2. **Working-Extent Output**: The created domain stores a single \"domain\" + feature — the bounding box of the input geometry, which is the working + extent used by all downstream services. The submitted geometry itself + is not stored. + + 3. **Projection**: Geographic coordinates are always projected to a suitable + UTM zone for accurate area calculations and grid operations. + + 4. **Maximum Area**: The 16 sq km limit ensures reasonable processing times. + Contact support if you need larger domains. + + ## Error Responses + + - **422 Unprocessable Entity**: + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/delete_domain.py b/fastfuels_sdk/v2/client_library/api/domains/delete_domain.py new file mode 100644 index 0000000..0334849 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/delete_domain.py @@ -0,0 +1,308 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + force: bool | Unset = False, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["force"] = force + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/domains/{domain_id}".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + force: bool | Unset = False, +) -> Response[Any | HTTPValidationError]: + """Delete a domain + + # Delete Domain Endpoint + + This endpoint permanently deletes a domain resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Query Parameters + + - **force**: (boolean, optional) If true, cascade-deletes all child resources + (grids, etc.) before deleting the domain. Default: false. + + ## Response + + On success, returns HTTP 204 No Content with an empty response body. + + ## Cascade Behavior (AIP-135) + + - **Without `force`**: If the domain has child grids, returns 412 Precondition + Failed. Delete child resources first, or use `force=true`. + - **With `force=true`**: Deletes the domain and all child grids in a single + operation. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **412 Precondition Failed**: The domain has child resources and `force` was + not set to true. + + Args: + domain_id (str): + force (bool | Unset): Force cascade delete of all child resources (grids, etc.). Without + this, returns 412 if child resources exist. Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + force=force, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + force: bool | Unset = False, +) -> Any | HTTPValidationError | None: + """Delete a domain + + # Delete Domain Endpoint + + This endpoint permanently deletes a domain resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Query Parameters + + - **force**: (boolean, optional) If true, cascade-deletes all child resources + (grids, etc.) before deleting the domain. Default: false. + + ## Response + + On success, returns HTTP 204 No Content with an empty response body. + + ## Cascade Behavior (AIP-135) + + - **Without `force`**: If the domain has child grids, returns 412 Precondition + Failed. Delete child resources first, or use `force=true`. + - **With `force=true`**: Deletes the domain and all child grids in a single + operation. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **412 Precondition Failed**: The domain has child resources and `force` was + not set to true. + + Args: + domain_id (str): + force (bool | Unset): Force cascade delete of all child resources (grids, etc.). Without + this, returns 412 if child resources exist. Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + force=force, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + force: bool | Unset = False, +) -> Response[Any | HTTPValidationError]: + """Delete a domain + + # Delete Domain Endpoint + + This endpoint permanently deletes a domain resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Query Parameters + + - **force**: (boolean, optional) If true, cascade-deletes all child resources + (grids, etc.) before deleting the domain. Default: false. + + ## Response + + On success, returns HTTP 204 No Content with an empty response body. + + ## Cascade Behavior (AIP-135) + + - **Without `force`**: If the domain has child grids, returns 412 Precondition + Failed. Delete child resources first, or use `force=true`. + - **With `force=true`**: Deletes the domain and all child grids in a single + operation. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **412 Precondition Failed**: The domain has child resources and `force` was + not set to true. + + Args: + domain_id (str): + force (bool | Unset): Force cascade delete of all child resources (grids, etc.). Without + this, returns 412 if child resources exist. Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + force=force, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + force: bool | Unset = False, +) -> Any | HTTPValidationError | None: + """Delete a domain + + # Delete Domain Endpoint + + This endpoint permanently deletes a domain resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Query Parameters + + - **force**: (boolean, optional) If true, cascade-deletes all child resources + (grids, etc.) before deleting the domain. Default: false. + + ## Response + + On success, returns HTTP 204 No Content with an empty response body. + + ## Cascade Behavior (AIP-135) + + - **Without `force`**: If the domain has child grids, returns 412 Precondition + Failed. Delete child resources first, or use `force=true`. + - **With `force=true`**: Deletes the domain and all child grids in a single + operation. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **412 Precondition Failed**: The domain has child resources and `force` was + not set to true. + + Args: + domain_id (str): + force (bool | Unset): Force cascade delete of all child resources (grids, etc.). Without + this, returns 412 if child resources exist. Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + force=force, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/get_domain.py b/fastfuels_sdk/v2/client_library/api/domains/get_domain.py new file mode 100644 index 0000000..0bdc681 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/get_domain.py @@ -0,0 +1,273 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain import Domain +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Domain | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Domain.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Domain | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Response[Domain | HTTPValidationError]: + r"""Get a domain by ID + + # Get Domain Endpoint + + This endpoint retrieves a specific domain resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Response + + On success, returns the domain resource with: + + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) The domain geometry features. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - Returns 404 for both missing documents and ownership mismatches to avoid + leaking information about document existence. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Domain | HTTPValidationError | None: + r"""Get a domain by ID + + # Get Domain Endpoint + + This endpoint retrieves a specific domain resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Response + + On success, returns the domain resource with: + + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) The domain geometry features. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - Returns 404 for both missing documents and ownership mismatches to avoid + leaking information about document existence. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Response[Domain | HTTPValidationError]: + r"""Get a domain by ID + + # Get Domain Endpoint + + This endpoint retrieves a specific domain resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Response + + On success, returns the domain resource with: + + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) The domain geometry features. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - Returns 404 for both missing documents and ownership mismatches to avoid + leaking information about document existence. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Domain | HTTPValidationError | None: + r"""Get a domain by ID + + # Get Domain Endpoint + + This endpoint retrieves a specific domain resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Response + + On success, returns the domain resource with: + + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system (always projected). + - **features**: (array) The domain geometry features. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - Returns 404 for both missing documents and ownership mismatches to avoid + leaking information about document existence. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/get_domain_lattice.py b/fastfuels_sdk/v2/client_library/api/domains/get_domain_lattice.py new file mode 100644 index 0000000..d0a87e5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/get_domain_lattice.py @@ -0,0 +1,317 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain_lattice import DomainLattice +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + resolution: float, + num_buffer_cells: int | Unset = 0, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["resolution"] = resolution + + params["num_buffer_cells"] = num_buffer_cells + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/lattice".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DomainLattice | HTTPValidationError | None: + if response.status_code == 200: + response_200 = DomainLattice.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DomainLattice | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: float, + num_buffer_cells: int | Unset = 0, +) -> Response[DomainLattice | HTTPValidationError]: + """Get the pixel lattice for a domain at a given resolution + + # Get Domain Lattice Endpoint + + Returns the pixel lattice (transform + shape) for the domain at the + requested resolution. Use this to align a GeoTIFF before uploading it + via `POST /domains/{domain_id}/grids/upload`. + + ## Query Parameters + + - **resolution** (required): Pixel size in meters. + - **num_buffer_cells** (optional, default 0): Expand the lattice by + `N * resolution` meters on each side. + + ## Response + + - **crs**: The domain CRS (always projected). + - **resolution**: Echoes the input. + - **num_buffer_cells**: Echoes the input. + - **transform**: Affine coefficients `[a, b, c, d, e, f]` (rasterio + convention). + - **shape**: `[height, width]` in pixels. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not + have access. + - **422 Unprocessable Entity**: `resolution` is missing or + non-positive, or `num_buffer_cells` is negative. + + Args: + domain_id (str): + resolution (float): Pixel size in meters (domain CRS units, always projected). + num_buffer_cells (int | Unset): Expand the lattice by N cells on each side. Mirrors the + buffer semantics of POST /domains/{domain_id}/grids/upload and the LANDFIRE/3DEP grid + creation endpoints. Default: 0. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DomainLattice | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + resolution=resolution, + num_buffer_cells=num_buffer_cells, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: float, + num_buffer_cells: int | Unset = 0, +) -> DomainLattice | HTTPValidationError | None: + """Get the pixel lattice for a domain at a given resolution + + # Get Domain Lattice Endpoint + + Returns the pixel lattice (transform + shape) for the domain at the + requested resolution. Use this to align a GeoTIFF before uploading it + via `POST /domains/{domain_id}/grids/upload`. + + ## Query Parameters + + - **resolution** (required): Pixel size in meters. + - **num_buffer_cells** (optional, default 0): Expand the lattice by + `N * resolution` meters on each side. + + ## Response + + - **crs**: The domain CRS (always projected). + - **resolution**: Echoes the input. + - **num_buffer_cells**: Echoes the input. + - **transform**: Affine coefficients `[a, b, c, d, e, f]` (rasterio + convention). + - **shape**: `[height, width]` in pixels. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not + have access. + - **422 Unprocessable Entity**: `resolution` is missing or + non-positive, or `num_buffer_cells` is negative. + + Args: + domain_id (str): + resolution (float): Pixel size in meters (domain CRS units, always projected). + num_buffer_cells (int | Unset): Expand the lattice by N cells on each side. Mirrors the + buffer semantics of POST /domains/{domain_id}/grids/upload and the LANDFIRE/3DEP grid + creation endpoints. Default: 0. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DomainLattice | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + resolution=resolution, + num_buffer_cells=num_buffer_cells, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: float, + num_buffer_cells: int | Unset = 0, +) -> Response[DomainLattice | HTTPValidationError]: + """Get the pixel lattice for a domain at a given resolution + + # Get Domain Lattice Endpoint + + Returns the pixel lattice (transform + shape) for the domain at the + requested resolution. Use this to align a GeoTIFF before uploading it + via `POST /domains/{domain_id}/grids/upload`. + + ## Query Parameters + + - **resolution** (required): Pixel size in meters. + - **num_buffer_cells** (optional, default 0): Expand the lattice by + `N * resolution` meters on each side. + + ## Response + + - **crs**: The domain CRS (always projected). + - **resolution**: Echoes the input. + - **num_buffer_cells**: Echoes the input. + - **transform**: Affine coefficients `[a, b, c, d, e, f]` (rasterio + convention). + - **shape**: `[height, width]` in pixels. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not + have access. + - **422 Unprocessable Entity**: `resolution` is missing or + non-positive, or `num_buffer_cells` is negative. + + Args: + domain_id (str): + resolution (float): Pixel size in meters (domain CRS units, always projected). + num_buffer_cells (int | Unset): Expand the lattice by N cells on each side. Mirrors the + buffer semantics of POST /domains/{domain_id}/grids/upload and the LANDFIRE/3DEP grid + creation endpoints. Default: 0. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DomainLattice | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + resolution=resolution, + num_buffer_cells=num_buffer_cells, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: float, + num_buffer_cells: int | Unset = 0, +) -> DomainLattice | HTTPValidationError | None: + """Get the pixel lattice for a domain at a given resolution + + # Get Domain Lattice Endpoint + + Returns the pixel lattice (transform + shape) for the domain at the + requested resolution. Use this to align a GeoTIFF before uploading it + via `POST /domains/{domain_id}/grids/upload`. + + ## Query Parameters + + - **resolution** (required): Pixel size in meters. + - **num_buffer_cells** (optional, default 0): Expand the lattice by + `N * resolution` meters on each side. + + ## Response + + - **crs**: The domain CRS (always projected). + - **resolution**: Echoes the input. + - **num_buffer_cells**: Echoes the input. + - **transform**: Affine coefficients `[a, b, c, d, e, f]` (rasterio + convention). + - **shape**: `[height, width]` in pixels. + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not + have access. + - **422 Unprocessable Entity**: `resolution` is missing or + non-positive, or `num_buffer_cells` is negative. + + Args: + domain_id (str): + resolution (float): Pixel size in meters (domain CRS units, always projected). + num_buffer_cells (int | Unset): Expand the lattice by N cells on each side. Mirrors the + buffer semantics of POST /domains/{domain_id}/grids/upload and the LANDFIRE/3DEP grid + creation endpoints. Default: 0. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DomainLattice | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + resolution=resolution, + num_buffer_cells=num_buffer_cells, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/list_domains.py b/fastfuels_sdk/v2/client_library/api/domains/list_domains.py new file mode 100644 index 0000000..4e02b13 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/list_domains.py @@ -0,0 +1,595 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain_sort_field import DomainSortField +from ...models.domain_sort_order import DomainSortOrder +from ...models.http_validation_error import HTTPValidationError +from ...models.list_domains_response import ListDomainsResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: DomainSortField | None | Unset = UNSET, + sort_order: DomainSortOrder | None | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, DomainSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, DomainSortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListDomainsResponse | None: + if response.status_code == 200: + response_200 = ListDomainsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListDomainsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: DomainSortField | None | Unset = UNSET, + sort_order: DomainSortOrder | None | Unset = UNSET, +) -> Response[HTTPValidationError | ListDomainsResponse]: + r"""List all domains + + # List Domains Endpoint + + This endpoint retrieves a paginated list of all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) The page number to retrieve. Zero-indexed, + meaning the first page is `0`. Default: 0. + - **size**: (integer, optional) The number of domains to retrieve per page. + Must be between 1 and 1000. Default: 100. + - **sort_by**: (string, optional) The field to sort results by. Valid values: + - `created_on`: Sort by creation date. + - `modified_on`: Sort by last modification date. + - `name`: Sort alphabetically by name. + - **sort_order**: (string, optional) The order to sort results. Valid values: + - `ascending`: Sort in ascending order (A-Z, oldest first). + - `descending`: Sort in descending order (Z-A, newest first). + Default: descending when sort_by is specified. + + ## Response + + Returns a paginated list of domains with metadata: + + - **domains**: (array) List of domain resources for the current page. + Each domain includes: + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system. + - **features**: (array) The domain geometry features. + - **current_page**: (integer) The current page number (zero-indexed). + - **page_size**: (integer) The number of domains per page. + - **total_items**: (integer) The total number of domains owned by the user. + + ## Pagination + + Use `page` and `size` parameters to navigate through large result sets: + + - First page: `?page=0&size=10` + - Second page: `?page=1&size=10` + - Calculate total pages: `ceil(total_items / page_size)` + + ## Sorting + + Combine `sort_by` and `sort_order` for custom ordering: + + - Newest first: `?sort_by=created_on&sort_order=descending` + - Alphabetical: `?sort_by=name&sort_order=ascending` + - Recently modified: `?sort_by=modified_on&sort_order=descending` + + ## Example Request + + ```http + GET /v2/domains?page=0&size=10&sort_by=created_on&sort_order=descending + ``` + + ## Example Response + + ```json + { + \"domains\": [ + { + \"id\": \"abc123...\", + \"type\": \"FeatureCollection\", + \"name\": \"My Domain\", + \"description\": \"A test domain\", + \"created_on\": \"2024-01-15T10:30:00\", + \"modified_on\": \"2024-01-15T10:30:00\", + \"tags\": [\"test\"], + \"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:32611\"}}, + \"features\": [...] + } + ], + \"current_page\": 0, + \"page_size\": 10, + \"total_items\": 42 + } + ``` + + ## Error Responses + + - **422 Unprocessable Entity**: Invalid query parameters. + - Page must be a non-negative integer. + - Size must be between 1 and 1000. + - Invalid sort_by or sort_order values. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of domains to retrieve per page. Default: 100. + sort_by (DomainSortField | None | Unset): The field to sort results by. + sort_order (DomainSortOrder | None | Unset): The order to sort results (ascending or + descending). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListDomainsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: DomainSortField | None | Unset = UNSET, + sort_order: DomainSortOrder | None | Unset = UNSET, +) -> HTTPValidationError | ListDomainsResponse | None: + r"""List all domains + + # List Domains Endpoint + + This endpoint retrieves a paginated list of all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) The page number to retrieve. Zero-indexed, + meaning the first page is `0`. Default: 0. + - **size**: (integer, optional) The number of domains to retrieve per page. + Must be between 1 and 1000. Default: 100. + - **sort_by**: (string, optional) The field to sort results by. Valid values: + - `created_on`: Sort by creation date. + - `modified_on`: Sort by last modification date. + - `name`: Sort alphabetically by name. + - **sort_order**: (string, optional) The order to sort results. Valid values: + - `ascending`: Sort in ascending order (A-Z, oldest first). + - `descending`: Sort in descending order (Z-A, newest first). + Default: descending when sort_by is specified. + + ## Response + + Returns a paginated list of domains with metadata: + + - **domains**: (array) List of domain resources for the current page. + Each domain includes: + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system. + - **features**: (array) The domain geometry features. + - **current_page**: (integer) The current page number (zero-indexed). + - **page_size**: (integer) The number of domains per page. + - **total_items**: (integer) The total number of domains owned by the user. + + ## Pagination + + Use `page` and `size` parameters to navigate through large result sets: + + - First page: `?page=0&size=10` + - Second page: `?page=1&size=10` + - Calculate total pages: `ceil(total_items / page_size)` + + ## Sorting + + Combine `sort_by` and `sort_order` for custom ordering: + + - Newest first: `?sort_by=created_on&sort_order=descending` + - Alphabetical: `?sort_by=name&sort_order=ascending` + - Recently modified: `?sort_by=modified_on&sort_order=descending` + + ## Example Request + + ```http + GET /v2/domains?page=0&size=10&sort_by=created_on&sort_order=descending + ``` + + ## Example Response + + ```json + { + \"domains\": [ + { + \"id\": \"abc123...\", + \"type\": \"FeatureCollection\", + \"name\": \"My Domain\", + \"description\": \"A test domain\", + \"created_on\": \"2024-01-15T10:30:00\", + \"modified_on\": \"2024-01-15T10:30:00\", + \"tags\": [\"test\"], + \"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:32611\"}}, + \"features\": [...] + } + ], + \"current_page\": 0, + \"page_size\": 10, + \"total_items\": 42 + } + ``` + + ## Error Responses + + - **422 Unprocessable Entity**: Invalid query parameters. + - Page must be a non-negative integer. + - Size must be between 1 and 1000. + - Invalid sort_by or sort_order values. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of domains to retrieve per page. Default: 100. + sort_by (DomainSortField | None | Unset): The field to sort results by. + sort_order (DomainSortOrder | None | Unset): The order to sort results (ascending or + descending). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListDomainsResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: DomainSortField | None | Unset = UNSET, + sort_order: DomainSortOrder | None | Unset = UNSET, +) -> Response[HTTPValidationError | ListDomainsResponse]: + r"""List all domains + + # List Domains Endpoint + + This endpoint retrieves a paginated list of all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) The page number to retrieve. Zero-indexed, + meaning the first page is `0`. Default: 0. + - **size**: (integer, optional) The number of domains to retrieve per page. + Must be between 1 and 1000. Default: 100. + - **sort_by**: (string, optional) The field to sort results by. Valid values: + - `created_on`: Sort by creation date. + - `modified_on`: Sort by last modification date. + - `name`: Sort alphabetically by name. + - **sort_order**: (string, optional) The order to sort results. Valid values: + - `ascending`: Sort in ascending order (A-Z, oldest first). + - `descending`: Sort in descending order (Z-A, newest first). + Default: descending when sort_by is specified. + + ## Response + + Returns a paginated list of domains with metadata: + + - **domains**: (array) List of domain resources for the current page. + Each domain includes: + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system. + - **features**: (array) The domain geometry features. + - **current_page**: (integer) The current page number (zero-indexed). + - **page_size**: (integer) The number of domains per page. + - **total_items**: (integer) The total number of domains owned by the user. + + ## Pagination + + Use `page` and `size` parameters to navigate through large result sets: + + - First page: `?page=0&size=10` + - Second page: `?page=1&size=10` + - Calculate total pages: `ceil(total_items / page_size)` + + ## Sorting + + Combine `sort_by` and `sort_order` for custom ordering: + + - Newest first: `?sort_by=created_on&sort_order=descending` + - Alphabetical: `?sort_by=name&sort_order=ascending` + - Recently modified: `?sort_by=modified_on&sort_order=descending` + + ## Example Request + + ```http + GET /v2/domains?page=0&size=10&sort_by=created_on&sort_order=descending + ``` + + ## Example Response + + ```json + { + \"domains\": [ + { + \"id\": \"abc123...\", + \"type\": \"FeatureCollection\", + \"name\": \"My Domain\", + \"description\": \"A test domain\", + \"created_on\": \"2024-01-15T10:30:00\", + \"modified_on\": \"2024-01-15T10:30:00\", + \"tags\": [\"test\"], + \"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:32611\"}}, + \"features\": [...] + } + ], + \"current_page\": 0, + \"page_size\": 10, + \"total_items\": 42 + } + ``` + + ## Error Responses + + - **422 Unprocessable Entity**: Invalid query parameters. + - Page must be a non-negative integer. + - Size must be between 1 and 1000. + - Invalid sort_by or sort_order values. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of domains to retrieve per page. Default: 100. + sort_by (DomainSortField | None | Unset): The field to sort results by. + sort_order (DomainSortOrder | None | Unset): The order to sort results (ascending or + descending). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListDomainsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: DomainSortField | None | Unset = UNSET, + sort_order: DomainSortOrder | None | Unset = UNSET, +) -> HTTPValidationError | ListDomainsResponse | None: + r"""List all domains + + # List Domains Endpoint + + This endpoint retrieves a paginated list of all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) The page number to retrieve. Zero-indexed, + meaning the first page is `0`. Default: 0. + - **size**: (integer, optional) The number of domains to retrieve per page. + Must be between 1 and 1000. Default: 100. + - **sort_by**: (string, optional) The field to sort results by. Valid values: + - `created_on`: Sort by creation date. + - `modified_on`: Sort by last modification date. + - `name`: Sort alphabetically by name. + - **sort_order**: (string, optional) The order to sort results. Valid values: + - `ascending`: Sort in ascending order (A-Z, oldest first). + - `descending`: Sort in descending order (Z-A, newest first). + Default: descending when sort_by is specified. + + ## Response + + Returns a paginated list of domains with metadata: + + - **domains**: (array) List of domain resources for the current page. + Each domain includes: + - **id**: (string) The unique identifier for the domain. + - **type**: (string) Always \"FeatureCollection\". + - **name**: (string) The name of the domain. + - **description**: (string) The description of the domain. + - **created_on**: (datetime) When the domain was created. + - **modified_on**: (datetime) When the domain was last modified. + - **tags**: (array) The tags associated with the domain. + - **crs**: (object) The coordinate reference system. + - **features**: (array) The domain geometry features. + - **current_page**: (integer) The current page number (zero-indexed). + - **page_size**: (integer) The number of domains per page. + - **total_items**: (integer) The total number of domains owned by the user. + + ## Pagination + + Use `page` and `size` parameters to navigate through large result sets: + + - First page: `?page=0&size=10` + - Second page: `?page=1&size=10` + - Calculate total pages: `ceil(total_items / page_size)` + + ## Sorting + + Combine `sort_by` and `sort_order` for custom ordering: + + - Newest first: `?sort_by=created_on&sort_order=descending` + - Alphabetical: `?sort_by=name&sort_order=ascending` + - Recently modified: `?sort_by=modified_on&sort_order=descending` + + ## Example Request + + ```http + GET /v2/domains?page=0&size=10&sort_by=created_on&sort_order=descending + ``` + + ## Example Response + + ```json + { + \"domains\": [ + { + \"id\": \"abc123...\", + \"type\": \"FeatureCollection\", + \"name\": \"My Domain\", + \"description\": \"A test domain\", + \"created_on\": \"2024-01-15T10:30:00\", + \"modified_on\": \"2024-01-15T10:30:00\", + \"tags\": [\"test\"], + \"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:32611\"}}, + \"features\": [...] + } + ], + \"current_page\": 0, + \"page_size\": 10, + \"total_items\": 42 + } + ``` + + ## Error Responses + + - **422 Unprocessable Entity**: Invalid query parameters. + - Page must be a non-negative integer. + - Size must be between 1 and 1000. + - Invalid sort_by or sort_order values. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of domains to retrieve per page. Default: 100. + sort_by (DomainSortField | None | Unset): The field to sort results by. + sort_order (DomainSortOrder | None | Unset): The order to sort results (ascending or + descending). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListDomainsResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/preview_domain.py b/fastfuels_sdk/v2/client_library/api/domains/preview_domain.py new file mode 100644 index 0000000..c7c2e5d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/preview_domain.py @@ -0,0 +1,290 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain import Domain +from ...models.geo_json_feature_collection import GeoJsonFeatureCollection +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + *, + body: GeoJsonFeatureCollection, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/preview", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Domain | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Domain.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Domain | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Response[Domain | HTTPValidationError]: + r"""Preview a domain without persisting it + + # Preview Domain Endpoint + + Runs the same validation and projection pipeline as `POST /v2/domains` but + returns the resulting `Domain` resource without writing to Firestore. Use + this to let users inspect the projected, padded bounding box before committing + to a create. + + ## Request Body + + Identical to `POST /v2/domains`. See that endpoint for full documentation. + + ## Response + + Returns the same `Domain` response model as create, with: + + - **id**: Always `\"preview\"` — not a real domain identifier. + - **created_on** / **modified_on**: Set to the current request time (not persisted). + - **features**: A single `\"domain\"` feature (the working extent), + identical to what create would return. + - **bbox**: Bounding box of the `\"domain\"` feature. + - **crs**: Projected CRS, identical to what create would return. + + ## Error Responses + + Same 422 error responses as `POST /v2/domains`: + + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Domain | HTTPValidationError | None: + r"""Preview a domain without persisting it + + # Preview Domain Endpoint + + Runs the same validation and projection pipeline as `POST /v2/domains` but + returns the resulting `Domain` resource without writing to Firestore. Use + this to let users inspect the projected, padded bounding box before committing + to a create. + + ## Request Body + + Identical to `POST /v2/domains`. See that endpoint for full documentation. + + ## Response + + Returns the same `Domain` response model as create, with: + + - **id**: Always `\"preview\"` — not a real domain identifier. + - **created_on** / **modified_on**: Set to the current request time (not persisted). + - **features**: A single `\"domain\"` feature (the working extent), + identical to what create would return. + - **bbox**: Bounding box of the `\"domain\"` feature. + - **crs**: Projected CRS, identical to what create would return. + + ## Error Responses + + Same 422 error responses as `POST /v2/domains`: + + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Response[Domain | HTTPValidationError]: + r"""Preview a domain without persisting it + + # Preview Domain Endpoint + + Runs the same validation and projection pipeline as `POST /v2/domains` but + returns the resulting `Domain` resource without writing to Firestore. Use + this to let users inspect the projected, padded bounding box before committing + to a create. + + ## Request Body + + Identical to `POST /v2/domains`. See that endpoint for full documentation. + + ## Response + + Returns the same `Domain` response model as create, with: + + - **id**: Always `\"preview\"` — not a real domain identifier. + - **created_on** / **modified_on**: Set to the current request time (not persisted). + - **features**: A single `\"domain\"` feature (the working extent), + identical to what create would return. + - **bbox**: Bounding box of the `\"domain\"` feature. + - **crs**: Projected CRS, identical to what create would return. + + ## Error Responses + + Same 422 error responses as `POST /v2/domains`: + + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, +) -> Domain | HTTPValidationError | None: + r"""Preview a domain without persisting it + + # Preview Domain Endpoint + + Runs the same validation and projection pipeline as `POST /v2/domains` but + returns the resulting `Domain` resource without writing to Firestore. Use + this to let users inspect the projected, padded bounding box before committing + to a create. + + ## Request Body + + Identical to `POST /v2/domains`. See that endpoint for full documentation. + + ## Response + + Returns the same `Domain` response model as create, with: + + - **id**: Always `\"preview\"` — not a real domain identifier. + - **created_on** / **modified_on**: Set to the current request time (not persisted). + - **features**: A single `\"domain\"` feature (the working extent), + identical to what create would return. + - **bbox**: Bounding box of the `\"domain\"` feature. + - **crs**: Projected CRS, identical to what create would return. + + ## Error Responses + + Same 422 error responses as `POST /v2/domains`: + + - \"Invalid CRS '{crs}'. Must be a valid authority string (e.g., 'EPSG:4326').\" + - \"Invalid geometry. The feature must have an area greater than zero.\" + - \"Invalid spatial extent. Area must be less than 16 square kilometers.\" + - \"Invalid spatial extent. The domain must be entirely within CONUS.\" + + Args: + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/reproject_domain.py b/fastfuels_sdk/v2/client_library/api/domains/reproject_domain.py new file mode 100644 index 0000000..c79ae6d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/reproject_domain.py @@ -0,0 +1,301 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.geo_json_feature_collection import GeoJsonFeatureCollection +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response + + +def _get_kwargs( + *, + body: GeoJsonFeatureCollection, + target_epsg: int, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["target_epsg"] = target_epsg + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/reproject", + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> GeoJsonFeatureCollection | HTTPValidationError | None: + if response.status_code == 200: + response_200 = GeoJsonFeatureCollection.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[GeoJsonFeatureCollection | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, + target_epsg: int, +) -> Response[GeoJsonFeatureCollection | HTTPValidationError]: + """Reproject a FeatureCollection to a target CRS + + # Reproject Domain Endpoint + + Stateless utility that reprojects a GeoJSON `FeatureCollection` from one + coordinate reference system to another. No resource is created; the + reprojected `FeatureCollection` is returned immediately. + + ## Query Parameters + + - **target_epsg**: (integer, required) EPSG code of the target CRS + (e.g., `4326` for WGS84, `32611` for UTM zone 11N). + + ## Request Body + + A GeoJSON `FeatureCollection`. The source CRS is read from the + `crs.properties.name` field if present; otherwise EPSG:4326 is assumed. + + ## Response + + Returns the reprojected `FeatureCollection` with: + + - **features**: All input features reprojected to the target CRS, with + original feature properties preserved. + - **crs**: Set to the target EPSG code. + + ## Error Responses + + - **422**: Invalid source CRS, invalid target EPSG, or geometry that + cannot be reprojected. + + Args: + target_epsg (int): EPSG code of the target CRS. + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GeoJsonFeatureCollection | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + target_epsg=target_epsg, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, + target_epsg: int, +) -> GeoJsonFeatureCollection | HTTPValidationError | None: + """Reproject a FeatureCollection to a target CRS + + # Reproject Domain Endpoint + + Stateless utility that reprojects a GeoJSON `FeatureCollection` from one + coordinate reference system to another. No resource is created; the + reprojected `FeatureCollection` is returned immediately. + + ## Query Parameters + + - **target_epsg**: (integer, required) EPSG code of the target CRS + (e.g., `4326` for WGS84, `32611` for UTM zone 11N). + + ## Request Body + + A GeoJSON `FeatureCollection`. The source CRS is read from the + `crs.properties.name` field if present; otherwise EPSG:4326 is assumed. + + ## Response + + Returns the reprojected `FeatureCollection` with: + + - **features**: All input features reprojected to the target CRS, with + original feature properties preserved. + - **crs**: Set to the target EPSG code. + + ## Error Responses + + - **422**: Invalid source CRS, invalid target EPSG, or geometry that + cannot be reprojected. + + Args: + target_epsg (int): EPSG code of the target CRS. + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GeoJsonFeatureCollection | HTTPValidationError + """ + + return sync_detailed( + client=client, + body=body, + target_epsg=target_epsg, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, + target_epsg: int, +) -> Response[GeoJsonFeatureCollection | HTTPValidationError]: + """Reproject a FeatureCollection to a target CRS + + # Reproject Domain Endpoint + + Stateless utility that reprojects a GeoJSON `FeatureCollection` from one + coordinate reference system to another. No resource is created; the + reprojected `FeatureCollection` is returned immediately. + + ## Query Parameters + + - **target_epsg**: (integer, required) EPSG code of the target CRS + (e.g., `4326` for WGS84, `32611` for UTM zone 11N). + + ## Request Body + + A GeoJSON `FeatureCollection`. The source CRS is read from the + `crs.properties.name` field if present; otherwise EPSG:4326 is assumed. + + ## Response + + Returns the reprojected `FeatureCollection` with: + + - **features**: All input features reprojected to the target CRS, with + original feature properties preserved. + - **crs**: Set to the target EPSG code. + + ## Error Responses + + - **422**: Invalid source CRS, invalid target EPSG, or geometry that + cannot be reprojected. + + Args: + target_epsg (int): EPSG code of the target CRS. + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GeoJsonFeatureCollection | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + target_epsg=target_epsg, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GeoJsonFeatureCollection, + target_epsg: int, +) -> GeoJsonFeatureCollection | HTTPValidationError | None: + """Reproject a FeatureCollection to a target CRS + + # Reproject Domain Endpoint + + Stateless utility that reprojects a GeoJSON `FeatureCollection` from one + coordinate reference system to another. No resource is created; the + reprojected `FeatureCollection` is returned immediately. + + ## Query Parameters + + - **target_epsg**: (integer, required) EPSG code of the target CRS + (e.g., `4326` for WGS84, `32611` for UTM zone 11N). + + ## Request Body + + A GeoJSON `FeatureCollection`. The source CRS is read from the + `crs.properties.name` field if present; otherwise EPSG:4326 is assumed. + + ## Response + + Returns the reprojected `FeatureCollection` with: + + - **features**: All input features reprojected to the target CRS, with + original feature properties preserved. + - **crs**: Set to the target EPSG code. + + ## Error Responses + + - **422**: Invalid source CRS, invalid target EPSG, or geometry that + cannot be reprojected. + + Args: + target_epsg (int): EPSG code of the target CRS. + body (GeoJsonFeatureCollection): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GeoJsonFeatureCollection | HTTPValidationError + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + target_epsg=target_epsg, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/domains/update_domain.py b/fastfuels_sdk/v2/client_library/api/domains/update_domain.py new file mode 100644 index 0000000..294c2e4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/domains/update_domain.py @@ -0,0 +1,402 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.domain import Domain +from ...models.http_validation_error import HTTPValidationError +from ...models.update_domain_request_body import UpdateDomainRequestBody +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: UpdateDomainRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/domains/{domain_id}".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Domain | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Domain.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Domain | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: UpdateDomainRequestBody, +) -> Response[Domain | HTTPValidationError]: + r"""Update a domain + + # Update Domain Endpoint + + This endpoint updates the metadata of an existing domain resource. Only the + fields provided in the request body will be modified; other fields remain + unchanged. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Request Body + + All fields are optional. Only provided fields will be updated. + + - **name**: (string, optional) The new name for the domain. + - **description**: (string, optional) The new description for the domain. + - **tags**: (array of strings, optional) The new tags for the domain. + This replaces the existing tags array entirely. + + ## What Cannot Be Updated + + The following fields are immutable after domain creation: + + - **id**: The domain identifier is permanent. + - **features**: Geometry cannot be modified. Create a new domain instead. + - **crs**: Coordinate reference system is tied to the geometry. + - **created_on**: Creation timestamp is permanent. + + The **modified_on** field is automatically updated to the current time. + + ## Response + + On success, returns the updated domain resource with all fields, + including the new `modified_on` timestamp. + + ## Example Request + + ```http + PATCH /v2/domains/abc123def456... + Content-Type: application/json + + { + \"name\": \"Updated Domain Name\", + \"tags\": [\"production\", \"verified\"] + } + ``` + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **422 Unprocessable Entity**: Invalid request body. + + Args: + domain_id (str): + body (UpdateDomainRequestBody): Request body for updating a domain's metadata. + + All fields are optional. Only provided fields will be updated. + Geometry (features) and CRS cannot be modified after creation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: UpdateDomainRequestBody, +) -> Domain | HTTPValidationError | None: + r"""Update a domain + + # Update Domain Endpoint + + This endpoint updates the metadata of an existing domain resource. Only the + fields provided in the request body will be modified; other fields remain + unchanged. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Request Body + + All fields are optional. Only provided fields will be updated. + + - **name**: (string, optional) The new name for the domain. + - **description**: (string, optional) The new description for the domain. + - **tags**: (array of strings, optional) The new tags for the domain. + This replaces the existing tags array entirely. + + ## What Cannot Be Updated + + The following fields are immutable after domain creation: + + - **id**: The domain identifier is permanent. + - **features**: Geometry cannot be modified. Create a new domain instead. + - **crs**: Coordinate reference system is tied to the geometry. + - **created_on**: Creation timestamp is permanent. + + The **modified_on** field is automatically updated to the current time. + + ## Response + + On success, returns the updated domain resource with all fields, + including the new `modified_on` timestamp. + + ## Example Request + + ```http + PATCH /v2/domains/abc123def456... + Content-Type: application/json + + { + \"name\": \"Updated Domain Name\", + \"tags\": [\"production\", \"verified\"] + } + ``` + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **422 Unprocessable Entity**: Invalid request body. + + Args: + domain_id (str): + body (UpdateDomainRequestBody): Request body for updating a domain's metadata. + + All fields are optional. Only provided fields will be updated. + Geometry (features) and CRS cannot be modified after creation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: UpdateDomainRequestBody, +) -> Response[Domain | HTTPValidationError]: + r"""Update a domain + + # Update Domain Endpoint + + This endpoint updates the metadata of an existing domain resource. Only the + fields provided in the request body will be modified; other fields remain + unchanged. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Request Body + + All fields are optional. Only provided fields will be updated. + + - **name**: (string, optional) The new name for the domain. + - **description**: (string, optional) The new description for the domain. + - **tags**: (array of strings, optional) The new tags for the domain. + This replaces the existing tags array entirely. + + ## What Cannot Be Updated + + The following fields are immutable after domain creation: + + - **id**: The domain identifier is permanent. + - **features**: Geometry cannot be modified. Create a new domain instead. + - **crs**: Coordinate reference system is tied to the geometry. + - **created_on**: Creation timestamp is permanent. + + The **modified_on** field is automatically updated to the current time. + + ## Response + + On success, returns the updated domain resource with all fields, + including the new `modified_on` timestamp. + + ## Example Request + + ```http + PATCH /v2/domains/abc123def456... + Content-Type: application/json + + { + \"name\": \"Updated Domain Name\", + \"tags\": [\"production\", \"verified\"] + } + ``` + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **422 Unprocessable Entity**: Invalid request body. + + Args: + domain_id (str): + body (UpdateDomainRequestBody): Request body for updating a domain's metadata. + + All fields are optional. Only provided fields will be updated. + Geometry (features) and CRS cannot be modified after creation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Domain | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: UpdateDomainRequestBody, +) -> Domain | HTTPValidationError | None: + r"""Update a domain + + # Update Domain Endpoint + + This endpoint updates the metadata of an existing domain resource. Only the + fields provided in the request body will be modified; other fields remain + unchanged. + + ## Path Parameters + + - **domain_id**: (string) The unique 32-character hex identifier of the domain. + + ## Request Body + + All fields are optional. Only provided fields will be updated. + + - **name**: (string, optional) The new name for the domain. + - **description**: (string, optional) The new description for the domain. + - **tags**: (array of strings, optional) The new tags for the domain. + This replaces the existing tags array entirely. + + ## What Cannot Be Updated + + The following fields are immutable after domain creation: + + - **id**: The domain identifier is permanent. + - **features**: Geometry cannot be modified. Create a new domain instead. + - **crs**: Coordinate reference system is tied to the geometry. + - **created_on**: Creation timestamp is permanent. + + The **modified_on** field is automatically updated to the current time. + + ## Response + + On success, returns the updated domain resource with all fields, + including the new `modified_on` timestamp. + + ## Example Request + + ```http + PATCH /v2/domains/abc123def456... + Content-Type: application/json + + { + \"name\": \"Updated Domain Name\", + \"tags\": [\"production\", \"verified\"] + } + ``` + + ## Error Responses + + - **404 Not Found**: The domain does not exist or the user does not have access. + - **422 Unprocessable Entity**: Invalid request body. + + Args: + domain_id (str): + body (UpdateDomainRequestBody): Request body for updating a domain's metadata. + + All fields are optional. Only provided fields will be updated. + Geometry (features) and CRS cannot be modified after creation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Domain | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/exports/__init__.py b/fastfuels_sdk/v2/client_library/api/exports/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/exports/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/exports/delete_export.py b/fastfuels_sdk/v2/client_library/api/exports/delete_export.py new file mode 100644 index 0000000..e44d99d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/exports/delete_export.py @@ -0,0 +1,227 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + export_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/exports/{export_id}".format( + export_id=quote(str(export_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + export_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an export + + # Delete Export Endpoint + + Permanently deletes an export resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + export_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an export + + # Delete Export Endpoint + + Permanently deletes an export resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + export_id=export_id, + client=client, + ).parsed + + +async def asyncio_detailed( + export_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an export + + # Delete Export Endpoint + + Permanently deletes an export resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + export_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an export + + # Delete Export Endpoint + + Permanently deletes an export resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + export_id=export_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/exports/get_export.py b/fastfuels_sdk/v2/client_library/api/exports/get_export.py new file mode 100644 index 0000000..c670379 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/exports/get_export.py @@ -0,0 +1,229 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + export_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/exports/{export_id}".format( + export_id=quote(str(export_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Export.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + export_id: str, + *, + client: AuthenticatedClient, +) -> Response[Export | HTTPValidationError]: + """Get an export by ID + + # Get Export Endpoint + + Retrieves a specific export resource by its unique identifier. + When the export is completed, the response includes a signed_url. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns the export resource. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + export_id: str, + *, + client: AuthenticatedClient, +) -> Export | HTTPValidationError | None: + """Get an export by ID + + # Get Export Endpoint + + Retrieves a specific export resource by its unique identifier. + When the export is completed, the response includes a signed_url. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns the export resource. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError + """ + + return sync_detailed( + export_id=export_id, + client=client, + ).parsed + + +async def asyncio_detailed( + export_id: str, + *, + client: AuthenticatedClient, +) -> Response[Export | HTTPValidationError]: + """Get an export by ID + + # Get Export Endpoint + + Retrieves a specific export resource by its unique identifier. + When the export is completed, the response includes a signed_url. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns the export resource. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + export_id: str, + *, + client: AuthenticatedClient, +) -> Export | HTTPValidationError | None: + """Get an export by ID + + # Get Export Endpoint + + Retrieves a specific export resource by its unique identifier. + When the export is completed, the response includes a signed_url. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Response + + Returns the export resource. + + ## Error Responses + + - **404 Not Found**: The export does not exist or the user does not have access. + + Args: + export_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError + """ + + return ( + await asyncio_detailed( + export_id=export_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/exports/list_exports.py b/fastfuels_sdk/v2/client_library/api/exports/list_exports.py new file mode 100644 index 0000000..b8c63ce --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/exports/list_exports.py @@ -0,0 +1,363 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export_sort_field import ExportSortField +from ...models.http_validation_error import HTTPValidationError +from ...models.list_exports_response import ListExportsResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: ExportSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + domain_id: None | str | Unset = UNSET, + source_name: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, ExportSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_domain_id: None | str | Unset + if isinstance(domain_id, Unset): + json_domain_id = UNSET + else: + json_domain_id = domain_id + params["domain_id"] = json_domain_id + + json_source_name: None | str | Unset + if isinstance(source_name, Unset): + json_source_name = UNSET + else: + json_source_name = source_name + params["source_name"] = json_source_name + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/exports", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListExportsResponse | None: + if response.status_code == 200: + response_200 = ListExportsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListExportsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: ExportSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + domain_id: None | str | Unset = UNSET, + source_name: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListExportsResponse]: + """List all exports + + # List Exports Endpoint + + Retrieves a paginated list of all exports belonging to the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **domain_id**: (string, optional) Filter by source domain. + - **source_name**: (string, optional) Filter by export format (e.g., `geotiff`). + - **tag**: (string, optional) Filter exports that contain this tag. + + ## Response + + Returns a paginated list of exports with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of exports to retrieve per page. Default: 100. + sort_by (ExportSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + domain_id (None | str | Unset): Filter exports by domain ID. + source_name (None | str | Unset): Filter exports by source format (e.g., 'geotiff'). + tag (None | str | Unset): Filter exports that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListExportsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + domain_id=domain_id, + source_name=source_name, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: ExportSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + domain_id: None | str | Unset = UNSET, + source_name: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListExportsResponse | None: + """List all exports + + # List Exports Endpoint + + Retrieves a paginated list of all exports belonging to the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **domain_id**: (string, optional) Filter by source domain. + - **source_name**: (string, optional) Filter by export format (e.g., `geotiff`). + - **tag**: (string, optional) Filter exports that contain this tag. + + ## Response + + Returns a paginated list of exports with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of exports to retrieve per page. Default: 100. + sort_by (ExportSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + domain_id (None | str | Unset): Filter exports by domain ID. + source_name (None | str | Unset): Filter exports by source format (e.g., 'geotiff'). + tag (None | str | Unset): Filter exports that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListExportsResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + domain_id=domain_id, + source_name=source_name, + tag=tag, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: ExportSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + domain_id: None | str | Unset = UNSET, + source_name: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListExportsResponse]: + """List all exports + + # List Exports Endpoint + + Retrieves a paginated list of all exports belonging to the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **domain_id**: (string, optional) Filter by source domain. + - **source_name**: (string, optional) Filter by export format (e.g., `geotiff`). + - **tag**: (string, optional) Filter exports that contain this tag. + + ## Response + + Returns a paginated list of exports with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of exports to retrieve per page. Default: 100. + sort_by (ExportSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + domain_id (None | str | Unset): Filter exports by domain ID. + source_name (None | str | Unset): Filter exports by source format (e.g., 'geotiff'). + tag (None | str | Unset): Filter exports that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListExportsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + domain_id=domain_id, + source_name=source_name, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: ExportSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + domain_id: None | str | Unset = UNSET, + source_name: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListExportsResponse | None: + """List all exports + + # List Exports Endpoint + + Retrieves a paginated list of all exports belonging to the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **domain_id**: (string, optional) Filter by source domain. + - **source_name**: (string, optional) Filter by export format (e.g., `geotiff`). + - **tag**: (string, optional) Filter exports that contain this tag. + + ## Response + + Returns a paginated list of exports with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of exports to retrieve per page. Default: 100. + sort_by (ExportSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + domain_id (None | str | Unset): Filter exports by domain ID. + source_name (None | str | Unset): Filter exports by source format (e.g., 'geotiff'). + tag (None | str | Unset): Filter exports that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListExportsResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + domain_id=domain_id, + source_name=source_name, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/exports/update_export.py b/fastfuels_sdk/v2/client_library/api/exports/update_export.py new file mode 100644 index 0000000..8a9869d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/exports/update_export.py @@ -0,0 +1,266 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.http_validation_error import HTTPValidationError +from ...models.update_export_request_body import UpdateExportRequestBody +from ...types import Response + + +def _get_kwargs( + export_id: str, + *, + body: UpdateExportRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/exports/{export_id}".format( + export_id=quote(str(export_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Export.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + export_id: str, + *, + client: AuthenticatedClient, + body: UpdateExportRequestBody, +) -> Response[Export | HTTPValidationError]: + """Update an export + + # Update Export Endpoint + + Updates the metadata of an existing export resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the export. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## Response + + Returns the updated export resource. + + Args: + export_id (str): + body (UpdateExportRequestBody): Request body for updating export metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + export_id: str, + *, + client: AuthenticatedClient, + body: UpdateExportRequestBody, +) -> Export | HTTPValidationError | None: + """Update an export + + # Update Export Endpoint + + Updates the metadata of an existing export resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the export. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## Response + + Returns the updated export resource. + + Args: + export_id (str): + body (UpdateExportRequestBody): Request body for updating export metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError + """ + + return sync_detailed( + export_id=export_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + export_id: str, + *, + client: AuthenticatedClient, + body: UpdateExportRequestBody, +) -> Response[Export | HTTPValidationError]: + """Update an export + + # Update Export Endpoint + + Updates the metadata of an existing export resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the export. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## Response + + Returns the updated export resource. + + Args: + export_id (str): + body (UpdateExportRequestBody): Request body for updating export metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError] + """ + + kwargs = _get_kwargs( + export_id=export_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + export_id: str, + *, + client: AuthenticatedClient, + body: UpdateExportRequestBody, +) -> Export | HTTPValidationError | None: + """Update an export + + # Update Export Endpoint + + Updates the metadata of an existing export resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **export_id**: (string) The unique identifier of the export. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the export. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## Response + + Returns the updated export resource. + + Args: + export_id (str): + body (UpdateExportRequestBody): Request body for updating export metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError + """ + + return ( + await asyncio_detailed( + export_id=export_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/__init__.py b/fastfuels_sdk/v2/client_library/api/features/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/features/create_layerset.py b/fastfuels_sdk/v2/client_library/api/features/create_layerset.py new file mode 100644 index 0000000..8ebe7ed --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/create_layerset.py @@ -0,0 +1,340 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_layerset_request_body import CreateLayersetRequestBody +from ...models.feature import Feature +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLayersetRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/features/layerset/geojson".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Feature.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRequestBody, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Upload a custom Layerset + + # Create Layerset Endpoint + + Uploads a flat GeoJSON FeatureCollection of fuelbed polygons as a new + feature resource. Each Feature's ``properties`` block carries one + fuelbed's input columns for ``fastfuels_core.rasterize_layerset``. The + payload is validated and saved directly to Cloud Storage. + + ## Path Parameters + - **domain_id**: (string) The domain this layerset belongs to. + + ## Request Body + + The body **is** the GeoJSON FeatureCollection (mirroring `POST /domains`), + with optional resource metadata as top-level fields: + + - **type**: (string) Must be `\"FeatureCollection\"`. + - **features**: (array) At least one Feature, each carrying one fuelbed's + `properties` and a `Polygon`/`MultiPolygon` `geometry`. + - **crs**: (object) The GeoJSON `crs` block declaring a **projected** CRS + (e.g. `EPSG:32612`). Geographic CRSes are rejected — rasterization + requires cell sizes in meters. + - **name**: (string, optional) Name of the layerset. + - **description**: (string, optional) Description of the data. + - **tags**: (array of strings, optional) Searchable tags. + + ## Response + Returns the created Feature resource with a status of `completed`. + + Args: + domain_id (str): + body (CreateLayersetRequestBody): Request body for uploading a flat GeoJSON layerset. + + The body **is** the GeoJSON FeatureCollection (matching ``POST /domains``, + whose body is a ``FeatureCollection`` directly), extended with the + resource-metadata fields. No ``type`` discriminator: the URL + ``/features/layerset/geojson`` already discriminates layersets from + road/water uploads. + + ``name`` overrides the optional GeoJSON ``name`` member inherited from + ``LayersetFeatureCollection`` — the FeatureCollection's name doubles as the + resource name, exactly as ``CreateDomainRequestBody`` treats it. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRequestBody, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Upload a custom Layerset + + # Create Layerset Endpoint + + Uploads a flat GeoJSON FeatureCollection of fuelbed polygons as a new + feature resource. Each Feature's ``properties`` block carries one + fuelbed's input columns for ``fastfuels_core.rasterize_layerset``. The + payload is validated and saved directly to Cloud Storage. + + ## Path Parameters + - **domain_id**: (string) The domain this layerset belongs to. + + ## Request Body + + The body **is** the GeoJSON FeatureCollection (mirroring `POST /domains`), + with optional resource metadata as top-level fields: + + - **type**: (string) Must be `\"FeatureCollection\"`. + - **features**: (array) At least one Feature, each carrying one fuelbed's + `properties` and a `Polygon`/`MultiPolygon` `geometry`. + - **crs**: (object) The GeoJSON `crs` block declaring a **projected** CRS + (e.g. `EPSG:32612`). Geographic CRSes are rejected — rasterization + requires cell sizes in meters. + - **name**: (string, optional) Name of the layerset. + - **description**: (string, optional) Description of the data. + - **tags**: (array of strings, optional) Searchable tags. + + ## Response + Returns the created Feature resource with a status of `completed`. + + Args: + domain_id (str): + body (CreateLayersetRequestBody): Request body for uploading a flat GeoJSON layerset. + + The body **is** the GeoJSON FeatureCollection (matching ``POST /domains``, + whose body is a ``FeatureCollection`` directly), extended with the + resource-metadata fields. No ``type`` discriminator: the URL + ``/features/layerset/geojson`` already discriminates layersets from + road/water uploads. + + ``name`` overrides the optional GeoJSON ``name`` member inherited from + ``LayersetFeatureCollection`` — the FeatureCollection's name doubles as the + resource name, exactly as ``CreateDomainRequestBody`` treats it. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRequestBody, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Upload a custom Layerset + + # Create Layerset Endpoint + + Uploads a flat GeoJSON FeatureCollection of fuelbed polygons as a new + feature resource. Each Feature's ``properties`` block carries one + fuelbed's input columns for ``fastfuels_core.rasterize_layerset``. The + payload is validated and saved directly to Cloud Storage. + + ## Path Parameters + - **domain_id**: (string) The domain this layerset belongs to. + + ## Request Body + + The body **is** the GeoJSON FeatureCollection (mirroring `POST /domains`), + with optional resource metadata as top-level fields: + + - **type**: (string) Must be `\"FeatureCollection\"`. + - **features**: (array) At least one Feature, each carrying one fuelbed's + `properties` and a `Polygon`/`MultiPolygon` `geometry`. + - **crs**: (object) The GeoJSON `crs` block declaring a **projected** CRS + (e.g. `EPSG:32612`). Geographic CRSes are rejected — rasterization + requires cell sizes in meters. + - **name**: (string, optional) Name of the layerset. + - **description**: (string, optional) Description of the data. + - **tags**: (array of strings, optional) Searchable tags. + + ## Response + Returns the created Feature resource with a status of `completed`. + + Args: + domain_id (str): + body (CreateLayersetRequestBody): Request body for uploading a flat GeoJSON layerset. + + The body **is** the GeoJSON FeatureCollection (matching ``POST /domains``, + whose body is a ``FeatureCollection`` directly), extended with the + resource-metadata fields. No ``type`` discriminator: the URL + ``/features/layerset/geojson`` already discriminates layersets from + road/water uploads. + + ``name`` overrides the optional GeoJSON ``name`` member inherited from + ``LayersetFeatureCollection`` — the FeatureCollection's name doubles as the + resource name, exactly as ``CreateDomainRequestBody`` treats it. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRequestBody, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Upload a custom Layerset + + # Create Layerset Endpoint + + Uploads a flat GeoJSON FeatureCollection of fuelbed polygons as a new + feature resource. Each Feature's ``properties`` block carries one + fuelbed's input columns for ``fastfuels_core.rasterize_layerset``. The + payload is validated and saved directly to Cloud Storage. + + ## Path Parameters + - **domain_id**: (string) The domain this layerset belongs to. + + ## Request Body + + The body **is** the GeoJSON FeatureCollection (mirroring `POST /domains`), + with optional resource metadata as top-level fields: + + - **type**: (string) Must be `\"FeatureCollection\"`. + - **features**: (array) At least one Feature, each carrying one fuelbed's + `properties` and a `Polygon`/`MultiPolygon` `geometry`. + - **crs**: (object) The GeoJSON `crs` block declaring a **projected** CRS + (e.g. `EPSG:32612`). Geographic CRSes are rejected — rasterization + requires cell sizes in meters. + - **name**: (string, optional) Name of the layerset. + - **description**: (string, optional) Description of the data. + - **tags**: (array of strings, optional) Searchable tags. + + ## Response + Returns the created Feature resource with a status of `completed`. + + Args: + domain_id (str): + body (CreateLayersetRequestBody): Request body for uploading a flat GeoJSON layerset. + + The body **is** the GeoJSON FeatureCollection (matching ``POST /domains``, + whose body is a ``FeatureCollection`` directly), extended with the + resource-metadata fields. No ``type`` discriminator: the URL + ``/features/layerset/geojson`` already discriminates layersets from + road/water uploads. + + ``name`` overrides the optional GeoJSON ``name`` member inherited from + ``LayersetFeatureCollection`` — the FeatureCollection's name doubles as the + resource name, exactly as ``CreateDomainRequestBody`` treats it. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/create_osm_road_feature.py b/fastfuels_sdk/v2/client_library/api/features/create_osm_road_feature.py new file mode 100644 index 0000000..0754dfc --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/create_osm_road_feature.py @@ -0,0 +1,316 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_osm_road_feature_request import CreateOsmRoadFeatureRequest +from ...models.feature import Feature +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateOsmRoadFeatureRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/features/road/osm".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Feature.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmRoadFeatureRequest, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Create a road feature from OpenStreetMap + + # Create OSM Road Feature + + Generates a polygon representation of the road network within the specified + domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for linear road segments (`highway=*`). + 3. Dynamically buffer the line strings into realistic polygon areas + based on their specific OSM classification (e.g., motorways receive + a wider buffer than residential streets or trails). + 4. Save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the road feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched roads. Lets roads that exit the + domain at the boundary extend slightly past the edge, providing context + for visualization and downstream operations. Applied in the domain's + projected CRS. If omitted, roads are clipped exactly to the domain + boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmRoadFeatureRequest): Request body for creating a road feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmRoadFeatureRequest, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a road feature from OpenStreetMap + + # Create OSM Road Feature + + Generates a polygon representation of the road network within the specified + domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for linear road segments (`highway=*`). + 3. Dynamically buffer the line strings into realistic polygon areas + based on their specific OSM classification (e.g., motorways receive + a wider buffer than residential streets or trails). + 4. Save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the road feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched roads. Lets roads that exit the + domain at the boundary extend slightly past the edge, providing context + for visualization and downstream operations. Applied in the domain's + projected CRS. If omitted, roads are clipped exactly to the domain + boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmRoadFeatureRequest): Request body for creating a road feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmRoadFeatureRequest, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Create a road feature from OpenStreetMap + + # Create OSM Road Feature + + Generates a polygon representation of the road network within the specified + domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for linear road segments (`highway=*`). + 3. Dynamically buffer the line strings into realistic polygon areas + based on their specific OSM classification (e.g., motorways receive + a wider buffer than residential streets or trails). + 4. Save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the road feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched roads. Lets roads that exit the + domain at the boundary extend slightly past the edge, providing context + for visualization and downstream operations. Applied in the domain's + projected CRS. If omitted, roads are clipped exactly to the domain + boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmRoadFeatureRequest): Request body for creating a road feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmRoadFeatureRequest, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a road feature from OpenStreetMap + + # Create OSM Road Feature + + Generates a polygon representation of the road network within the specified + domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for linear road segments (`highway=*`). + 3. Dynamically buffer the line strings into realistic polygon areas + based on their specific OSM classification (e.g., motorways receive + a wider buffer than residential streets or trails). + 4. Save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the road feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched roads. Lets roads that exit the + domain at the boundary extend slightly past the edge, providing context + for visualization and downstream operations. Applied in the domain's + projected CRS. If omitted, roads are clipped exactly to the domain + boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmRoadFeatureRequest): Request body for creating a road feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/create_osm_water_feature.py b/fastfuels_sdk/v2/client_library/api/features/create_osm_water_feature.py new file mode 100644 index 0000000..348431f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/create_osm_water_feature.py @@ -0,0 +1,316 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_osm_water_feature_request import CreateOsmWaterFeatureRequest +from ...models.feature import Feature +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateOsmWaterFeatureRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/features/water/osm".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Feature.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmWaterFeatureRequest, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Create a water feature from OpenStreetMap + + # Create OSM Water Feature + + Generates a polygon representation of water bodies and waterways within the + specified domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for water features (e.g., `water=*`, `waterway=*`, `natural=water`). + 3. Extract existing polygon features (lakes, ponds, wide rivers). + 4. Dynamically buffer linear water features (streams, creeks, narrow rivers) + into polygon areas based on their specific OSM classification. + 5. Merge the geometries and save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the water feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched water features. Lets streams and + rivers that exit the domain at the boundary extend slightly past the + edge, providing context for visualization and downstream operations. + Applied in the domain's projected CRS. If omitted, water features are + clipped exactly to the domain boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmWaterFeatureRequest): Request body for creating a water feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmWaterFeatureRequest, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a water feature from OpenStreetMap + + # Create OSM Water Feature + + Generates a polygon representation of water bodies and waterways within the + specified domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for water features (e.g., `water=*`, `waterway=*`, `natural=water`). + 3. Extract existing polygon features (lakes, ponds, wide rivers). + 4. Dynamically buffer linear water features (streams, creeks, narrow rivers) + into polygon areas based on their specific OSM classification. + 5. Merge the geometries and save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the water feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched water features. Lets streams and + rivers that exit the domain at the boundary extend slightly past the + edge, providing context for visualization and downstream operations. + Applied in the domain's projected CRS. If omitted, water features are + clipped exactly to the domain boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmWaterFeatureRequest): Request body for creating a water feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmWaterFeatureRequest, +) -> Response[Feature | HTTPValidationError | QuotaExceededDetail]: + r"""Create a water feature from OpenStreetMap + + # Create OSM Water Feature + + Generates a polygon representation of water bodies and waterways within the + specified domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for water features (e.g., `water=*`, `waterway=*`, `natural=water`). + 3. Extract existing polygon features (lakes, ponds, wide rivers). + 4. Dynamically buffer linear water features (streams, creeks, narrow rivers) + into polygon areas based on their specific OSM classification. + 5. Merge the geometries and save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the water feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched water features. Lets streams and + rivers that exit the domain at the boundary extend slightly past the + edge, providing context for visualization and downstream operations. + Applied in the domain's projected CRS. If omitted, water features are + clipped exactly to the domain boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmWaterFeatureRequest): Request body for creating a water feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateOsmWaterFeatureRequest, +) -> Feature | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a water feature from OpenStreetMap + + # Create OSM Water Feature + + Generates a polygon representation of water bodies and waterways within the + specified domain using data from OpenStreetMap (OSM). + + The backend worker will: + 1. Fetch the bounding box for the target domain. + 2. Query OpenStreetMap for water features (e.g., `water=*`, `waterway=*`, `natural=water`). + 3. Extract existing polygon features (lakes, ponds, wide rivers). + 4. Dynamically buffer linear water features (streams, creeks, narrow rivers) + into polygon areas based on their specific OSM classification. + 5. Merge the geometries and save the resulting GeoJSON to the features bucket. + + ## Request Body + + - **name**: (optional) Name for the water feature. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing features. + - **extent_buffer_m**: (optional) Distance in meters to expand the domain + extent outward before clipping fetched water features. Lets streams and + rivers that exit the domain at the boundary extend slightly past the + edge, providing context for visualization and downstream operations. + Applied in the domain's projected CRS. If omitted, water features are + clipped exactly to the domain boundary. Range: 0–100 meters. + + ## Response + + Returns the created Feature resource with status ``\"pending\"``. The + backend worker will process the OSM extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateOsmWaterFeatureRequest): Request body for creating a water feature via + OpenStreetMap. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/delete_feature.py b/fastfuels_sdk/v2/client_library/api/features/delete_feature.py new file mode 100644 index 0000000..ff51d83 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/delete_feature.py @@ -0,0 +1,189 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + feature_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/domains/{domain_id}/features/{feature_id}".format( + domain_id=quote(str(domain_id), safe=""), + feature_id=quote(str(feature_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a feature + + # Delete Feature Endpoint + + Permanently deletes a feature resource by its unique identifier. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a feature + + # Delete Feature Endpoint + + Permanently deletes a feature resource by its unique identifier. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a feature + + # Delete Feature Endpoint + + Permanently deletes a feature resource by its unique identifier. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a feature + + # Delete Feature Endpoint + + Permanently deletes a feature resource by its unique identifier. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/get_feature.py b/fastfuels_sdk/v2/client_library/api/features/get_feature.py new file mode 100644 index 0000000..b460e80 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/get_feature.py @@ -0,0 +1,243 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.feature import Feature +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + feature_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/features/{feature_id}".format( + domain_id=quote(str(domain_id), safe=""), + feature_id=quote(str(feature_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Feature | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Feature.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Feature | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[Feature | HTTPValidationError]: + """Get a feature by ID + + # Get Feature Endpoint + + Retrieves a specific feature resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Response + + Returns the feature resource. + + ## Error Responses + + - **404 Not Found**: The feature does not exist or the user does not have access. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Feature | HTTPValidationError | None: + """Get a feature by ID + + # Get Feature Endpoint + + Retrieves a specific feature resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Response + + Returns the feature resource. + + ## Error Responses + + - **404 Not Found**: The feature does not exist or the user does not have access. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[Feature | HTTPValidationError]: + """Get a feature by ID + + # Get Feature Endpoint + + Retrieves a specific feature resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Response + + Returns the feature resource. + + ## Error Responses + + - **404 Not Found**: The feature does not exist or the user does not have access. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Feature | HTTPValidationError | None: + """Get a feature by ID + + # Get Feature Endpoint + + Retrieves a specific feature resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Response + + Returns the feature resource. + + ## Error Responses + + - **404 Not Found**: The feature does not exist or the user does not have access. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/get_feature_data_metadata.py b/fastfuels_sdk/v2/client_library/api/features/get_feature_data_metadata.py new file mode 100644 index 0000000..e4222a8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/get_feature_data_metadata.py @@ -0,0 +1,371 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.feature_data_metadata import FeatureDataMetadata +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + feature_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/features/{feature_id}/data/metadata".format( + domain_id=quote(str(domain_id), safe=""), + feature_id=quote(str(feature_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> FeatureDataMetadata | HTTPValidationError | None: + if response.status_code == 200: + response_200 = FeatureDataMetadata.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[FeatureDataMetadata | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[FeatureDataMetadata | HTTPValidationError]: + r"""Get feature data partition layout + + # Get Feature Data Metadata + + Returns the partition layout for a completed feature's data blob. Use + this to discover how many partitions exist before streaming them via + `GET /domains/{domain_id}/features/{feature_id}/data/{partition_index}`. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + + ## Response + + JSON object describing the partition layout of the underlying + GeoParquet blob: + + ```json + { + \"total_features\": 5400, + \"partition_count\": 6, + \"partitions\": [ + {\"index\": 0, \"num_features\": 1000}, + {\"index\": 1, \"num_features\": 1000}, + {\"index\": 2, \"num_features\": 1000}, + {\"index\": 3, \"num_features\": 1000}, + {\"index\": 4, \"num_features\": 1000}, + {\"index\": 5, \"num_features\": 400} + ] + } + ``` + + - **total_features**: Total number of features across all partitions. + - **partition_count**: Number of valid `partition_index` values. Iterate + from `0` to `partition_count - 1` to retrieve every feature exactly + once, in source order. + - **partitions**: Per-partition row counts read directly from the + GeoParquet footer. Sum of `num_features` equals `total_features`. + + A feature with zero features has `partition_count = 0` and an empty + `partitions` list — no `/data/{partition_index}` calls are valid in + that case. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **422 Unprocessable Entity**: Feature is not in `completed` status, or + the underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[FeatureDataMetadata | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> FeatureDataMetadata | HTTPValidationError | None: + r"""Get feature data partition layout + + # Get Feature Data Metadata + + Returns the partition layout for a completed feature's data blob. Use + this to discover how many partitions exist before streaming them via + `GET /domains/{domain_id}/features/{feature_id}/data/{partition_index}`. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + + ## Response + + JSON object describing the partition layout of the underlying + GeoParquet blob: + + ```json + { + \"total_features\": 5400, + \"partition_count\": 6, + \"partitions\": [ + {\"index\": 0, \"num_features\": 1000}, + {\"index\": 1, \"num_features\": 1000}, + {\"index\": 2, \"num_features\": 1000}, + {\"index\": 3, \"num_features\": 1000}, + {\"index\": 4, \"num_features\": 1000}, + {\"index\": 5, \"num_features\": 400} + ] + } + ``` + + - **total_features**: Total number of features across all partitions. + - **partition_count**: Number of valid `partition_index` values. Iterate + from `0` to `partition_count - 1` to retrieve every feature exactly + once, in source order. + - **partitions**: Per-partition row counts read directly from the + GeoParquet footer. Sum of `num_features` equals `total_features`. + + A feature with zero features has `partition_count = 0` and an empty + `partitions` list — no `/data/{partition_index}` calls are valid in + that case. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **422 Unprocessable Entity**: Feature is not in `completed` status, or + the underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + FeatureDataMetadata | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> Response[FeatureDataMetadata | HTTPValidationError]: + r"""Get feature data partition layout + + # Get Feature Data Metadata + + Returns the partition layout for a completed feature's data blob. Use + this to discover how many partitions exist before streaming them via + `GET /domains/{domain_id}/features/{feature_id}/data/{partition_index}`. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + + ## Response + + JSON object describing the partition layout of the underlying + GeoParquet blob: + + ```json + { + \"total_features\": 5400, + \"partition_count\": 6, + \"partitions\": [ + {\"index\": 0, \"num_features\": 1000}, + {\"index\": 1, \"num_features\": 1000}, + {\"index\": 2, \"num_features\": 1000}, + {\"index\": 3, \"num_features\": 1000}, + {\"index\": 4, \"num_features\": 1000}, + {\"index\": 5, \"num_features\": 400} + ] + } + ``` + + - **total_features**: Total number of features across all partitions. + - **partition_count**: Number of valid `partition_index` values. Iterate + from `0` to `partition_count - 1` to retrieve every feature exactly + once, in source order. + - **partitions**: Per-partition row counts read directly from the + GeoParquet footer. Sum of `num_features` equals `total_features`. + + A feature with zero features has `partition_count = 0` and an empty + `partitions` list — no `/data/{partition_index}` calls are valid in + that case. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **422 Unprocessable Entity**: Feature is not in `completed` status, or + the underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[FeatureDataMetadata | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, +) -> FeatureDataMetadata | HTTPValidationError | None: + r"""Get feature data partition layout + + # Get Feature Data Metadata + + Returns the partition layout for a completed feature's data blob. Use + this to discover how many partitions exist before streaming them via + `GET /domains/{domain_id}/features/{feature_id}/data/{partition_index}`. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + + ## Response + + JSON object describing the partition layout of the underlying + GeoParquet blob: + + ```json + { + \"total_features\": 5400, + \"partition_count\": 6, + \"partitions\": [ + {\"index\": 0, \"num_features\": 1000}, + {\"index\": 1, \"num_features\": 1000}, + {\"index\": 2, \"num_features\": 1000}, + {\"index\": 3, \"num_features\": 1000}, + {\"index\": 4, \"num_features\": 1000}, + {\"index\": 5, \"num_features\": 400} + ] + } + ``` + + - **total_features**: Total number of features across all partitions. + - **partition_count**: Number of valid `partition_index` values. Iterate + from `0` to `partition_count - 1` to retrieve every feature exactly + once, in source order. + - **partitions**: Per-partition row counts read directly from the + GeoParquet footer. Sum of `num_features` equals `total_features`. + + A feature with zero features has `partition_count = 0` and an empty + `partitions` list — no `/data/{partition_index}` calls are valid in + that case. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **422 Unprocessable Entity**: Feature is not in `completed` status, or + the underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + FeatureDataMetadata | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/get_feature_data_partition.py b/fastfuels_sdk/v2/client_library/api/features/get_feature_data_partition.py new file mode 100644 index 0000000..c495579 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/get_feature_data_partition.py @@ -0,0 +1,331 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + feature_id: str, + partition_index: int, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/features/{feature_id}/data/{partition_index}".format( + domain_id=quote(str(domain_id), safe=""), + feature_id=quote(str(feature_id), safe=""), + partition_index=quote(str(partition_index), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 200: + response_200 = response.json() + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + feature_id: str, + partition_index: int, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Get one partition of feature data as GeoJSON + + # Get Feature Data Partition + + Returns one partition of a completed feature's data as a self-contained + GeoJSON `FeatureCollection`. To stream the full collection, first call + `GET /domains/{domain_id}/features/{feature_id}/data/metadata` to + discover `partition_count`, then GET this endpoint for each + `partition_index` from `0` to `partition_count - 1`. The concatenated + `features` arrays reproduce the source feature list in source order. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + - **partition_index**: Zero-indexed partition number. Must be `< partition_count` + from `/data/metadata`. + + ## Response + + `application/geo+json` body — a valid GeoJSON `FeatureCollection` + containing up to `partition_size` features. Each feature's `properties` + and `geometry` round-trip from the source GeoParquet via geopandas. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **413 Content Too Large**: Serialized partition exceeds the 30 MB + response cap. Partition size is fixed server-side and cannot be + adjusted by re-creating the feature; contact + `support.fastfuels@silvxlabs.com` so the partition size can be + reduced for your feature. + - **422 Unprocessable Entity**: `partition_index` is past the last + partition, the feature is not in `completed` status, or the + underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + partition_index (int): Zero-indexed partition number. Valid range is `0` ≤ + `partition_index` < `partition_count` from `GET /data/metadata`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + partition_index=partition_index, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + feature_id: str, + partition_index: int, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Get one partition of feature data as GeoJSON + + # Get Feature Data Partition + + Returns one partition of a completed feature's data as a self-contained + GeoJSON `FeatureCollection`. To stream the full collection, first call + `GET /domains/{domain_id}/features/{feature_id}/data/metadata` to + discover `partition_count`, then GET this endpoint for each + `partition_index` from `0` to `partition_count - 1`. The concatenated + `features` arrays reproduce the source feature list in source order. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + - **partition_index**: Zero-indexed partition number. Must be `< partition_count` + from `/data/metadata`. + + ## Response + + `application/geo+json` body — a valid GeoJSON `FeatureCollection` + containing up to `partition_size` features. Each feature's `properties` + and `geometry` round-trip from the source GeoParquet via geopandas. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **413 Content Too Large**: Serialized partition exceeds the 30 MB + response cap. Partition size is fixed server-side and cannot be + adjusted by re-creating the feature; contact + `support.fastfuels@silvxlabs.com` so the partition size can be + reduced for your feature. + - **422 Unprocessable Entity**: `partition_index` is past the last + partition, the feature is not in `completed` status, or the + underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + partition_index (int): Zero-indexed partition number. Valid range is `0` ≤ + `partition_index` < `partition_count` from `GET /data/metadata`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + feature_id=feature_id, + partition_index=partition_index, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + feature_id: str, + partition_index: int, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Get one partition of feature data as GeoJSON + + # Get Feature Data Partition + + Returns one partition of a completed feature's data as a self-contained + GeoJSON `FeatureCollection`. To stream the full collection, first call + `GET /domains/{domain_id}/features/{feature_id}/data/metadata` to + discover `partition_count`, then GET this endpoint for each + `partition_index` from `0` to `partition_count - 1`. The concatenated + `features` arrays reproduce the source feature list in source order. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + - **partition_index**: Zero-indexed partition number. Must be `< partition_count` + from `/data/metadata`. + + ## Response + + `application/geo+json` body — a valid GeoJSON `FeatureCollection` + containing up to `partition_size` features. Each feature's `properties` + and `geometry` round-trip from the source GeoParquet via geopandas. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **413 Content Too Large**: Serialized partition exceeds the 30 MB + response cap. Partition size is fixed server-side and cannot be + adjusted by re-creating the feature; contact + `support.fastfuels@silvxlabs.com` so the partition size can be + reduced for your feature. + - **422 Unprocessable Entity**: `partition_index` is past the last + partition, the feature is not in `completed` status, or the + underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + partition_index (int): Zero-indexed partition number. Valid range is `0` ≤ + `partition_index` < `partition_count` from `GET /data/metadata`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + partition_index=partition_index, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + feature_id: str, + partition_index: int, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Get one partition of feature data as GeoJSON + + # Get Feature Data Partition + + Returns one partition of a completed feature's data as a self-contained + GeoJSON `FeatureCollection`. To stream the full collection, first call + `GET /domains/{domain_id}/features/{feature_id}/data/metadata` to + discover `partition_count`, then GET this endpoint for each + `partition_index` from `0` to `partition_count - 1`. The concatenated + `features` arrays reproduce the source feature list in source order. + + ## Path Parameters + + - **domain_id**: The domain the feature belongs to. + - **feature_id**: The unique identifier of the feature. + - **partition_index**: Zero-indexed partition number. Must be `< partition_count` + from `/data/metadata`. + + ## Response + + `application/geo+json` body — a valid GeoJSON `FeatureCollection` + containing up to `partition_size` features. Each feature's `properties` + and `geometry` round-trip from the source GeoParquet via geopandas. + + ## Error Responses + + - **404 Not Found**: Feature does not exist or is not accessible to the + caller. + - **413 Content Too Large**: Serialized partition exceeds the 30 MB + response cap. Partition size is fixed server-side and cannot be + adjusted by re-creating the feature; contact + `support.fastfuels@silvxlabs.com` so the partition size can be + reduced for your feature. + - **422 Unprocessable Entity**: `partition_index` is past the last + partition, the feature is not in `completed` status, or the + underlying GeoParquet blob is missing / malformed. + + Args: + domain_id (str): + feature_id (str): + partition_index (int): Zero-indexed partition number. Valid range is `0` ≤ + `partition_index` < `partition_count` from `GET /data/metadata`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + feature_id=feature_id, + partition_index=partition_index, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/list_features.py b/fastfuels_sdk/v2/client_library/api/features/list_features.py new file mode 100644 index 0000000..2551cdc --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/list_features.py @@ -0,0 +1,406 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.feature_sort_field import FeatureSortField +from ...models.feature_type import FeatureType +from ...models.http_validation_error import HTTPValidationError +from ...models.list_features_response import ListFeaturesResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, FeatureSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, FeatureType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_product: None | str | Unset + if isinstance(product, Unset): + json_product = UNSET + else: + json_product = product + params["product"] = json_product + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/features".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListFeaturesResponse | None: + if response.status_code == 200: + response_200 = ListFeaturesResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListFeaturesResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListFeaturesResponse]: + """List all features + + # List Features Endpoint + + Retrieves a paginated list of all features within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list features for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListFeaturesResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListFeaturesResponse | None: + """List all features + + # List Features Endpoint + + Retrieves a paginated list of all features within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list features for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListFeaturesResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListFeaturesResponse]: + """List all features + + # List Features Endpoint + + Retrieves a paginated list of all features within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list features for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListFeaturesResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListFeaturesResponse | None: + """List all features + + # List Features Endpoint + + Retrieves a paginated list of all features within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list features for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListFeaturesResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/list_features_cross_domain.py b/fastfuels_sdk/v2/client_library/api/features/list_features_cross_domain.py new file mode 100644 index 0000000..bb0e06f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/list_features_cross_domain.py @@ -0,0 +1,374 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.feature_sort_field import FeatureSortField +from ...models.feature_type import FeatureType +from ...models.http_validation_error import HTTPValidationError +from ...models.list_features_response import ListFeaturesResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, FeatureSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, FeatureType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_product: None | str | Unset + if isinstance(product, Unset): + json_product = UNSET + else: + json_product = product + params["product"] = json_product + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/-/features", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListFeaturesResponse | None: + if response.status_code == 200: + response_200 = ListFeaturesResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListFeaturesResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListFeaturesResponse]: + """List features across all domains + + # List Features Endpoint + + Retrieves a paginated list of all features across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListFeaturesResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListFeaturesResponse | None: + """List features across all domains + + # List Features Endpoint + + Retrieves a paginated list of all features across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListFeaturesResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListFeaturesResponse]: + """List features across all domains + + # List Features Endpoint + + Retrieves a paginated list of all features across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListFeaturesResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: FeatureSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: FeatureType | None | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListFeaturesResponse | None: + """List features across all domains + + # List Features Endpoint + + Retrieves a paginated list of all features across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `road`). + - **product**: (string, optional) Filter by source product (e.g., `osm`). + - **tag**: (string, optional) Filter features that contain this tag. + + ## Response + + Returns a paginated list of features with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of features to retrieve per page. Default: 100. + sort_by (FeatureSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (FeatureType | None | Unset): Filter features by entity type (e.g., 'road', + 'water'). + product (None | str | Unset): Filter features by source product (e.g., 'osm'). + tag (None | str | Unset): Filter features that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListFeaturesResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + product=product, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/features/update_feature.py b/fastfuels_sdk/v2/client_library/api/features/update_feature.py new file mode 100644 index 0000000..e648638 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/features/update_feature.py @@ -0,0 +1,320 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.feature import Feature +from ...models.http_validation_error import HTTPValidationError +from ...models.update_feature_request_body import UpdateFeatureRequestBody +from ...types import Response + + +def _get_kwargs( + domain_id: str, + feature_id: str, + *, + body: UpdateFeatureRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/domains/{domain_id}/features/{feature_id}".format( + domain_id=quote(str(domain_id), safe=""), + feature_id=quote(str(feature_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Feature | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Feature.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Feature | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, + body: UpdateFeatureRequestBody, +) -> Response[Feature | HTTPValidationError]: + """Update a feature + + # Update Feature Endpoint + + Updates the metadata of an existing feature resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the feature. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated feature resource. + + Args: + domain_id (str): + feature_id (str): + body (UpdateFeatureRequestBody): Request body for updating feature metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, + body: UpdateFeatureRequestBody, +) -> Feature | HTTPValidationError | None: + """Update a feature + + # Update Feature Endpoint + + Updates the metadata of an existing feature resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the feature. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated feature resource. + + Args: + domain_id (str): + feature_id (str): + body (UpdateFeatureRequestBody): Request body for updating feature metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, + body: UpdateFeatureRequestBody, +) -> Response[Feature | HTTPValidationError]: + """Update a feature + + # Update Feature Endpoint + + Updates the metadata of an existing feature resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the feature. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated feature resource. + + Args: + domain_id (str): + feature_id (str): + body (UpdateFeatureRequestBody): Request body for updating feature metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Feature | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + feature_id=feature_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + feature_id: str, + *, + client: AuthenticatedClient, + body: UpdateFeatureRequestBody, +) -> Feature | HTTPValidationError | None: + """Update a feature + + # Update Feature Endpoint + + Updates the metadata of an existing feature resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the feature belongs to. + - **feature_id**: (string) The unique identifier of the feature. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the feature. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated feature resource. + + Args: + domain_id (str): + feature_id (str): + body (UpdateFeatureRequestBody): Request body for updating feature metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Feature | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + feature_id=feature_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/__init__.py b/fastfuels_sdk/v2/client_library/api/grids/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/grids/apply_grid_modifications.py b/fastfuels_sdk/v2/client_library/api/grids/apply_grid_modifications.py new file mode 100644 index 0000000..ea419a8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/apply_grid_modifications.py @@ -0,0 +1,590 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.apply_grid_modifications_request import ApplyGridModificationsRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, + *, + body: ApplyGridModificationsRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/{grid_id}/modifications".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 200: + response_200 = Grid.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: ApplyGridModificationsRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Apply modifications to a grid in place + + # Apply Modifications to a Grid (in place) + + Applies modification rules to **this** grid in place — the grid keeps its + ID and the submitted rules are applied on top of its current data + asynchronously. To keep the original data instead, duplicate the grid + first (`POST .../{grid_id}/duplicate`) and modify the copy. + + The grid's stored data is updated directly; the upstream source (LANDFIRE, + 3DEP, ...) is **not** re-fetched, so cells your rules don't touch are + byte-for-byte unchanged — even if the upstream product has been updated + since the grid was built. + + Modifications select cells by conditions and apply actions to the matching + cells. + + ## Combining conditions: AND within a rule, OR across rules + + Each rule's `conditions` are **ANDed** — a cell is selected only when it + satisfies *every* condition in that rule. Adding a condition to a rule + therefore **narrows** the selection (the intersection). Example: a feature + condition plus an attribute condition matches cells inside the feature + **and** above a value threshold. + + There is **no OR within a rule**. To act on a **union** — \"roads *or* + water bodies\", \"GR1 *or* GR2 cells\" — use **multiple rules**. Rules are + applied independently and in order, so a cell matched by *any* rule is + affected. Adding a rule therefore **widens** the overall selection. + + Putting two mutually exclusive conditions in one rule (e.g. a road feature + AND a water feature) is the classic mistake: it selects cells that are + both at once — usually none. Split them into one rule per feature instead. + + ## Conditions + + **Attribute conditions** compare a band's cell values against a value: + - `band`: dot-notation band key (e.g., `fbfm`, `fuel_load.1hr`) + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`eq`/`ne` also accept a list of values) + - `value`: number or list for `eq`/`ne`. For `fbfm` bands you may use the + human-readable Scott-Burgan labels (`\"GR1\"`) or the numeric codes (`101`) + interchangeably — labels are resolved to codes when the rule is stored. + + **Spatial conditions** test each cell's location against a geometry. Two + variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this grid and be in `completed` status; cross-domain, + missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. + - `target`: `centroid` (default) tests the cell center; `cell` tests the + cell's full footprint — use it with linestring features (e.g. roads) + so every crossed cell matches. + + ## Actions + + - `{\"band\": \"...\", \"modifier\": \"replace|multiply|divide|add|subtract\", \"value\": ...}` + - Non-`replace` results are clamped at zero (grid bands are physical + quantities). + + ## Response + + Returns this grid (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it (resample, lookup, + exports) can detect that the source has changed. The submitted rules + appear in the grid's `modifications` list once processing completes — + poll the grid until status returns to `\"completed\"`. + + If processing fails, the grid's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not owned by the caller, + or is not in this domain. + - **422 Unprocessable Content**: The grid is not in `completed` status + (and is not a retryable failed modification); the grid is a 3D voxel + grid (apply modifications to the source tree inventory and re-voxelize + instead); a referenced `feature_id` is missing, cross-domain, or not + completed; or a referenced band does not exist on this grid. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (ApplyGridModificationsRequest): Request body for applying modifications to a grid in + place. + + Metadata (name, description, tags) is not accepted here — the grid keeps + its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: ApplyGridModificationsRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Apply modifications to a grid in place + + # Apply Modifications to a Grid (in place) + + Applies modification rules to **this** grid in place — the grid keeps its + ID and the submitted rules are applied on top of its current data + asynchronously. To keep the original data instead, duplicate the grid + first (`POST .../{grid_id}/duplicate`) and modify the copy. + + The grid's stored data is updated directly; the upstream source (LANDFIRE, + 3DEP, ...) is **not** re-fetched, so cells your rules don't touch are + byte-for-byte unchanged — even if the upstream product has been updated + since the grid was built. + + Modifications select cells by conditions and apply actions to the matching + cells. + + ## Combining conditions: AND within a rule, OR across rules + + Each rule's `conditions` are **ANDed** — a cell is selected only when it + satisfies *every* condition in that rule. Adding a condition to a rule + therefore **narrows** the selection (the intersection). Example: a feature + condition plus an attribute condition matches cells inside the feature + **and** above a value threshold. + + There is **no OR within a rule**. To act on a **union** — \"roads *or* + water bodies\", \"GR1 *or* GR2 cells\" — use **multiple rules**. Rules are + applied independently and in order, so a cell matched by *any* rule is + affected. Adding a rule therefore **widens** the overall selection. + + Putting two mutually exclusive conditions in one rule (e.g. a road feature + AND a water feature) is the classic mistake: it selects cells that are + both at once — usually none. Split them into one rule per feature instead. + + ## Conditions + + **Attribute conditions** compare a band's cell values against a value: + - `band`: dot-notation band key (e.g., `fbfm`, `fuel_load.1hr`) + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`eq`/`ne` also accept a list of values) + - `value`: number or list for `eq`/`ne`. For `fbfm` bands you may use the + human-readable Scott-Burgan labels (`\"GR1\"`) or the numeric codes (`101`) + interchangeably — labels are resolved to codes when the rule is stored. + + **Spatial conditions** test each cell's location against a geometry. Two + variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this grid and be in `completed` status; cross-domain, + missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. + - `target`: `centroid` (default) tests the cell center; `cell` tests the + cell's full footprint — use it with linestring features (e.g. roads) + so every crossed cell matches. + + ## Actions + + - `{\"band\": \"...\", \"modifier\": \"replace|multiply|divide|add|subtract\", \"value\": ...}` + - Non-`replace` results are clamped at zero (grid bands are physical + quantities). + + ## Response + + Returns this grid (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it (resample, lookup, + exports) can detect that the source has changed. The submitted rules + appear in the grid's `modifications` list once processing completes — + poll the grid until status returns to `\"completed\"`. + + If processing fails, the grid's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not owned by the caller, + or is not in this domain. + - **422 Unprocessable Content**: The grid is not in `completed` status + (and is not a retryable failed modification); the grid is a 3D voxel + grid (apply modifications to the source tree inventory and re-voxelize + instead); a referenced `feature_id` is missing, cross-domain, or not + completed; or a referenced band does not exist on this grid. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (ApplyGridModificationsRequest): Request body for applying modifications to a grid in + place. + + Metadata (name, description, tags) is not accepted here — the grid keeps + its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: ApplyGridModificationsRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Apply modifications to a grid in place + + # Apply Modifications to a Grid (in place) + + Applies modification rules to **this** grid in place — the grid keeps its + ID and the submitted rules are applied on top of its current data + asynchronously. To keep the original data instead, duplicate the grid + first (`POST .../{grid_id}/duplicate`) and modify the copy. + + The grid's stored data is updated directly; the upstream source (LANDFIRE, + 3DEP, ...) is **not** re-fetched, so cells your rules don't touch are + byte-for-byte unchanged — even if the upstream product has been updated + since the grid was built. + + Modifications select cells by conditions and apply actions to the matching + cells. + + ## Combining conditions: AND within a rule, OR across rules + + Each rule's `conditions` are **ANDed** — a cell is selected only when it + satisfies *every* condition in that rule. Adding a condition to a rule + therefore **narrows** the selection (the intersection). Example: a feature + condition plus an attribute condition matches cells inside the feature + **and** above a value threshold. + + There is **no OR within a rule**. To act on a **union** — \"roads *or* + water bodies\", \"GR1 *or* GR2 cells\" — use **multiple rules**. Rules are + applied independently and in order, so a cell matched by *any* rule is + affected. Adding a rule therefore **widens** the overall selection. + + Putting two mutually exclusive conditions in one rule (e.g. a road feature + AND a water feature) is the classic mistake: it selects cells that are + both at once — usually none. Split them into one rule per feature instead. + + ## Conditions + + **Attribute conditions** compare a band's cell values against a value: + - `band`: dot-notation band key (e.g., `fbfm`, `fuel_load.1hr`) + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`eq`/`ne` also accept a list of values) + - `value`: number or list for `eq`/`ne`. For `fbfm` bands you may use the + human-readable Scott-Burgan labels (`\"GR1\"`) or the numeric codes (`101`) + interchangeably — labels are resolved to codes when the rule is stored. + + **Spatial conditions** test each cell's location against a geometry. Two + variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this grid and be in `completed` status; cross-domain, + missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. + - `target`: `centroid` (default) tests the cell center; `cell` tests the + cell's full footprint — use it with linestring features (e.g. roads) + so every crossed cell matches. + + ## Actions + + - `{\"band\": \"...\", \"modifier\": \"replace|multiply|divide|add|subtract\", \"value\": ...}` + - Non-`replace` results are clamped at zero (grid bands are physical + quantities). + + ## Response + + Returns this grid (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it (resample, lookup, + exports) can detect that the source has changed. The submitted rules + appear in the grid's `modifications` list once processing completes — + poll the grid until status returns to `\"completed\"`. + + If processing fails, the grid's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not owned by the caller, + or is not in this domain. + - **422 Unprocessable Content**: The grid is not in `completed` status + (and is not a retryable failed modification); the grid is a 3D voxel + grid (apply modifications to the source tree inventory and re-voxelize + instead); a referenced `feature_id` is missing, cross-domain, or not + completed; or a referenced band does not exist on this grid. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (ApplyGridModificationsRequest): Request body for applying modifications to a grid in + place. + + Metadata (name, description, tags) is not accepted here — the grid keeps + its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: ApplyGridModificationsRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Apply modifications to a grid in place + + # Apply Modifications to a Grid (in place) + + Applies modification rules to **this** grid in place — the grid keeps its + ID and the submitted rules are applied on top of its current data + asynchronously. To keep the original data instead, duplicate the grid + first (`POST .../{grid_id}/duplicate`) and modify the copy. + + The grid's stored data is updated directly; the upstream source (LANDFIRE, + 3DEP, ...) is **not** re-fetched, so cells your rules don't touch are + byte-for-byte unchanged — even if the upstream product has been updated + since the grid was built. + + Modifications select cells by conditions and apply actions to the matching + cells. + + ## Combining conditions: AND within a rule, OR across rules + + Each rule's `conditions` are **ANDed** — a cell is selected only when it + satisfies *every* condition in that rule. Adding a condition to a rule + therefore **narrows** the selection (the intersection). Example: a feature + condition plus an attribute condition matches cells inside the feature + **and** above a value threshold. + + There is **no OR within a rule**. To act on a **union** — \"roads *or* + water bodies\", \"GR1 *or* GR2 cells\" — use **multiple rules**. Rules are + applied independently and in order, so a cell matched by *any* rule is + affected. Adding a rule therefore **widens** the overall selection. + + Putting two mutually exclusive conditions in one rule (e.g. a road feature + AND a water feature) is the classic mistake: it selects cells that are + both at once — usually none. Split them into one rule per feature instead. + + ## Conditions + + **Attribute conditions** compare a band's cell values against a value: + - `band`: dot-notation band key (e.g., `fbfm`, `fuel_load.1hr`) + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`eq`/`ne` also accept a list of values) + - `value`: number or list for `eq`/`ne`. For `fbfm` bands you may use the + human-readable Scott-Burgan labels (`\"GR1\"`) or the numeric codes (`101`) + interchangeably — labels are resolved to codes when the rule is stored. + + **Spatial conditions** test each cell's location against a geometry. Two + variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this grid and be in `completed` status; cross-domain, + missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. + - `target`: `centroid` (default) tests the cell center; `cell` tests the + cell's full footprint — use it with linestring features (e.g. roads) + so every crossed cell matches. + + ## Actions + + - `{\"band\": \"...\", \"modifier\": \"replace|multiply|divide|add|subtract\", \"value\": ...}` + - Non-`replace` results are clamped at zero (grid bands are physical + quantities). + + ## Response + + Returns this grid (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it (resample, lookup, + exports) can detect that the source has changed. The submitted rules + appear in the grid's `modifications` list once processing completes — + poll the grid until status returns to `\"completed\"`. + + If processing fails, the grid's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not owned by the caller, + or is not in this domain. + - **422 Unprocessable Content**: The grid is not in `completed` status + (and is not a retryable failed modification); the grid is a 3D voxel + grid (apply modifications to the source tree inventory and re-voxelize + instead); a referenced `feature_id` is missing, cross-domain, or not + completed; or a referenced band does not exist on this grid. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (ApplyGridModificationsRequest): Request body for applying modifications to a grid in + place. + + Metadata (name, description, tags) is not accepted here — the grid keeps + its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/check_3dep_coverage.py b/fastfuels_sdk/v2/client_library/api/grids/check_3dep_coverage.py new file mode 100644 index 0000000..260887f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/check_3dep_coverage.py @@ -0,0 +1,253 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.three_dep_resolution import ThreeDepResolution +from ...models.topography_three_dep_coverage_response import ( + TopographyThreeDepCoverageResponse, +) +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + resolution: ThreeDepResolution | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + json_resolution: int | Unset = UNSET + if not isinstance(resolution, Unset): + json_resolution = resolution.value + + params["resolution"] = json_resolution + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids/topography/3dep/coverage".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | TopographyThreeDepCoverageResponse | None: + if response.status_code == 200: + response_200 = TopographyThreeDepCoverageResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | TopographyThreeDepCoverageResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: ThreeDepResolution | Unset = UNSET, +) -> Response[HTTPValidationError | TopographyThreeDepCoverageResponse]: + """Check 3DEP tile coverage for a domain + + # Check 3DEP Tile Coverage + + Immediate pre-flight check that reports which 3DEP tiles are available + for the domain at the requested resolution. Use this before creating a + 3DEP grid to avoid waiting for async processing only to discover a + coverage gap — especially useful for 1m (S1M) data where coverage is + regional. + + ## Query Parameters + + - **resolution**: Resolution in meters: 1, 10, or 30. Default: 1. + + ## Response + + Returns tile availability, count, URLs, and (for 1m) acquisition dates. + + Args: + domain_id (str): + resolution (ThreeDepResolution | Unset): Available resolutions for 3DEP data (meters). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | TopographyThreeDepCoverageResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + resolution=resolution, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: ThreeDepResolution | Unset = UNSET, +) -> HTTPValidationError | TopographyThreeDepCoverageResponse | None: + """Check 3DEP tile coverage for a domain + + # Check 3DEP Tile Coverage + + Immediate pre-flight check that reports which 3DEP tiles are available + for the domain at the requested resolution. Use this before creating a + 3DEP grid to avoid waiting for async processing only to discover a + coverage gap — especially useful for 1m (S1M) data where coverage is + regional. + + ## Query Parameters + + - **resolution**: Resolution in meters: 1, 10, or 30. Default: 1. + + ## Response + + Returns tile availability, count, URLs, and (for 1m) acquisition dates. + + Args: + domain_id (str): + resolution (ThreeDepResolution | Unset): Available resolutions for 3DEP data (meters). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | TopographyThreeDepCoverageResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + resolution=resolution, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: ThreeDepResolution | Unset = UNSET, +) -> Response[HTTPValidationError | TopographyThreeDepCoverageResponse]: + """Check 3DEP tile coverage for a domain + + # Check 3DEP Tile Coverage + + Immediate pre-flight check that reports which 3DEP tiles are available + for the domain at the requested resolution. Use this before creating a + 3DEP grid to avoid waiting for async processing only to discover a + coverage gap — especially useful for 1m (S1M) data where coverage is + regional. + + ## Query Parameters + + - **resolution**: Resolution in meters: 1, 10, or 30. Default: 1. + + ## Response + + Returns tile availability, count, URLs, and (for 1m) acquisition dates. + + Args: + domain_id (str): + resolution (ThreeDepResolution | Unset): Available resolutions for 3DEP data (meters). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | TopographyThreeDepCoverageResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + resolution=resolution, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + resolution: ThreeDepResolution | Unset = UNSET, +) -> HTTPValidationError | TopographyThreeDepCoverageResponse | None: + """Check 3DEP tile coverage for a domain + + # Check 3DEP Tile Coverage + + Immediate pre-flight check that reports which 3DEP tiles are available + for the domain at the requested resolution. Use this before creating a + 3DEP grid to avoid waiting for async processing only to discover a + coverage gap — especially useful for 1m (S1M) data where coverage is + regional. + + ## Query Parameters + + - **resolution**: Resolution in meters: 1, 10, or 30. Default: 1. + + ## Response + + Returns tile availability, count, URLs, and (for 1m) acquisition dates. + + Args: + domain_id (str): + resolution (ThreeDepResolution | Unset): Available resolutions for 3DEP data (meters). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | TopographyThreeDepCoverageResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + resolution=resolution, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_3dep_topography.py b/fastfuels_sdk/v2/client_library/api/grids/create_3dep_topography.py new file mode 100644 index 0000000..6dd4997 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_3dep_topography.py @@ -0,0 +1,350 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_three_dep_topography_request import ( + CreateThreeDepTopographyRequest, +) +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateThreeDepTopographyRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/topography/3dep".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepTopographyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from 3DEP topographic data + + # Create 3DEP Topography Grid + + Creates a grid with topographic data from USGS 3DEP at selectable resolution. + + Available resolutions: + - **1m**: Seamless 1-meter (S1M). Coverage varies by region; areas without + S1M data will return a COVERAGE_ERROR. + - **10m**: 1/3 arc-second seamless (default) + - **30m**: 1 arc-second seamless + + Available bands: + - **elevation**: meters above sea level (default) + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + Slope and aspect are computed locally from the DEM using Horn's method. + + ## Request Body + + - **source_resolution**: (optional) Source product family in meters: + 1, 10, or 30. Default: 10. To change the *output* cell size, set + ``alignment.resolution``. + - **bands**: (optional) Which bands to include. Default: elevation only. + - **alignment**: (optional) Output alignment target. See alignment docs. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateThreeDepTopographyRequest): Request to create a grid from 3DEP topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + `source_resolution` selects the 3DEP product family (1m, 10m, or 30m). + To change the *output* cell size, set ``alignment.resolution`` instead. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepTopographyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from 3DEP topographic data + + # Create 3DEP Topography Grid + + Creates a grid with topographic data from USGS 3DEP at selectable resolution. + + Available resolutions: + - **1m**: Seamless 1-meter (S1M). Coverage varies by region; areas without + S1M data will return a COVERAGE_ERROR. + - **10m**: 1/3 arc-second seamless (default) + - **30m**: 1 arc-second seamless + + Available bands: + - **elevation**: meters above sea level (default) + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + Slope and aspect are computed locally from the DEM using Horn's method. + + ## Request Body + + - **source_resolution**: (optional) Source product family in meters: + 1, 10, or 30. Default: 10. To change the *output* cell size, set + ``alignment.resolution``. + - **bands**: (optional) Which bands to include. Default: elevation only. + - **alignment**: (optional) Output alignment target. See alignment docs. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateThreeDepTopographyRequest): Request to create a grid from 3DEP topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + `source_resolution` selects the 3DEP product family (1m, 10m, or 30m). + To change the *output* cell size, set ``alignment.resolution`` instead. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepTopographyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from 3DEP topographic data + + # Create 3DEP Topography Grid + + Creates a grid with topographic data from USGS 3DEP at selectable resolution. + + Available resolutions: + - **1m**: Seamless 1-meter (S1M). Coverage varies by region; areas without + S1M data will return a COVERAGE_ERROR. + - **10m**: 1/3 arc-second seamless (default) + - **30m**: 1 arc-second seamless + + Available bands: + - **elevation**: meters above sea level (default) + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + Slope and aspect are computed locally from the DEM using Horn's method. + + ## Request Body + + - **source_resolution**: (optional) Source product family in meters: + 1, 10, or 30. Default: 10. To change the *output* cell size, set + ``alignment.resolution``. + - **bands**: (optional) Which bands to include. Default: elevation only. + - **alignment**: (optional) Output alignment target. See alignment docs. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateThreeDepTopographyRequest): Request to create a grid from 3DEP topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + `source_resolution` selects the 3DEP product family (1m, 10m, or 30m). + To change the *output* cell size, set ``alignment.resolution`` instead. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepTopographyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from 3DEP topographic data + + # Create 3DEP Topography Grid + + Creates a grid with topographic data from USGS 3DEP at selectable resolution. + + Available resolutions: + - **1m**: Seamless 1-meter (S1M). Coverage varies by region; areas without + S1M data will return a COVERAGE_ERROR. + - **10m**: 1/3 arc-second seamless (default) + - **30m**: 1 arc-second seamless + + Available bands: + - **elevation**: meters above sea level (default) + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + Slope and aspect are computed locally from the DEM using Horn's method. + + ## Request Body + + - **source_resolution**: (optional) Source product family in meters: + 1, 10, or 30. Default: 10. To change the *output* cell size, set + ``alignment.resolution``. + - **bands**: (optional) Which bands to include. Default: elevation only. + - **alignment**: (optional) Output alignment target. See alignment docs. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateThreeDepTopographyRequest): Request to create a grid from 3DEP topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + `source_resolution` selects the 3DEP product family (1m, 10m, or 30m). + To change the *output* cell size, set ``alignment.resolution`` instead. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_compose_grid.py b/fastfuels_sdk/v2/client_library/api/grids/create_compose_grid.py new file mode 100644 index 0000000..949a82a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_compose_grid.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_compose_request import CreateComposeRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateComposeRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/compose".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateComposeRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + """Create a grid by composing existing grids + + # Create Compose Grid + + Creates a new grid by selecting bands, computing bands, and applying + optional conditional fallback rules across one or more completed grids. + + Args: + domain_id (str): + body (CreateComposeRequest): Request to create a grid by composing one or more existing + grids. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateComposeRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + """Create a grid by composing existing grids + + # Create Compose Grid + + Creates a new grid by selecting bands, computing bands, and applying + optional conditional fallback rules across one or more completed grids. + + Args: + domain_id (str): + body (CreateComposeRequest): Request to create a grid by composing one or more existing + grids. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateComposeRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + """Create a grid by composing existing grids + + # Create Compose Grid + + Creates a new grid by selecting bands, computing bands, and applying + optional conditional fallback rules across one or more completed grids. + + Args: + domain_id (str): + body (CreateComposeRequest): Request to create a grid by composing one or more existing + grids. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateComposeRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + """Create a grid by composing existing grids + + # Create Compose Grid + + Creates a new grid by selecting bands, computing bands, and applying + optional conditional fallback rules across one or more completed grids. + + Args: + domain_id (str): + body (CreateComposeRequest): Request to create a grid by composing one or more existing + grids. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_duet_grid.py b/fastfuels_sdk/v2/client_library/api/grids/create_duet_grid.py new file mode 100644 index 0000000..b42f5cf --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_duet_grid.py @@ -0,0 +1,452 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_duet_request import CreateDuetRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateDuetRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/duet".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateDuetRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a surface fuel grid with DUET + + # Create a DUET Surface Fuel Grid + + Runs DUET (Distribution of Understory using Elliptical Transport) over a 3D + tree grid to produce 2D surface fuels. DUET drops leaf and needle litter + from each tree's crown along wind-driven elliptical fall trajectories, then + grows grass as a function of shade and litter cover — so litter accumulates + under and downwind of crowns, and grass fills the gaps between them. + + ## What DUET does and does not give you + + DUET supplies the **spatial pattern** of surface fuels, keyed to real canopy + structure. It does **not** supply physical magnitudes: raw DUET loadings are + idiosyncratic to the model and should not be read as fuel loads or fed to a + fire model as-is. Use `calibration` to impose magnitudes you trust — from + field data, from the literature, or from an FBFM40 grid. + + ## Request Body + + - **source_grid_id**: (required) A completed 3D tree grid carrying the + `bulk_density.foliage.live`, `spcd`, and `fuel_moisture.live` bands. + Create one with `POST /grids/voxelize/inventory/tree`, requesting those + three bands — `spcd` in particular is not voxelized by default. + - **years_since_burn**: (required) Years of litter accumulation to simulate, + 1–100. DUET starts from the year of the last fire, with grass and litter + consumed, so this is the stand's time since fire. It is the single most + consequential parameter: a low value yields almost no litter because there + has been no time for any to fall. It also drives runtime. + - **wind_direction**: (optional) Degrees clockwise from north. Default 270. + - **wind_variability**: (optional) Angular spread in degrees. Default 30. + - **bands**: (optional) Output bands. Defaults to `fuel_load.grass` and + `fuel_load.litter`. DUET separates fuels by type rather than size class, + so bands are named for `grass`, `litter` (and its `litter.coniferous` / + `litter.deciduous` parts), and `total`. + - **calibration**: (optional) Per-parameter, per-fuel-type targets. Omit to + store raw output. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Calibration + + Each of `fuel_load`, `fuel_depth`, and `fuel_moisture` is calibrated + independently, and within each, per fuel type (`grass`, `coniferous`, + `deciduous`, `litter`, or `all` — which is exclusive of the others). Methods: + + - `maxmin` — rescale to a target maximum and minimum. Best when fuel data + are limited, or when their distribution does not resemble DUET's. + - `meansd` — rescale to a target mean and standard deviation. Appropriate + only when the targets come from a dataset large enough to approximate a + normal distribution. + - `constant` — assign a single value. Reasonable only when that is the only + value available. + + Calibration rescales only cells that already carry fuel; cells DUET left + empty stay empty. A consequence worth expecting: where cover is sparse, the + domain-wide mean will sit well below a `meansd` target, because the target + applies to the covered cells rather than to the domain. + + ## Response + + Returns the created Grid with status `\"pending\"` and `georeference: null`. + Treevox runs DUET asynchronously and updates the grid to `\"completed\"` with + a 2D `Georeference` when done. + + Args: + domain_id (str): + body (CreateDuetRequest): Request body for creating a DUET surface fuel grid from a tree + grid. + + Does not extend CreateGridRequestBase: like the 3D grids it derives from, + DUET grids do not support modifications. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateDuetRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a surface fuel grid with DUET + + # Create a DUET Surface Fuel Grid + + Runs DUET (Distribution of Understory using Elliptical Transport) over a 3D + tree grid to produce 2D surface fuels. DUET drops leaf and needle litter + from each tree's crown along wind-driven elliptical fall trajectories, then + grows grass as a function of shade and litter cover — so litter accumulates + under and downwind of crowns, and grass fills the gaps between them. + + ## What DUET does and does not give you + + DUET supplies the **spatial pattern** of surface fuels, keyed to real canopy + structure. It does **not** supply physical magnitudes: raw DUET loadings are + idiosyncratic to the model and should not be read as fuel loads or fed to a + fire model as-is. Use `calibration` to impose magnitudes you trust — from + field data, from the literature, or from an FBFM40 grid. + + ## Request Body + + - **source_grid_id**: (required) A completed 3D tree grid carrying the + `bulk_density.foliage.live`, `spcd`, and `fuel_moisture.live` bands. + Create one with `POST /grids/voxelize/inventory/tree`, requesting those + three bands — `spcd` in particular is not voxelized by default. + - **years_since_burn**: (required) Years of litter accumulation to simulate, + 1–100. DUET starts from the year of the last fire, with grass and litter + consumed, so this is the stand's time since fire. It is the single most + consequential parameter: a low value yields almost no litter because there + has been no time for any to fall. It also drives runtime. + - **wind_direction**: (optional) Degrees clockwise from north. Default 270. + - **wind_variability**: (optional) Angular spread in degrees. Default 30. + - **bands**: (optional) Output bands. Defaults to `fuel_load.grass` and + `fuel_load.litter`. DUET separates fuels by type rather than size class, + so bands are named for `grass`, `litter` (and its `litter.coniferous` / + `litter.deciduous` parts), and `total`. + - **calibration**: (optional) Per-parameter, per-fuel-type targets. Omit to + store raw output. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Calibration + + Each of `fuel_load`, `fuel_depth`, and `fuel_moisture` is calibrated + independently, and within each, per fuel type (`grass`, `coniferous`, + `deciduous`, `litter`, or `all` — which is exclusive of the others). Methods: + + - `maxmin` — rescale to a target maximum and minimum. Best when fuel data + are limited, or when their distribution does not resemble DUET's. + - `meansd` — rescale to a target mean and standard deviation. Appropriate + only when the targets come from a dataset large enough to approximate a + normal distribution. + - `constant` — assign a single value. Reasonable only when that is the only + value available. + + Calibration rescales only cells that already carry fuel; cells DUET left + empty stay empty. A consequence worth expecting: where cover is sparse, the + domain-wide mean will sit well below a `meansd` target, because the target + applies to the covered cells rather than to the domain. + + ## Response + + Returns the created Grid with status `\"pending\"` and `georeference: null`. + Treevox runs DUET asynchronously and updates the grid to `\"completed\"` with + a 2D `Georeference` when done. + + Args: + domain_id (str): + body (CreateDuetRequest): Request body for creating a DUET surface fuel grid from a tree + grid. + + Does not extend CreateGridRequestBase: like the 3D grids it derives from, + DUET grids do not support modifications. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateDuetRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a surface fuel grid with DUET + + # Create a DUET Surface Fuel Grid + + Runs DUET (Distribution of Understory using Elliptical Transport) over a 3D + tree grid to produce 2D surface fuels. DUET drops leaf and needle litter + from each tree's crown along wind-driven elliptical fall trajectories, then + grows grass as a function of shade and litter cover — so litter accumulates + under and downwind of crowns, and grass fills the gaps between them. + + ## What DUET does and does not give you + + DUET supplies the **spatial pattern** of surface fuels, keyed to real canopy + structure. It does **not** supply physical magnitudes: raw DUET loadings are + idiosyncratic to the model and should not be read as fuel loads or fed to a + fire model as-is. Use `calibration` to impose magnitudes you trust — from + field data, from the literature, or from an FBFM40 grid. + + ## Request Body + + - **source_grid_id**: (required) A completed 3D tree grid carrying the + `bulk_density.foliage.live`, `spcd`, and `fuel_moisture.live` bands. + Create one with `POST /grids/voxelize/inventory/tree`, requesting those + three bands — `spcd` in particular is not voxelized by default. + - **years_since_burn**: (required) Years of litter accumulation to simulate, + 1–100. DUET starts from the year of the last fire, with grass and litter + consumed, so this is the stand's time since fire. It is the single most + consequential parameter: a low value yields almost no litter because there + has been no time for any to fall. It also drives runtime. + - **wind_direction**: (optional) Degrees clockwise from north. Default 270. + - **wind_variability**: (optional) Angular spread in degrees. Default 30. + - **bands**: (optional) Output bands. Defaults to `fuel_load.grass` and + `fuel_load.litter`. DUET separates fuels by type rather than size class, + so bands are named for `grass`, `litter` (and its `litter.coniferous` / + `litter.deciduous` parts), and `total`. + - **calibration**: (optional) Per-parameter, per-fuel-type targets. Omit to + store raw output. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Calibration + + Each of `fuel_load`, `fuel_depth`, and `fuel_moisture` is calibrated + independently, and within each, per fuel type (`grass`, `coniferous`, + `deciduous`, `litter`, or `all` — which is exclusive of the others). Methods: + + - `maxmin` — rescale to a target maximum and minimum. Best when fuel data + are limited, or when their distribution does not resemble DUET's. + - `meansd` — rescale to a target mean and standard deviation. Appropriate + only when the targets come from a dataset large enough to approximate a + normal distribution. + - `constant` — assign a single value. Reasonable only when that is the only + value available. + + Calibration rescales only cells that already carry fuel; cells DUET left + empty stay empty. A consequence worth expecting: where cover is sparse, the + domain-wide mean will sit well below a `meansd` target, because the target + applies to the covered cells rather than to the domain. + + ## Response + + Returns the created Grid with status `\"pending\"` and `georeference: null`. + Treevox runs DUET asynchronously and updates the grid to `\"completed\"` with + a 2D `Georeference` when done. + + Args: + domain_id (str): + body (CreateDuetRequest): Request body for creating a DUET surface fuel grid from a tree + grid. + + Does not extend CreateGridRequestBase: like the 3D grids it derives from, + DUET grids do not support modifications. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateDuetRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a surface fuel grid with DUET + + # Create a DUET Surface Fuel Grid + + Runs DUET (Distribution of Understory using Elliptical Transport) over a 3D + tree grid to produce 2D surface fuels. DUET drops leaf and needle litter + from each tree's crown along wind-driven elliptical fall trajectories, then + grows grass as a function of shade and litter cover — so litter accumulates + under and downwind of crowns, and grass fills the gaps between them. + + ## What DUET does and does not give you + + DUET supplies the **spatial pattern** of surface fuels, keyed to real canopy + structure. It does **not** supply physical magnitudes: raw DUET loadings are + idiosyncratic to the model and should not be read as fuel loads or fed to a + fire model as-is. Use `calibration` to impose magnitudes you trust — from + field data, from the literature, or from an FBFM40 grid. + + ## Request Body + + - **source_grid_id**: (required) A completed 3D tree grid carrying the + `bulk_density.foliage.live`, `spcd`, and `fuel_moisture.live` bands. + Create one with `POST /grids/voxelize/inventory/tree`, requesting those + three bands — `spcd` in particular is not voxelized by default. + - **years_since_burn**: (required) Years of litter accumulation to simulate, + 1–100. DUET starts from the year of the last fire, with grass and litter + consumed, so this is the stand's time since fire. It is the single most + consequential parameter: a low value yields almost no litter because there + has been no time for any to fall. It also drives runtime. + - **wind_direction**: (optional) Degrees clockwise from north. Default 270. + - **wind_variability**: (optional) Angular spread in degrees. Default 30. + - **bands**: (optional) Output bands. Defaults to `fuel_load.grass` and + `fuel_load.litter`. DUET separates fuels by type rather than size class, + so bands are named for `grass`, `litter` (and its `litter.coniferous` / + `litter.deciduous` parts), and `total`. + - **calibration**: (optional) Per-parameter, per-fuel-type targets. Omit to + store raw output. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Calibration + + Each of `fuel_load`, `fuel_depth`, and `fuel_moisture` is calibrated + independently, and within each, per fuel type (`grass`, `coniferous`, + `deciduous`, `litter`, or `all` — which is exclusive of the others). Methods: + + - `maxmin` — rescale to a target maximum and minimum. Best when fuel data + are limited, or when their distribution does not resemble DUET's. + - `meansd` — rescale to a target mean and standard deviation. Appropriate + only when the targets come from a dataset large enough to approximate a + normal distribution. + - `constant` — assign a single value. Reasonable only when that is the only + value available. + + Calibration rescales only cells that already carry fuel; cells DUET left + empty stay empty. A consequence worth expecting: where cover is sparse, the + domain-wide mean will sit well below a `meansd` target, because the target + applies to the covered cells rather than to the domain. + + ## Response + + Returns the created Grid with status `\"pending\"` and `georeference: null`. + Treevox runs DUET asynchronously and updates the grid to `\"completed\"` with + a 2D `Georeference` when done. + + Args: + domain_id (str): + body (CreateDuetRequest): Request body for creating a DUET surface fuel grid from a tree + grid. + + Does not extend CreateGridRequestBase: like the 3D grids it derives from, + DUET grids do not support modifications. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_fbfm13_lookup.py b/fastfuels_sdk/v2/client_library/api/grids/create_fbfm13_lookup.py new file mode 100644 index 0000000..0985a5c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_fbfm13_lookup.py @@ -0,0 +1,408 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_fbfm_13_lookup_request import CreateFbfm13LookupRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateFbfm13LookupRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/lookup/fbfm13".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm13LookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FBFM13 fuel parameters + + # Create FBFM13 Lookup Grid + + Converts Anderson 13 fuel model codes to fuel parameters using the + Anderson 13 lookup table. + + Takes a source grid containing categorical FBFM13 codes (from + `/grids/fbfm13/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM13 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_foliage` - Live foliage fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_foliage` - Live foliage fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM13 codes. Defaults to `\"fbfm13\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM13 Codes + + The source grid must contain only valid Anderson 13 fuel model codes. + The 18 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **Anderson 13 models**: 1–13 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (91-99) produce zero values for all bands. + - Fuel parameter values are from Anderson, Hal E. 1982. *Aids to + determining fuel models for estimating fire behavior.* USDA Forest + Service General Technical Report INT-122. + - All output values are in metric units (converted from Anderson 13 imperial values). + + Args: + domain_id (str): + body (CreateFbfm13LookupRequest): Request to create a grid by looking up FBFM13 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm13LookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FBFM13 fuel parameters + + # Create FBFM13 Lookup Grid + + Converts Anderson 13 fuel model codes to fuel parameters using the + Anderson 13 lookup table. + + Takes a source grid containing categorical FBFM13 codes (from + `/grids/fbfm13/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM13 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_foliage` - Live foliage fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_foliage` - Live foliage fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM13 codes. Defaults to `\"fbfm13\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM13 Codes + + The source grid must contain only valid Anderson 13 fuel model codes. + The 18 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **Anderson 13 models**: 1–13 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (91-99) produce zero values for all bands. + - Fuel parameter values are from Anderson, Hal E. 1982. *Aids to + determining fuel models for estimating fire behavior.* USDA Forest + Service General Technical Report INT-122. + - All output values are in metric units (converted from Anderson 13 imperial values). + + Args: + domain_id (str): + body (CreateFbfm13LookupRequest): Request to create a grid by looking up FBFM13 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm13LookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FBFM13 fuel parameters + + # Create FBFM13 Lookup Grid + + Converts Anderson 13 fuel model codes to fuel parameters using the + Anderson 13 lookup table. + + Takes a source grid containing categorical FBFM13 codes (from + `/grids/fbfm13/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM13 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_foliage` - Live foliage fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_foliage` - Live foliage fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM13 codes. Defaults to `\"fbfm13\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM13 Codes + + The source grid must contain only valid Anderson 13 fuel model codes. + The 18 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **Anderson 13 models**: 1–13 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (91-99) produce zero values for all bands. + - Fuel parameter values are from Anderson, Hal E. 1982. *Aids to + determining fuel models for estimating fire behavior.* USDA Forest + Service General Technical Report INT-122. + - All output values are in metric units (converted from Anderson 13 imperial values). + + Args: + domain_id (str): + body (CreateFbfm13LookupRequest): Request to create a grid by looking up FBFM13 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm13LookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FBFM13 fuel parameters + + # Create FBFM13 Lookup Grid + + Converts Anderson 13 fuel model codes to fuel parameters using the + Anderson 13 lookup table. + + Takes a source grid containing categorical FBFM13 codes (from + `/grids/fbfm13/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM13 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_foliage` - Live foliage fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_foliage` - Live foliage fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM13 codes. Defaults to `\"fbfm13\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM13 Codes + + The source grid must contain only valid Anderson 13 fuel model codes. + The 18 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **Anderson 13 models**: 1–13 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (91-99) produce zero values for all bands. + - Fuel parameter values are from Anderson, Hal E. 1982. *Aids to + determining fuel models for estimating fire behavior.* USDA Forest + Service General Technical Report INT-122. + - All output values are in metric units (converted from Anderson 13 imperial values). + + Args: + domain_id (str): + body (CreateFbfm13LookupRequest): Request to create a grid by looking up FBFM13 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_fbfm40_lookup.py b/fastfuels_sdk/v2/client_library/api/grids/create_fbfm40_lookup.py new file mode 100644 index 0000000..7ef9db3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_fbfm40_lookup.py @@ -0,0 +1,416 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_fbfm_40_lookup_request import CreateFbfm40LookupRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateFbfm40LookupRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/lookup/fbfm40".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm40LookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FBFM40 fuel parameters + + # Create FBFM40 Lookup Grid + + Converts FBFM40 fuel model codes to fuel parameters using Scott-Burgan 40 + lookup tables. + + Takes a source grid containing categorical FBFM codes (from + `/grids/fbfm40/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM40 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_herb`, `fuel_load.live_woody` - Live fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_herb`, `savr.live_woody` - Live fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM codes. Defaults to `\"fbfm\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM40 Codes + + The source grid must contain only valid Scott-Burgan 40 fuel model codes. + The 46 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **GR** (grass): 101–109 + - **GS** (grass-shrub): 121–124 + - **SH** (shrub): 141–149 + - **TU** (timber-understory): 161–165 + - **TL** (timber litter): 181–189 + - **SB** (slash-blowdown): 201–204 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (NB1–NB9) produce zero values for all bands. + - All output values are in metric units (converted from SB40 imperial values). + + Args: + domain_id (str): + body (CreateFbfm40LookupRequest): Request to create a grid by looking up FBFM40 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm40LookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FBFM40 fuel parameters + + # Create FBFM40 Lookup Grid + + Converts FBFM40 fuel model codes to fuel parameters using Scott-Burgan 40 + lookup tables. + + Takes a source grid containing categorical FBFM codes (from + `/grids/fbfm40/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM40 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_herb`, `fuel_load.live_woody` - Live fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_herb`, `savr.live_woody` - Live fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM codes. Defaults to `\"fbfm\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM40 Codes + + The source grid must contain only valid Scott-Burgan 40 fuel model codes. + The 46 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **GR** (grass): 101–109 + - **GS** (grass-shrub): 121–124 + - **SH** (shrub): 141–149 + - **TU** (timber-understory): 161–165 + - **TL** (timber litter): 181–189 + - **SB** (slash-blowdown): 201–204 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (NB1–NB9) produce zero values for all bands. + - All output values are in metric units (converted from SB40 imperial values). + + Args: + domain_id (str): + body (CreateFbfm40LookupRequest): Request to create a grid by looking up FBFM40 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm40LookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FBFM40 fuel parameters + + # Create FBFM40 Lookup Grid + + Converts FBFM40 fuel model codes to fuel parameters using Scott-Burgan 40 + lookup tables. + + Takes a source grid containing categorical FBFM codes (from + `/grids/fbfm40/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM40 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_herb`, `fuel_load.live_woody` - Live fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_herb`, `savr.live_woody` - Live fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM codes. Defaults to `\"fbfm\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM40 Codes + + The source grid must contain only valid Scott-Burgan 40 fuel model codes. + The 46 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **GR** (grass): 101–109 + - **GS** (grass-shrub): 121–124 + - **SH** (shrub): 141–149 + - **TU** (timber-understory): 161–165 + - **TL** (timber litter): 181–189 + - **SB** (slash-blowdown): 201–204 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (NB1–NB9) produce zero values for all bands. + - All output values are in metric units (converted from SB40 imperial values). + + Args: + domain_id (str): + body (CreateFbfm40LookupRequest): Request to create a grid by looking up FBFM40 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFbfm40LookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FBFM40 fuel parameters + + # Create FBFM40 Lookup Grid + + Converts FBFM40 fuel model codes to fuel parameters using Scott-Burgan 40 + lookup tables. + + Takes a source grid containing categorical FBFM codes (from + `/grids/fbfm40/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FBFM40 codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.live_herb`, `fuel_load.live_woody` - Live fuel loads (kg/m**2) + - `savr.1hr`, `savr.10hr`, `savr.100hr` - Dead fuel SAV ratios (1/m) + - `savr.live_herb`, `savr.live_woody` - Live fuel SAV ratios (1/m) + - `fuel_depth` - Fuel bed depth (m) + - **source_band**: (optional) Band in source grid containing FBFM codes. Defaults to `\"fbfm\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Valid FBFM40 Codes + + The source grid must contain only valid Scott-Burgan 40 fuel model codes. + The 46 valid codes are: + + - **NB** (non-burnable): 91, 92, 93, 98, 99 + - **GR** (grass): 101–109 + - **GS** (grass-shrub): 121–124 + - **SH** (shrub): 141–149 + - **TU** (timber-understory): 161–165 + - **TL** (timber litter): 181–189 + - **SB** (slash-blowdown): 201–204 + + If any cell in the source grid contains a code not in this set (including 0 + or nodata), the job will fail with an `INVALID_FBFM_CODES` error listing + the invalid codes found. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - Non-burnable codes (NB1–NB9) produce zero values for all bands. + - All output values are in metric units (converted from SB40 imperial values). + + Args: + domain_id (str): + body (CreateFbfm40LookupRequest): Request to create a grid by looking up FBFM40 fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_fccs_lookup.py b/fastfuels_sdk/v2/client_library/api/grids/create_fccs_lookup.py new file mode 100644 index 0000000..d38fc88 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_fccs_lookup.py @@ -0,0 +1,464 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_fccs_lookup_request import CreateFccsLookupRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateFccsLookupRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/lookup/fccs".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFccsLookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FCCS fuel parameters + + # Create FCCS Lookup Grid + + Converts FCCS fuelbed codes to fuel parameters using the FOFEM FCCS + fuelbed lookup table (see the + [FOFEM/SpatialFOFEM FCCS lookup table](https://www.landfire.gov/sites/default/files/CSV/SpatialFOFEM + _FCCS_Formatted_TS_06-27-24.csv), + the USDA Forest Service data source this endpoint converts). + + Takes a source grid containing categorical FCCS codes (from + `/grids/fccs/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FCCS codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.litter`, `fuel_load.duff` - Ground fuel loads (kg/m**2) + - `duff_depth` - Duff layer depth (m) + - `fuel_load.live_shrub`, `fuel_load.live_herb` - Live surface fuel loads (kg/m**2) + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.1000hr_sound`, `fuel_load.1000hr_rotten` - Dead fuel loads, >3in. diameter (kg/m**2) + - `fuel_load.live_foliage`, `fuel_load.live_branch` - Live crown fuel loads (kg/m**2) + - **source_band**: (optional) Band in source grid containing FCCS codes. Defaults to `\"fccs\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Band Coverage + + These 12 bands are a starting subset of what FOFEM provides, not the + full table. `fuel_load.1000hr_sound` and `fuel_load.1000hr_rotten` + are each calculated by summing three FOFEM size-class columns + (3-9 in., 9-20 in., 20+ in.) rather than mapping to a single source + column. FOFEM also provides finer sound/rotten size-class + breakdowns, a cover-group code, and emission factors that aren't + exposed as bands here — additional bands can be added on request. + + ## Valid FCCS Codes + + Each `FCCS` code is a synthetic key: `base * 10_000 + suffix`, where + `base` is the `FCCSID` fuelbed number and the 3-digit `suffix` + encodes an FCCS Potential rating (Fire Behavior / Crown Fire / + Available Fuel Potential, each a 0-9 digit) per the [Fuel Characteristic + Classification System Version 3.0: Technical Documentation (PNW- + GTR-887)](https://www.fs.usda.gov/pnw/pubs/pnw_gtr887.pdf). + + The source grid must contain FCCS codes whose base fuelbed number matches + a recognized `FCCSID`. A code with a valid base but no matching row in the + FOFEM lookup table is not an error. It's a fuelbed/fire-potential combination + the table doesn't cover, so its output is `NaN` for every band, and a + progress warning lists these codes. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - All output values are in metric units (converted from FOFEM imperial + values). + + Args: + domain_id (str): + body (CreateFccsLookupRequest): Request to create a grid by looking up FCCS fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFccsLookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FCCS fuel parameters + + # Create FCCS Lookup Grid + + Converts FCCS fuelbed codes to fuel parameters using the FOFEM FCCS + fuelbed lookup table (see the + [FOFEM/SpatialFOFEM FCCS lookup table](https://www.landfire.gov/sites/default/files/CSV/SpatialFOFEM + _FCCS_Formatted_TS_06-27-24.csv), + the USDA Forest Service data source this endpoint converts). + + Takes a source grid containing categorical FCCS codes (from + `/grids/fccs/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FCCS codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.litter`, `fuel_load.duff` - Ground fuel loads (kg/m**2) + - `duff_depth` - Duff layer depth (m) + - `fuel_load.live_shrub`, `fuel_load.live_herb` - Live surface fuel loads (kg/m**2) + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.1000hr_sound`, `fuel_load.1000hr_rotten` - Dead fuel loads, >3in. diameter (kg/m**2) + - `fuel_load.live_foliage`, `fuel_load.live_branch` - Live crown fuel loads (kg/m**2) + - **source_band**: (optional) Band in source grid containing FCCS codes. Defaults to `\"fccs\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Band Coverage + + These 12 bands are a starting subset of what FOFEM provides, not the + full table. `fuel_load.1000hr_sound` and `fuel_load.1000hr_rotten` + are each calculated by summing three FOFEM size-class columns + (3-9 in., 9-20 in., 20+ in.) rather than mapping to a single source + column. FOFEM also provides finer sound/rotten size-class + breakdowns, a cover-group code, and emission factors that aren't + exposed as bands here — additional bands can be added on request. + + ## Valid FCCS Codes + + Each `FCCS` code is a synthetic key: `base * 10_000 + suffix`, where + `base` is the `FCCSID` fuelbed number and the 3-digit `suffix` + encodes an FCCS Potential rating (Fire Behavior / Crown Fire / + Available Fuel Potential, each a 0-9 digit) per the [Fuel Characteristic + Classification System Version 3.0: Technical Documentation (PNW- + GTR-887)](https://www.fs.usda.gov/pnw/pubs/pnw_gtr887.pdf). + + The source grid must contain FCCS codes whose base fuelbed number matches + a recognized `FCCSID`. A code with a valid base but no matching row in the + FOFEM lookup table is not an error. It's a fuelbed/fire-potential combination + the table doesn't cover, so its output is `NaN` for every band, and a + progress warning lists these codes. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - All output values are in metric units (converted from FOFEM imperial + values). + + Args: + domain_id (str): + body (CreateFccsLookupRequest): Request to create a grid by looking up FCCS fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFccsLookupRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by looking up FCCS fuel parameters + + # Create FCCS Lookup Grid + + Converts FCCS fuelbed codes to fuel parameters using the FOFEM FCCS + fuelbed lookup table (see the + [FOFEM/SpatialFOFEM FCCS lookup table](https://www.landfire.gov/sites/default/files/CSV/SpatialFOFEM + _FCCS_Formatted_TS_06-27-24.csv), + the USDA Forest Service data source this endpoint converts). + + Takes a source grid containing categorical FCCS codes (from + `/grids/fccs/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FCCS codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.litter`, `fuel_load.duff` - Ground fuel loads (kg/m**2) + - `duff_depth` - Duff layer depth (m) + - `fuel_load.live_shrub`, `fuel_load.live_herb` - Live surface fuel loads (kg/m**2) + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.1000hr_sound`, `fuel_load.1000hr_rotten` - Dead fuel loads, >3in. diameter (kg/m**2) + - `fuel_load.live_foliage`, `fuel_load.live_branch` - Live crown fuel loads (kg/m**2) + - **source_band**: (optional) Band in source grid containing FCCS codes. Defaults to `\"fccs\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Band Coverage + + These 12 bands are a starting subset of what FOFEM provides, not the + full table. `fuel_load.1000hr_sound` and `fuel_load.1000hr_rotten` + are each calculated by summing three FOFEM size-class columns + (3-9 in., 9-20 in., 20+ in.) rather than mapping to a single source + column. FOFEM also provides finer sound/rotten size-class + breakdowns, a cover-group code, and emission factors that aren't + exposed as bands here — additional bands can be added on request. + + ## Valid FCCS Codes + + Each `FCCS` code is a synthetic key: `base * 10_000 + suffix`, where + `base` is the `FCCSID` fuelbed number and the 3-digit `suffix` + encodes an FCCS Potential rating (Fire Behavior / Crown Fire / + Available Fuel Potential, each a 0-9 digit) per the [Fuel Characteristic + Classification System Version 3.0: Technical Documentation (PNW- + GTR-887)](https://www.fs.usda.gov/pnw/pubs/pnw_gtr887.pdf). + + The source grid must contain FCCS codes whose base fuelbed number matches + a recognized `FCCSID`. A code with a valid base but no matching row in the + FOFEM lookup table is not an error. It's a fuelbed/fire-potential combination + the table doesn't cover, so its output is `NaN` for every band, and a + progress warning lists these codes. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - All output values are in metric units (converted from FOFEM imperial + values). + + Args: + domain_id (str): + body (CreateFccsLookupRequest): Request to create a grid by looking up FCCS fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateFccsLookupRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by looking up FCCS fuel parameters + + # Create FCCS Lookup Grid + + Converts FCCS fuelbed codes to fuel parameters using the FOFEM FCCS + fuelbed lookup table (see the + [FOFEM/SpatialFOFEM FCCS lookup table](https://www.landfire.gov/sites/default/files/CSV/SpatialFOFEM + _FCCS_Formatted_TS_06-27-24.csv), + the USDA Forest Service data source this endpoint converts). + + Takes a source grid containing categorical FCCS codes (from + `/grids/fccs/landfire`) and produces a new grid with the requested + continuous fuel parameters. + + ## Request Body + + - **source_grid_id**: (required) Grid containing FCCS codes. + - **bands**: (required) Bands to look up. Valid values: + - `fuel_load.litter`, `fuel_load.duff` - Ground fuel loads (kg/m**2) + - `duff_depth` - Duff layer depth (m) + - `fuel_load.live_shrub`, `fuel_load.live_herb` - Live surface fuel loads (kg/m**2) + - `fuel_load.1hr`, `fuel_load.10hr`, `fuel_load.100hr` - Dead fuel loads (kg/m**2) + - `fuel_load.1000hr_sound`, `fuel_load.1000hr_rotten` - Dead fuel loads, >3in. diameter (kg/m**2) + - `fuel_load.live_foliage`, `fuel_load.live_branch` - Live crown fuel loads (kg/m**2) + - **source_band**: (optional) Band in source grid containing FCCS codes. Defaults to `\"fccs\"`. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Band Coverage + + These 12 bands are a starting subset of what FOFEM provides, not the + full table. `fuel_load.1000hr_sound` and `fuel_load.1000hr_rotten` + are each calculated by summing three FOFEM size-class columns + (3-9 in., 9-20 in., 20+ in.) rather than mapping to a single source + column. FOFEM also provides finer sound/rotten size-class + breakdowns, a cover-group code, and emission factors that aren't + exposed as bands here — additional bands can be added on request. + + ## Valid FCCS Codes + + Each `FCCS` code is a synthetic key: `base * 10_000 + suffix`, where + `base` is the `FCCSID` fuelbed number and the 3-digit `suffix` + encodes an FCCS Potential rating (Fire Behavior / Crown Fire / + Available Fuel Potential, each a 0-9 digit) per the [Fuel Characteristic + Classification System Version 3.0: Technical Documentation (PNW- + GTR-887)](https://www.fs.usda.gov/pnw/pubs/pnw_gtr887.pdf). + + The source grid must contain FCCS codes whose base fuelbed number matches + a recognized `FCCSID`. A code with a valid base but no matching row in the + FOFEM lookup table is not an error. It's a fuelbed/fire-potential combination + the table doesn't cover, so its output is `NaN` for every band, and a + progress warning lists these codes. + + ## Response + + Returns the created Grid with status \"pending\". The backend applies the + lookup transformation and updates status to \"completed\" when ready. + + ## Notes + + - Domain is propagated from the source grid (derived grids carry the + same domain reference as their source). + - The output grid inherits georeference from the source grid. + - All output values are in metric units (converted from FOFEM imperial + values). + + Args: + domain_id (str): + body (CreateFccsLookupRequest): Request to create a grid by looking up FCCS fuel + parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_geotiff_upload.py b/fastfuels_sdk/v2/client_library/api/grids/create_geotiff_upload.py new file mode 100644 index 0000000..3a64c1a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_geotiff_upload.py @@ -0,0 +1,344 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_geo_tiff_upload_request import CreateGeoTIFFUploadRequest +from ...models.grid_upload_created_response import GridUploadCreatedResponse +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateGeoTIFFUploadRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/upload/geotiff".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = GridUploadCreatedResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGeoTIFFUploadRequest, +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from a direct GeoTIFF upload + + # Create Upload Grid (GeoTIFF) + + Creates a grid resource and returns a signed URL for uploading a GeoTIFF + directly to GCS. Upload with HTTP PUT, sending **every header in the + response's `upload.headers`** exactly as given — the signed URL commits to + them, and the upload is rejected if any is missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.tif -H \"Content-Type: image/tiff\" -H \"x-goog-content- + length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Band Definitions + + The `bands` array maps 1:1 to GeoTIFF raster bands in order: + `bands[0]` → GeoTIFF band 1, `bands[1]` → GeoTIFF band 2, etc. + Each band key becomes a variable name in the output Zarr store. + + ## CRS Handling + + The GeoTIFF must have a CRS set and must match the domain CRS. A mismatch + fails with `CRS_MISMATCH`; reproject the GeoTIFF (e.g., `gdalwarp -t_srs`) + before uploading. + + ## Buffer Cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain extent + in the stored grid. The uploaded GeoTIFF must cover the domain bbox + expanded by `num_buffer_cells * native_pixel_size` on each side; pixels + beyond that expanded extent are clipped away. + + ## File requirements + + Single or multi-band GeoTIFF (`.tif`, `.tiff`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateGeoTIFFUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGeoTIFFUploadRequest, +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from a direct GeoTIFF upload + + # Create Upload Grid (GeoTIFF) + + Creates a grid resource and returns a signed URL for uploading a GeoTIFF + directly to GCS. Upload with HTTP PUT, sending **every header in the + response's `upload.headers`** exactly as given — the signed URL commits to + them, and the upload is rejected if any is missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.tif -H \"Content-Type: image/tiff\" -H \"x-goog-content- + length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Band Definitions + + The `bands` array maps 1:1 to GeoTIFF raster bands in order: + `bands[0]` → GeoTIFF band 1, `bands[1]` → GeoTIFF band 2, etc. + Each band key becomes a variable name in the output Zarr store. + + ## CRS Handling + + The GeoTIFF must have a CRS set and must match the domain CRS. A mismatch + fails with `CRS_MISMATCH`; reproject the GeoTIFF (e.g., `gdalwarp -t_srs`) + before uploading. + + ## Buffer Cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain extent + in the stored grid. The uploaded GeoTIFF must cover the domain bbox + expanded by `num_buffer_cells * native_pixel_size` on each side; pixels + beyond that expanded extent are clipped away. + + ## File requirements + + Single or multi-band GeoTIFF (`.tif`, `.tiff`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateGeoTIFFUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGeoTIFFUploadRequest, +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from a direct GeoTIFF upload + + # Create Upload Grid (GeoTIFF) + + Creates a grid resource and returns a signed URL for uploading a GeoTIFF + directly to GCS. Upload with HTTP PUT, sending **every header in the + response's `upload.headers`** exactly as given — the signed URL commits to + them, and the upload is rejected if any is missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.tif -H \"Content-Type: image/tiff\" -H \"x-goog-content- + length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Band Definitions + + The `bands` array maps 1:1 to GeoTIFF raster bands in order: + `bands[0]` → GeoTIFF band 1, `bands[1]` → GeoTIFF band 2, etc. + Each band key becomes a variable name in the output Zarr store. + + ## CRS Handling + + The GeoTIFF must have a CRS set and must match the domain CRS. A mismatch + fails with `CRS_MISMATCH`; reproject the GeoTIFF (e.g., `gdalwarp -t_srs`) + before uploading. + + ## Buffer Cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain extent + in the stored grid. The uploaded GeoTIFF must cover the domain bbox + expanded by `num_buffer_cells * native_pixel_size` on each side; pixels + beyond that expanded extent are clipped away. + + ## File requirements + + Single or multi-band GeoTIFF (`.tif`, `.tiff`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateGeoTIFFUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGeoTIFFUploadRequest, +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from a direct GeoTIFF upload + + # Create Upload Grid (GeoTIFF) + + Creates a grid resource and returns a signed URL for uploading a GeoTIFF + directly to GCS. Upload with HTTP PUT, sending **every header in the + response's `upload.headers`** exactly as given — the signed URL commits to + them, and the upload is rejected if any is missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.tif -H \"Content-Type: image/tiff\" -H \"x-goog-content- + length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Band Definitions + + The `bands` array maps 1:1 to GeoTIFF raster bands in order: + `bands[0]` → GeoTIFF band 1, `bands[1]` → GeoTIFF band 2, etc. + Each band key becomes a variable name in the output Zarr store. + + ## CRS Handling + + The GeoTIFF must have a CRS set and must match the domain CRS. A mismatch + fails with `CRS_MISMATCH`; reproject the GeoTIFF (e.g., `gdalwarp -t_srs`) + before uploading. + + ## Buffer Cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain extent + in the stored grid. The uploaded GeoTIFF must cover the domain bbox + expanded by `num_buffer_cells * native_pixel_size` on each side; pixels + beyond that expanded extent are clipped away. + + ## File requirements + + Single or multi-band GeoTIFF (`.tif`, `.tiff`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateGeoTIFFUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_grid_export.py b/fastfuels_sdk/v2/client_library/api/grids/create_grid_export.py new file mode 100644 index 0000000..090de14 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_grid_export.py @@ -0,0 +1,281 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.export_grid_request import ExportGridRequest +from ...models.grid_export_format import GridExportFormat +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, + format_: GridExportFormat, + *, + body: ExportGridRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/{grid_id}/exports/{format_}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + format_=quote(str(format_), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Export.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + format_: GridExportFormat, + *, + client: AuthenticatedClient, + body: ExportGridRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export a grid + + Export a grid to the specified format. + + Supported formats: `geotiff`, `zarr` (zipped), `netcdf` (CF-1.13). + `geotiff` supports 2D grids only; use `netcdf` or `zarr` for 3D voxel + grids. + + The grid must belong to this domain and have status `completed`. + If `bands` is specified, only those bands are included; otherwise + all bands are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + grid_id (str): + format_ (GridExportFormat): Supported grid export formats. + body (ExportGridRequest): Request body for creating a grid export. + + Used at: POST /domains/{domain_id}/grids/{grid_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + format_=format_, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + format_: GridExportFormat, + *, + client: AuthenticatedClient, + body: ExportGridRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export a grid + + Export a grid to the specified format. + + Supported formats: `geotiff`, `zarr` (zipped), `netcdf` (CF-1.13). + `geotiff` supports 2D grids only; use `netcdf` or `zarr` for 3D voxel + grids. + + The grid must belong to this domain and have status `completed`. + If `bands` is specified, only those bands are included; otherwise + all bands are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + grid_id (str): + format_ (GridExportFormat): Supported grid export formats. + body (ExportGridRequest): Request body for creating a grid export. + + Used at: POST /domains/{domain_id}/grids/{grid_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + format_=format_, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + format_: GridExportFormat, + *, + client: AuthenticatedClient, + body: ExportGridRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export a grid + + Export a grid to the specified format. + + Supported formats: `geotiff`, `zarr` (zipped), `netcdf` (CF-1.13). + `geotiff` supports 2D grids only; use `netcdf` or `zarr` for 3D voxel + grids. + + The grid must belong to this domain and have status `completed`. + If `bands` is specified, only those bands are included; otherwise + all bands are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + grid_id (str): + format_ (GridExportFormat): Supported grid export formats. + body (ExportGridRequest): Request body for creating a grid export. + + Used at: POST /domains/{domain_id}/grids/{grid_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + format_=format_, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + format_: GridExportFormat, + *, + client: AuthenticatedClient, + body: ExportGridRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export a grid + + Export a grid to the specified format. + + Supported formats: `geotiff`, `zarr` (zipped), `netcdf` (CF-1.13). + `geotiff` supports 2D grids only; use `netcdf` or `zarr` for 3D voxel + grids. + + The grid must belong to this domain and have status `completed`. + If `bands` is specified, only those bands are included; otherwise + all bands are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + grid_id (str): + format_ (GridExportFormat): Supported grid export formats. + body (ExportGridRequest): Request body for creating a grid export. + + Used at: POST /domains/{domain_id}/grids/{grid_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + format_=format_, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landfire_canopy.py b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_canopy.py new file mode 100644 index 0000000..57122c7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_canopy.py @@ -0,0 +1,336 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_landfire_canopy_request import CreateLandfireCanopyRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLandfireCanopyRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/canopy/landfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireCanopyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE canopy data + + # Create LANDFIRE Canopy Grid + + Creates a grid with canopy fuel data from LANDFIRE at 30m resolution + (CONUS). + + Available bands: + - **chm**: canopy height in meters + - **cbd**: canopy bulk density in kg/m**3 + - **cbh**: canopy base height in meters + - **cc**: canopy cover in percent (0-100) + + By default all four bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all four. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireCanopyRequest): Request to create a grid from LANDFIRE canopy data. + + Returns a grid with one or more continuous canopy bands at 30m + resolution (CONUS): + - chm: Canopy height (m) + - cbd: Canopy bulk density (kg/m**3) + - cbh: Canopy base height (m) + - cc: Canopy cover (%) + + Bands are validated against the canopy band vocabulary and may not be + duplicated. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireCanopyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE canopy data + + # Create LANDFIRE Canopy Grid + + Creates a grid with canopy fuel data from LANDFIRE at 30m resolution + (CONUS). + + Available bands: + - **chm**: canopy height in meters + - **cbd**: canopy bulk density in kg/m**3 + - **cbh**: canopy base height in meters + - **cc**: canopy cover in percent (0-100) + + By default all four bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all four. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireCanopyRequest): Request to create a grid from LANDFIRE canopy data. + + Returns a grid with one or more continuous canopy bands at 30m + resolution (CONUS): + - chm: Canopy height (m) + - cbd: Canopy bulk density (kg/m**3) + - cbh: Canopy base height (m) + - cc: Canopy cover (%) + + Bands are validated against the canopy band vocabulary and may not be + duplicated. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireCanopyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE canopy data + + # Create LANDFIRE Canopy Grid + + Creates a grid with canopy fuel data from LANDFIRE at 30m resolution + (CONUS). + + Available bands: + - **chm**: canopy height in meters + - **cbd**: canopy bulk density in kg/m**3 + - **cbh**: canopy base height in meters + - **cc**: canopy cover in percent (0-100) + + By default all four bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all four. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireCanopyRequest): Request to create a grid from LANDFIRE canopy data. + + Returns a grid with one or more continuous canopy bands at 30m + resolution (CONUS): + - chm: Canopy height (m) + - cbd: Canopy bulk density (kg/m**3) + - cbh: Canopy base height (m) + - cc: Canopy cover (%) + + Bands are validated against the canopy band vocabulary and may not be + duplicated. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireCanopyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE canopy data + + # Create LANDFIRE Canopy Grid + + Creates a grid with canopy fuel data from LANDFIRE at 30m resolution + (CONUS). + + Available bands: + - **chm**: canopy height in meters + - **cbd**: canopy bulk density in kg/m**3 + - **cbh**: canopy base height in meters + - **cc**: canopy cover in percent (0-100) + + By default all four bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all four. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireCanopyRequest): Request to create a grid from LANDFIRE canopy data. + + Returns a grid with one or more continuous canopy bands at 30m + resolution (CONUS): + - chm: Canopy height (m) + - cbd: Canopy bulk density (kg/m**3) + - cbh: Canopy base height (m) + - cc: Canopy cover (%) + + Bands are validated against the canopy band vocabulary and may not be + duplicated. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm13.py b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm13.py new file mode 100644 index 0000000..5e5c6b9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm13.py @@ -0,0 +1,288 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_landfire_fbfm_13_request import CreateLandfireFbfm13Request +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLandfireFbfm13Request, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/fbfm13/landfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm13Request, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FBFM13 + + # Create LANDFIRE FBFM13 Grid + + Creates a grid with FBFM13 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm13`) with Anderson 13 + fuel model codes (1-13). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, + depth), use the `/grids/lookup/fbfm13` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm13Request): Request to create a grid from LANDFIRE FBFM13. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm13. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm13Request, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FBFM13 + + # Create LANDFIRE FBFM13 Grid + + Creates a grid with FBFM13 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm13`) with Anderson 13 + fuel model codes (1-13). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, + depth), use the `/grids/lookup/fbfm13` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm13Request): Request to create a grid from LANDFIRE FBFM13. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm13. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm13Request, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FBFM13 + + # Create LANDFIRE FBFM13 Grid + + Creates a grid with FBFM13 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm13`) with Anderson 13 + fuel model codes (1-13). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, + depth), use the `/grids/lookup/fbfm13` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm13Request): Request to create a grid from LANDFIRE FBFM13. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm13. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm13Request, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FBFM13 + + # Create LANDFIRE FBFM13 Grid + + Creates a grid with FBFM13 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm13`) with Anderson 13 + fuel model codes (1-13). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, + depth), use the `/grids/lookup/fbfm13` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm13Request): Request to create a grid from LANDFIRE FBFM13. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm13. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm40.py b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm40.py new file mode 100644 index 0000000..c873764 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fbfm40.py @@ -0,0 +1,288 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_landfire_fbfm_40_request import CreateLandfireFbfm40Request +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLandfireFbfm40Request, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/fbfm40/landfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm40Request, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FBFM40 + + # Create LANDFIRE FBFM40 Grid + + Creates a grid with FBFM40 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm`) with Scott-Burgan 40 + fuel model codes (e.g., GR1, TL3, SH5). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fbfm40` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm40Request): Request to create a grid from LANDFIRE FBFM40. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm40. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm40Request, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FBFM40 + + # Create LANDFIRE FBFM40 Grid + + Creates a grid with FBFM40 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm`) with Scott-Burgan 40 + fuel model codes (e.g., GR1, TL3, SH5). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fbfm40` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm40Request): Request to create a grid from LANDFIRE FBFM40. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm40. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm40Request, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FBFM40 + + # Create LANDFIRE FBFM40 Grid + + Creates a grid with FBFM40 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm`) with Scott-Burgan 40 + fuel model codes (e.g., GR1, TL3, SH5). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fbfm40` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm40Request): Request to create a grid from LANDFIRE FBFM40. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm40. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFbfm40Request, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FBFM40 + + # Create LANDFIRE FBFM40 Grid + + Creates a grid with FBFM40 fuel model codes from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fbfm`) with Scott-Burgan 40 + fuel model codes (e.g., GR1, TL3, SH5). + + To convert fuel model codes to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fbfm40` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2024\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFbfm40Request): Request to create a grid from LANDFIRE FBFM40. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm40. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fccs.py b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fccs.py new file mode 100644 index 0000000..9c254d9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_fccs.py @@ -0,0 +1,296 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_landfire_fccs_request import CreateLandfireFccsRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLandfireFccsRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/fccs/landfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFccsRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FCCS + + # Create LANDFIRE FCCS Grid + + Creates a grid with FCCS fuelbed IDs from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fccs`) with fuel + classification system fuelbed IDs (e.g., 26, 598, 34721). + + To convert fuelbed IDs to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fccs` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2023\". + - **remove_bare_ground**: (optional) Remove bare ground cells (fuelbed ID 0), + replaced by neighboring majority. Default: False. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFccsRequest): Request to create a grid from LANDFIRE FCCS. + + Returns a single-band grid with categorical fuelbed IDs. + To convert IDs to fuel parameters, use /grids/lookup/fccs. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFccsRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FCCS + + # Create LANDFIRE FCCS Grid + + Creates a grid with FCCS fuelbed IDs from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fccs`) with fuel + classification system fuelbed IDs (e.g., 26, 598, 34721). + + To convert fuelbed IDs to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fccs` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2023\". + - **remove_bare_ground**: (optional) Remove bare ground cells (fuelbed ID 0), + replaced by neighboring majority. Default: False. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFccsRequest): Request to create a grid from LANDFIRE FCCS. + + Returns a single-band grid with categorical fuelbed IDs. + To convert IDs to fuel parameters, use /grids/lookup/fccs. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFccsRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE FCCS + + # Create LANDFIRE FCCS Grid + + Creates a grid with FCCS fuelbed IDs from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fccs`) with fuel + classification system fuelbed IDs (e.g., 26, 598, 34721). + + To convert fuelbed IDs to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fccs` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2023\". + - **remove_bare_ground**: (optional) Remove bare ground cells (fuelbed ID 0), + replaced by neighboring majority. Default: False. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFccsRequest): Request to create a grid from LANDFIRE FCCS. + + Returns a single-band grid with categorical fuelbed IDs. + To convert IDs to fuel parameters, use /grids/lookup/fccs. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireFccsRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE FCCS + + # Create LANDFIRE FCCS Grid + + Creates a grid with FCCS fuelbed IDs from LANDFIRE at 30m resolution. + + The grid contains a single categorical band (`fccs`) with fuel + classification system fuelbed IDs (e.g., 26, 598, 34721). + + To convert fuelbed IDs to fuel parameters (fuel loads, SAV, depth), + use the `/grids/lookup/fccs` endpoint. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2023\". + - **remove_bare_ground**: (optional) Remove bare ground cells (fuelbed ID 0), + replaced by neighboring majority. Default: False. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireFccsRequest): Request to create a grid from LANDFIRE FCCS. + + Returns a single-band grid with categorical fuelbed IDs. + To convert IDs to fuel parameters, use /grids/lookup/fccs. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landfire_topography.py b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_topography.py new file mode 100644 index 0000000..824da4c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landfire_topography.py @@ -0,0 +1,304 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_landfire_topography_request import CreateLandfireTopographyRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLandfireTopographyRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/topography/landfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireTopographyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE topographic data + + # Create LANDFIRE Topography Grid + + Creates a grid with topographic data from LANDFIRE at 30m resolution. + + Available bands: + - **elevation**: meters above sea level + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + By default all three bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all three. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2020\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireTopographyRequest): Request to create a grid from LANDFIRE topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireTopographyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE topographic data + + # Create LANDFIRE Topography Grid + + Creates a grid with topographic data from LANDFIRE at 30m resolution. + + Available bands: + - **elevation**: meters above sea level + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + By default all three bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all three. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2020\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireTopographyRequest): Request to create a grid from LANDFIRE topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireTopographyRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from LANDFIRE topographic data + + # Create LANDFIRE Topography Grid + + Creates a grid with topographic data from LANDFIRE at 30m resolution. + + Available bands: + - **elevation**: meters above sea level + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + By default all three bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all three. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2020\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireTopographyRequest): Request to create a grid from LANDFIRE topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLandfireTopographyRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from LANDFIRE topographic data + + # Create LANDFIRE Topography Grid + + Creates a grid with topographic data from LANDFIRE at 30m resolution. + + Available bands: + - **elevation**: meters above sea level + - **slope**: degrees (0-90) + - **aspect**: degrees clockwise from north (0-360) + + By default all three bands are included. Use the `bands` field to select + a subset. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: all three. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) LANDFIRE version. Default: \"2020\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateLandfireTopographyRequest): Request to create a grid from LANDFIRE topographic + data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_landscape_export.py b/fastfuels_sdk/v2/client_library/api/grids/create_landscape_export.py new file mode 100644 index 0000000..7a6e793 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_landscape_export.py @@ -0,0 +1,296 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.http_validation_error import HTTPValidationError +from ...models.landscape_export_request import LandscapeExportRequest +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: LandscapeExportRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/exports/landscape".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Export.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: LandscapeExportRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export terrain + fuel + canopy grids to a landscape GeoTIFF + + Assemble terrain, surface fuel model, and canopy grids into an + 8-band LANDFIRE-style landscape GeoTIFF for operational fire behavior + tools (FlamMap, IFTDSS, WFDSS). + + The output `landscape.tif` carries the standard LANDFIRE band order — + elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density — with LANDFIRE's int16 scaled + encodings, embedded CRS, and per-band name/unit metadata. This is the + format LANDFIRE distributes and IFTDSS accepts for upload. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (LandscapeExportRequest): Request body for creating a landscape export. + + Eight required roles produce an 8-band landscape GeoTIFF in LANDFIRE band + order: elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density. This is the shape modern fire + behavior tools consume — IFTDSS requires all eight bands for upload. + + The landscape lattice is defined by the `alignment` field — either the + Domain bounding box tiled at `resolution` (default 30 m, LANDFIRE-native), + or the lattice of an existing grid. Every role grid must be lattice-aligned + to the landscape and cover its full extent; otherwise the request is + rejected with 422. The exporter only crops oversized roles by integer + slicing — it never resamples or reprojects. To change a grid's resolution + or anchor, use `POST /v2/domains/{domain_id}/grids/{grid_id}/resample`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: LandscapeExportRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export terrain + fuel + canopy grids to a landscape GeoTIFF + + Assemble terrain, surface fuel model, and canopy grids into an + 8-band LANDFIRE-style landscape GeoTIFF for operational fire behavior + tools (FlamMap, IFTDSS, WFDSS). + + The output `landscape.tif` carries the standard LANDFIRE band order — + elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density — with LANDFIRE's int16 scaled + encodings, embedded CRS, and per-band name/unit metadata. This is the + format LANDFIRE distributes and IFTDSS accepts for upload. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (LandscapeExportRequest): Request body for creating a landscape export. + + Eight required roles produce an 8-band landscape GeoTIFF in LANDFIRE band + order: elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density. This is the shape modern fire + behavior tools consume — IFTDSS requires all eight bands for upload. + + The landscape lattice is defined by the `alignment` field — either the + Domain bounding box tiled at `resolution` (default 30 m, LANDFIRE-native), + or the lattice of an existing grid. Every role grid must be lattice-aligned + to the landscape and cover its full extent; otherwise the request is + rejected with 422. The exporter only crops oversized roles by integer + slicing — it never resamples or reprojects. To change a grid's resolution + or anchor, use `POST /v2/domains/{domain_id}/grids/{grid_id}/resample`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: LandscapeExportRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export terrain + fuel + canopy grids to a landscape GeoTIFF + + Assemble terrain, surface fuel model, and canopy grids into an + 8-band LANDFIRE-style landscape GeoTIFF for operational fire behavior + tools (FlamMap, IFTDSS, WFDSS). + + The output `landscape.tif` carries the standard LANDFIRE band order — + elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density — with LANDFIRE's int16 scaled + encodings, embedded CRS, and per-band name/unit metadata. This is the + format LANDFIRE distributes and IFTDSS accepts for upload. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (LandscapeExportRequest): Request body for creating a landscape export. + + Eight required roles produce an 8-band landscape GeoTIFF in LANDFIRE band + order: elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density. This is the shape modern fire + behavior tools consume — IFTDSS requires all eight bands for upload. + + The landscape lattice is defined by the `alignment` field — either the + Domain bounding box tiled at `resolution` (default 30 m, LANDFIRE-native), + or the lattice of an existing grid. Every role grid must be lattice-aligned + to the landscape and cover its full extent; otherwise the request is + rejected with 422. The exporter only crops oversized roles by integer + slicing — it never resamples or reprojects. To change a grid's resolution + or anchor, use `POST /v2/domains/{domain_id}/grids/{grid_id}/resample`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: LandscapeExportRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export terrain + fuel + canopy grids to a landscape GeoTIFF + + Assemble terrain, surface fuel model, and canopy grids into an + 8-band LANDFIRE-style landscape GeoTIFF for operational fire behavior + tools (FlamMap, IFTDSS, WFDSS). + + The output `landscape.tif` carries the standard LANDFIRE band order — + elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density — with LANDFIRE's int16 scaled + encodings, embedded CRS, and per-band name/unit metadata. This is the + format LANDFIRE distributes and IFTDSS accepts for upload. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (LandscapeExportRequest): Request body for creating a landscape export. + + Eight required roles produce an 8-band landscape GeoTIFF in LANDFIRE band + order: elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density. This is the shape modern fire + behavior tools consume — IFTDSS requires all eight bands for upload. + + The landscape lattice is defined by the `alignment` field — either the + Domain bounding box tiled at `resolution` (default 30 m, LANDFIRE-native), + or the lattice of an existing grid. Every role grid must be lattice-aligned + to the landscape and cover its full extent; otherwise the request is + rejected with 422. The exporter only crops oversized roles by integer + slicing — it never resamples or reprojects. To change a grid's resolution + or anchor, use `POST /v2/domains/{domain_id}/grids/{grid_id}/resample`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_layerset_rasterize.py b/fastfuels_sdk/v2/client_library/api/grids/create_layerset_rasterize.py new file mode 100644 index 0000000..37b7f4a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_layerset_rasterize.py @@ -0,0 +1,320 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_layerset_rasterize_request import CreateLayersetRasterizeRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateLayersetRasterizeRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/rasterize/layerset".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRasterizeRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + """Create a grid by rasterizing a layerset + + # Create Layerset-Rasterized Grid + + Rasterizes a previously-uploaded fuelbed layerset into a grid aligned + to the domain (default) or to a target grid. + + The `layerset_id` must reference a Feature uploaded for this domain via + `POST /domains/{domain_id}/features/layerset` and owned by the caller. + + ## Request Body + + - **layerset_id**: (required) Feature ID of the layerset to rasterize. + - **overlap_method**: (optional) Per-cell reduction when polygons of the + same `fuel_type` overlap a single cell. One of `mean`, `max`, `min`. + Default: `mean`. (Loading is always summed across overlapping polygons + regardless of this setting.) + - **alignment**: (optional) See alignment docs. Default: anchored to domain. + - **extent_buffer_cells**: (optional) Buffer in result-grid cells around + the domain extent. Cells inside the buffered extent that fall outside + polygon coverage are populated with the rasterizer's fill value. + - **name**, **description**, **tags**, **modifications**: standard grid metadata. + + ## Response + + Returns the created Grid resource with status `pending`. The backend + fetches the layerset GeoJSON from GCS, rasterizes it, and updates the + status to `completed` when ready. + + Args: + domain_id (str): + body (CreateLayersetRasterizeRequest): Request to create a grid by rasterizing a + previously-uploaded layerset. + + The referenced layerset must be an existing Feature owned by the caller, + uploaded via ``POST /domains/{id}/features/layerset``. The worker fetches + the GeoJSON from GCS at job time; a fresh upload produces a new + ``feature_id``, so the reference is effectively immutable. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRasterizeRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + """Create a grid by rasterizing a layerset + + # Create Layerset-Rasterized Grid + + Rasterizes a previously-uploaded fuelbed layerset into a grid aligned + to the domain (default) or to a target grid. + + The `layerset_id` must reference a Feature uploaded for this domain via + `POST /domains/{domain_id}/features/layerset` and owned by the caller. + + ## Request Body + + - **layerset_id**: (required) Feature ID of the layerset to rasterize. + - **overlap_method**: (optional) Per-cell reduction when polygons of the + same `fuel_type` overlap a single cell. One of `mean`, `max`, `min`. + Default: `mean`. (Loading is always summed across overlapping polygons + regardless of this setting.) + - **alignment**: (optional) See alignment docs. Default: anchored to domain. + - **extent_buffer_cells**: (optional) Buffer in result-grid cells around + the domain extent. Cells inside the buffered extent that fall outside + polygon coverage are populated with the rasterizer's fill value. + - **name**, **description**, **tags**, **modifications**: standard grid metadata. + + ## Response + + Returns the created Grid resource with status `pending`. The backend + fetches the layerset GeoJSON from GCS, rasterizes it, and updates the + status to `completed` when ready. + + Args: + domain_id (str): + body (CreateLayersetRasterizeRequest): Request to create a grid by rasterizing a + previously-uploaded layerset. + + The referenced layerset must be an existing Feature owned by the caller, + uploaded via ``POST /domains/{id}/features/layerset``. The worker fetches + the GeoJSON from GCS at job time; a fresh upload produces a new + ``feature_id``, so the reference is effectively immutable. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRasterizeRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + """Create a grid by rasterizing a layerset + + # Create Layerset-Rasterized Grid + + Rasterizes a previously-uploaded fuelbed layerset into a grid aligned + to the domain (default) or to a target grid. + + The `layerset_id` must reference a Feature uploaded for this domain via + `POST /domains/{domain_id}/features/layerset` and owned by the caller. + + ## Request Body + + - **layerset_id**: (required) Feature ID of the layerset to rasterize. + - **overlap_method**: (optional) Per-cell reduction when polygons of the + same `fuel_type` overlap a single cell. One of `mean`, `max`, `min`. + Default: `mean`. (Loading is always summed across overlapping polygons + regardless of this setting.) + - **alignment**: (optional) See alignment docs. Default: anchored to domain. + - **extent_buffer_cells**: (optional) Buffer in result-grid cells around + the domain extent. Cells inside the buffered extent that fall outside + polygon coverage are populated with the rasterizer's fill value. + - **name**, **description**, **tags**, **modifications**: standard grid metadata. + + ## Response + + Returns the created Grid resource with status `pending`. The backend + fetches the layerset GeoJSON from GCS, rasterizes it, and updates the + status to `completed` when ready. + + Args: + domain_id (str): + body (CreateLayersetRasterizeRequest): Request to create a grid by rasterizing a + previously-uploaded layerset. + + The referenced layerset must be an existing Feature owned by the caller, + uploaded via ``POST /domains/{id}/features/layerset``. The worker fetches + the GeoJSON from GCS at job time; a fresh upload produces a new + ``feature_id``, so the reference is effectively immutable. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateLayersetRasterizeRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + """Create a grid by rasterizing a layerset + + # Create Layerset-Rasterized Grid + + Rasterizes a previously-uploaded fuelbed layerset into a grid aligned + to the domain (default) or to a target grid. + + The `layerset_id` must reference a Feature uploaded for this domain via + `POST /domains/{domain_id}/features/layerset` and owned by the caller. + + ## Request Body + + - **layerset_id**: (required) Feature ID of the layerset to rasterize. + - **overlap_method**: (optional) Per-cell reduction when polygons of the + same `fuel_type` overlap a single cell. One of `mean`, `max`, `min`. + Default: `mean`. (Loading is always summed across overlapping polygons + regardless of this setting.) + - **alignment**: (optional) See alignment docs. Default: anchored to domain. + - **extent_buffer_cells**: (optional) Buffer in result-grid cells around + the domain extent. Cells inside the buffered extent that fall outside + polygon coverage are populated with the rasterizer's fill value. + - **name**, **description**, **tags**, **modifications**: standard grid metadata. + + ## Response + + Returns the created Grid resource with status `pending`. The backend + fetches the layerset GeoJSON from GCS, rasterizes it, and updates the + status to `completed` when ready. + + Args: + domain_id (str): + body (CreateLayersetRasterizeRequest): Request to create a grid by rasterizing a + previously-uploaded layerset. + + The referenced layerset must be an existing Feature owned by the caller, + uploaded via ``POST /domains/{id}/features/layerset``. The worker fetches + the GeoJSON from GCS at job time; a fresh upload produces a new + ``feature_id``, so the reference is effectively immutable. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_meta_chm.py b/fastfuels_sdk/v2/client_library/api/grids/create_meta_chm.py new file mode 100644 index 0000000..5f832ba --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_meta_chm.py @@ -0,0 +1,280 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_meta_chm_request import CreateMetaChmRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateMetaChmRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/canopy/meta".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateMetaChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from Meta CHM + + # Create Meta CHM Grid + + Creates a grid with canopy height data from Meta's global canopy height + model at ~1m resolution. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) Meta CHM version. Default: \"2\". + - **1**: Tolan, J. et al. (2024). Very high resolution canopy height maps from RGB imagery. + - **2**: Brandt, J. et al. (2026). CHMv2: Improvements in Global Canopy Height Mapping using + DINOv3. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateMetaChmRequest): Request to create a grid from Meta CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateMetaChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from Meta CHM + + # Create Meta CHM Grid + + Creates a grid with canopy height data from Meta's global canopy height + model at ~1m resolution. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) Meta CHM version. Default: \"2\". + - **1**: Tolan, J. et al. (2024). Very high resolution canopy height maps from RGB imagery. + - **2**: Brandt, J. et al. (2026). CHMv2: Improvements in Global Canopy Height Mapping using + DINOv3. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateMetaChmRequest): Request to create a grid from Meta CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateMetaChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from Meta CHM + + # Create Meta CHM Grid + + Creates a grid with canopy height data from Meta's global canopy height + model at ~1m resolution. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) Meta CHM version. Default: \"2\". + - **1**: Tolan, J. et al. (2024). Very high resolution canopy height maps from RGB imagery. + - **2**: Brandt, J. et al. (2026). CHMv2: Improvements in Global Canopy Height Mapping using + DINOv3. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateMetaChmRequest): Request to create a grid from Meta CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateMetaChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from Meta CHM + + # Create Meta CHM Grid + + Creates a grid with canopy height data from Meta's global canopy height + model at ~1m resolution. + + ## Request Body + + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) Meta CHM version. Default: \"2\". + - **1**: Tolan, J. et al. (2024). Very high resolution canopy height maps from RGB imagery. + - **2**: Brandt, J. et al. (2026). CHMv2: Improvements in Global Canopy Height Mapping using + DINOv3. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateMetaChmRequest): Request to create a grid from Meta CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_naip_chm.py b/fastfuels_sdk/v2/client_library/api/grids/create_naip_chm.py new file mode 100644 index 0000000..fad5f86 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_naip_chm.py @@ -0,0 +1,260 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_naip_chm_request import CreateNaipChmRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateNaipChmRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/canopy/naip".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNaipChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from NAIP CHM + + # Create NAIP CHM Grid + + Creates a grid with canopy height data from the NAIP high-resolution + canopy height model at ~0.6m resolution (CONUS). + + ## Request Body + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateNaipChmRequest): Request to create a grid from NAIP CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNaipChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from NAIP CHM + + # Create NAIP CHM Grid + + Creates a grid with canopy height data from the NAIP high-resolution + canopy height model at ~0.6m resolution (CONUS). + + ## Request Body + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateNaipChmRequest): Request to create a grid from NAIP CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNaipChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from NAIP CHM + + # Create NAIP CHM Grid + + Creates a grid with canopy height data from the NAIP high-resolution + canopy height model at ~0.6m resolution (CONUS). + + ## Request Body + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateNaipChmRequest): Request to create a grid from NAIP CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNaipChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from NAIP CHM + + # Create NAIP CHM Grid + + Creates a grid with canopy height data from the NAIP high-resolution + canopy height model at ~0.6m resolution (CONUS). + + ## Request Body + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateNaipChmRequest): Request to create a grid from NAIP CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_netcdf_upload.py b/fastfuels_sdk/v2/client_library/api/grids/create_netcdf_upload.py new file mode 100644 index 0000000..e5f8bb6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_netcdf_upload.py @@ -0,0 +1,436 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_netcdf_upload_request import CreateNetcdfUploadRequest +from ...models.grid_upload_created_response import GridUploadCreatedResponse +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateNetcdfUploadRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/upload/netcdf".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = GridUploadCreatedResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNetcdfUploadRequest, +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from a direct netCDF upload + + # Create Upload Grid (netCDF) + + Creates a grid resource and returns a signed URL for uploading a + CF-conformant netCDF directly to GCS. Upload with HTTP PUT, sending + **every header in the response's `upload.headers`** exactly as given — + the signed URL commits to them, and the upload is rejected if any is + missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.nc -H \"Content-Type: application/x-netcdf\" -H \"x-goog- + content-length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Bands + + Unlike the GeoTIFF route, the request body has **no `bands` field**. + netCDF data variable names are the canonical band keys — they are + extracted directly from the file and become the variable names in the + output Zarr store. Per-band `units` (if set on the variable) and dtype + drive the stored band metadata. + + ## Dimensions + + Each data variable must have dims exactly `(\"y\",\"x\")` (2D) or + `(\"z\",\"y\",\"x\")` (3D) in that order. Mixed-rank datasets are rejected + with `WRONG_DIMS`. + + ## CRS + + The dataset must carry a CF `grid_mapping` (typically `spatial_ref`) + that matches the domain CRS. Missing CRS fails with `MISSING_CRS`; + mismatched CRS fails with `CRS_MISMATCH`. No auto-reproject. + + ## Units + + If a data variable has a `units` attribute it must be in canonical + UDUNITS-2 ASCII form with `**` exponents (e.g. `kg/m**3`, `1/m`, `%`). + Non-canonical forms (`kg/m³`, `kg/m^3`, `kg/m3`) fail with + `INVALID_UNITS`. See docs/units.md. + + ## Z axis (3D only) + + - `z.attrs[\"positive\"]` must equal `\"up\"`. `\"down\"` is rejected + (`MISSING_Z_POSITIVE`). + - z-coordinates must be uniformly spaced. Non-uniform spacing is + rejected with `NONUNIFORM_Z`. + + ## Buffer cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain + extent in the stored grid. The uploaded netCDF must cover the domain + bbox expanded by `num_buffer_cells * native_pixel_size` on each side; + pixels beyond that expanded extent are clipped away. + + ## File requirements + + CF-conformant netCDF (`.nc`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateNetcdfUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNetcdfUploadRequest, +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from a direct netCDF upload + + # Create Upload Grid (netCDF) + + Creates a grid resource and returns a signed URL for uploading a + CF-conformant netCDF directly to GCS. Upload with HTTP PUT, sending + **every header in the response's `upload.headers`** exactly as given — + the signed URL commits to them, and the upload is rejected if any is + missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.nc -H \"Content-Type: application/x-netcdf\" -H \"x-goog- + content-length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Bands + + Unlike the GeoTIFF route, the request body has **no `bands` field**. + netCDF data variable names are the canonical band keys — they are + extracted directly from the file and become the variable names in the + output Zarr store. Per-band `units` (if set on the variable) and dtype + drive the stored band metadata. + + ## Dimensions + + Each data variable must have dims exactly `(\"y\",\"x\")` (2D) or + `(\"z\",\"y\",\"x\")` (3D) in that order. Mixed-rank datasets are rejected + with `WRONG_DIMS`. + + ## CRS + + The dataset must carry a CF `grid_mapping` (typically `spatial_ref`) + that matches the domain CRS. Missing CRS fails with `MISSING_CRS`; + mismatched CRS fails with `CRS_MISMATCH`. No auto-reproject. + + ## Units + + If a data variable has a `units` attribute it must be in canonical + UDUNITS-2 ASCII form with `**` exponents (e.g. `kg/m**3`, `1/m`, `%`). + Non-canonical forms (`kg/m³`, `kg/m^3`, `kg/m3`) fail with + `INVALID_UNITS`. See docs/units.md. + + ## Z axis (3D only) + + - `z.attrs[\"positive\"]` must equal `\"up\"`. `\"down\"` is rejected + (`MISSING_Z_POSITIVE`). + - z-coordinates must be uniformly spaced. Non-uniform spacing is + rejected with `NONUNIFORM_Z`. + + ## Buffer cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain + extent in the stored grid. The uploaded netCDF must cover the domain + bbox expanded by `num_buffer_cells * native_pixel_size` on each side; + pixels beyond that expanded extent are clipped away. + + ## File requirements + + CF-conformant netCDF (`.nc`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateNetcdfUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNetcdfUploadRequest, +) -> Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from a direct netCDF upload + + # Create Upload Grid (netCDF) + + Creates a grid resource and returns a signed URL for uploading a + CF-conformant netCDF directly to GCS. Upload with HTTP PUT, sending + **every header in the response's `upload.headers`** exactly as given — + the signed URL commits to them, and the upload is rejected if any is + missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.nc -H \"Content-Type: application/x-netcdf\" -H \"x-goog- + content-length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Bands + + Unlike the GeoTIFF route, the request body has **no `bands` field**. + netCDF data variable names are the canonical band keys — they are + extracted directly from the file and become the variable names in the + output Zarr store. Per-band `units` (if set on the variable) and dtype + drive the stored band metadata. + + ## Dimensions + + Each data variable must have dims exactly `(\"y\",\"x\")` (2D) or + `(\"z\",\"y\",\"x\")` (3D) in that order. Mixed-rank datasets are rejected + with `WRONG_DIMS`. + + ## CRS + + The dataset must carry a CF `grid_mapping` (typically `spatial_ref`) + that matches the domain CRS. Missing CRS fails with `MISSING_CRS`; + mismatched CRS fails with `CRS_MISMATCH`. No auto-reproject. + + ## Units + + If a data variable has a `units` attribute it must be in canonical + UDUNITS-2 ASCII form with `**` exponents (e.g. `kg/m**3`, `1/m`, `%`). + Non-canonical forms (`kg/m³`, `kg/m^3`, `kg/m3`) fail with + `INVALID_UNITS`. See docs/units.md. + + ## Z axis (3D only) + + - `z.attrs[\"positive\"]` must equal `\"up\"`. `\"down\"` is rejected + (`MISSING_Z_POSITIVE`). + - z-coordinates must be uniformly spaced. Non-uniform spacing is + rejected with `NONUNIFORM_Z`. + + ## Buffer cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain + extent in the stored grid. The uploaded netCDF must cover the domain + bbox expanded by `num_buffer_cells * native_pixel_size` on each side; + pixels beyond that expanded extent are clipped away. + + ## File requirements + + CF-conformant netCDF (`.nc`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateNetcdfUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateNetcdfUploadRequest, +) -> GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from a direct netCDF upload + + # Create Upload Grid (netCDF) + + Creates a grid resource and returns a signed URL for uploading a + CF-conformant netCDF directly to GCS. Upload with HTTP PUT, sending + **every header in the response's `upload.headers`** exactly as given — + the signed URL commits to them, and the upload is rejected if any is + missing or altered. For example: + + ```bash + curl -X PUT --upload-file grid.nc -H \"Content-Type: application/x-netcdf\" -H \"x-goog- + content-length-range: 0,1073741824\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the grid status to `completed` + (or `failed` on error). + + ## Bands + + Unlike the GeoTIFF route, the request body has **no `bands` field**. + netCDF data variable names are the canonical band keys — they are + extracted directly from the file and become the variable names in the + output Zarr store. Per-band `units` (if set on the variable) and dtype + drive the stored band metadata. + + ## Dimensions + + Each data variable must have dims exactly `(\"y\",\"x\")` (2D) or + `(\"z\",\"y\",\"x\")` (3D) in that order. Mixed-rank datasets are rejected + with `WRONG_DIMS`. + + ## CRS + + The dataset must carry a CF `grid_mapping` (typically `spatial_ref`) + that matches the domain CRS. Missing CRS fails with `MISSING_CRS`; + mismatched CRS fails with `CRS_MISMATCH`. No auto-reproject. + + ## Units + + If a data variable has a `units` attribute it must be in canonical + UDUNITS-2 ASCII form with `**` exponents (e.g. `kg/m**3`, `1/m`, `%`). + Non-canonical forms (`kg/m³`, `kg/m^3`, `kg/m3`) fail with + `INVALID_UNITS`. See docs/units.md. + + ## Z axis (3D only) + + - `z.attrs[\"positive\"]` must equal `\"up\"`. `\"down\"` is rejected + (`MISSING_Z_POSITIVE`). + - z-coordinates must be uniformly spaced. Non-uniform spacing is + rejected with `NONUNIFORM_Z`. + + ## Buffer cells + + `num_buffer_cells` (default 0) keeps extra cells around the domain + extent in the stored grid. The uploaded netCDF must cover the domain + bbox expanded by `num_buffer_cells * native_pixel_size` on each side; + pixels beyond that expanded extent are clipped away. + + ## File requirements + + CF-conformant netCDF (`.nc`). Maximum 1 GB. + + Args: + domain_id (str): + body (CreateNetcdfUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridUploadCreatedResponse | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_point_cloud_chm.py b/fastfuels_sdk/v2/client_library/api/grids/create_point_cloud_chm.py new file mode 100644 index 0000000..9b36f19 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_point_cloud_chm.py @@ -0,0 +1,400 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_point_cloud_chm_request import CreatePointCloudChmRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreatePointCloudChmRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/canopy/point_cloud".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a CHM grid from a point cloud + + # Create a CHM Grid from a Point Cloud + + Creates a grid with canopy height data rasterized from a point cloud. Each + cell holds the greatest height above ground of any return that falls in it. + + The resulting grid carries the same `chm` band as the Meta, NAIP, and + LANDFIRE canopy sources, so it can be used anywhere they can — including as + the source for individual tree detection + (`POST /domains/{domain_id}/inventories/tree/chm`). + + ## Ground + + Canopy height is height above ground, so the ground surface underneath + matters. When the point cloud carries ASPRS ground classification + (class 2), those returns define the ground. When it does not — an upload + may carry no classification at all — the ground surface is derived from the + data. + + Which path was taken, and how well the data constrained it, is recorded on + the completed grid under `source.ground`. Derived ground is accurate in + forested terrain and degrades over wide areas with no ground returns, such + as large building footprints or very dense canopy; + `source.ground.ground_coverage` and `source.ground.max_ground_distance_m` + are what reveal that. + + ## Request Body + + - **source_point_cloud_id**: The point cloud to rasterize. Must be airborne + (`type: als`), `completed`, and in this domain. + - **alignment**: (optional) Output lattice. Against the domain + (`target: \"domain\"`, the default) `resolution` defaults to 1 m — unlike + the raster-backed canopy sources there is no source pixel size to + inherit. Against another grid (`target: \"grid\"`) omitting `resolution` + matches that grid cell-for-cell, and the output covers the target's + extent rather than the domain's; giving one keeps the target's origin at + the new cell size. The target grid must be in this domain's CRS. + `target: \"native\"` is not supported — a point cloud has no pixel anchor + to preserve. + - **name**, **description**, **tags**: (optional) Metadata. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + build the grid and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreatePointCloudChmRequest): Request to create a canopy height model grid from a + point cloud. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + The point cloud must be airborne (`type: als`) and `completed`. Cell size + comes from `alignment.resolution`, defaulting to 1 m — unlike the + raster-backed canopy sources there is no source pixel size to inherit. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a CHM grid from a point cloud + + # Create a CHM Grid from a Point Cloud + + Creates a grid with canopy height data rasterized from a point cloud. Each + cell holds the greatest height above ground of any return that falls in it. + + The resulting grid carries the same `chm` band as the Meta, NAIP, and + LANDFIRE canopy sources, so it can be used anywhere they can — including as + the source for individual tree detection + (`POST /domains/{domain_id}/inventories/tree/chm`). + + ## Ground + + Canopy height is height above ground, so the ground surface underneath + matters. When the point cloud carries ASPRS ground classification + (class 2), those returns define the ground. When it does not — an upload + may carry no classification at all — the ground surface is derived from the + data. + + Which path was taken, and how well the data constrained it, is recorded on + the completed grid under `source.ground`. Derived ground is accurate in + forested terrain and degrades over wide areas with no ground returns, such + as large building footprints or very dense canopy; + `source.ground.ground_coverage` and `source.ground.max_ground_distance_m` + are what reveal that. + + ## Request Body + + - **source_point_cloud_id**: The point cloud to rasterize. Must be airborne + (`type: als`), `completed`, and in this domain. + - **alignment**: (optional) Output lattice. Against the domain + (`target: \"domain\"`, the default) `resolution` defaults to 1 m — unlike + the raster-backed canopy sources there is no source pixel size to + inherit. Against another grid (`target: \"grid\"`) omitting `resolution` + matches that grid cell-for-cell, and the output covers the target's + extent rather than the domain's; giving one keeps the target's origin at + the new cell size. The target grid must be in this domain's CRS. + `target: \"native\"` is not supported — a point cloud has no pixel anchor + to preserve. + - **name**, **description**, **tags**: (optional) Metadata. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + build the grid and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreatePointCloudChmRequest): Request to create a canopy height model grid from a + point cloud. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + The point cloud must be airborne (`type: als`) and `completed`. Cell size + comes from `alignment.resolution`, defaulting to 1 m — unlike the + raster-backed canopy sources there is no source pixel size to inherit. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudChmRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a CHM grid from a point cloud + + # Create a CHM Grid from a Point Cloud + + Creates a grid with canopy height data rasterized from a point cloud. Each + cell holds the greatest height above ground of any return that falls in it. + + The resulting grid carries the same `chm` band as the Meta, NAIP, and + LANDFIRE canopy sources, so it can be used anywhere they can — including as + the source for individual tree detection + (`POST /domains/{domain_id}/inventories/tree/chm`). + + ## Ground + + Canopy height is height above ground, so the ground surface underneath + matters. When the point cloud carries ASPRS ground classification + (class 2), those returns define the ground. When it does not — an upload + may carry no classification at all — the ground surface is derived from the + data. + + Which path was taken, and how well the data constrained it, is recorded on + the completed grid under `source.ground`. Derived ground is accurate in + forested terrain and degrades over wide areas with no ground returns, such + as large building footprints or very dense canopy; + `source.ground.ground_coverage` and `source.ground.max_ground_distance_m` + are what reveal that. + + ## Request Body + + - **source_point_cloud_id**: The point cloud to rasterize. Must be airborne + (`type: als`), `completed`, and in this domain. + - **alignment**: (optional) Output lattice. Against the domain + (`target: \"domain\"`, the default) `resolution` defaults to 1 m — unlike + the raster-backed canopy sources there is no source pixel size to + inherit. Against another grid (`target: \"grid\"`) omitting `resolution` + matches that grid cell-for-cell, and the output covers the target's + extent rather than the domain's; giving one keeps the target's origin at + the new cell size. The target grid must be in this domain's CRS. + `target: \"native\"` is not supported — a point cloud has no pixel anchor + to preserve. + - **name**, **description**, **tags**: (optional) Metadata. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + build the grid and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreatePointCloudChmRequest): Request to create a canopy height model grid from a + point cloud. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + The point cloud must be airborne (`type: als`) and `completed`. Cell size + comes from `alignment.resolution`, defaulting to 1 m — unlike the + raster-backed canopy sources there is no source pixel size to inherit. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudChmRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a CHM grid from a point cloud + + # Create a CHM Grid from a Point Cloud + + Creates a grid with canopy height data rasterized from a point cloud. Each + cell holds the greatest height above ground of any return that falls in it. + + The resulting grid carries the same `chm` band as the Meta, NAIP, and + LANDFIRE canopy sources, so it can be used anywhere they can — including as + the source for individual tree detection + (`POST /domains/{domain_id}/inventories/tree/chm`). + + ## Ground + + Canopy height is height above ground, so the ground surface underneath + matters. When the point cloud carries ASPRS ground classification + (class 2), those returns define the ground. When it does not — an upload + may carry no classification at all — the ground surface is derived from the + data. + + Which path was taken, and how well the data constrained it, is recorded on + the completed grid under `source.ground`. Derived ground is accurate in + forested terrain and degrades over wide areas with no ground returns, such + as large building footprints or very dense canopy; + `source.ground.ground_coverage` and `source.ground.max_ground_distance_m` + are what reveal that. + + ## Request Body + + - **source_point_cloud_id**: The point cloud to rasterize. Must be airborne + (`type: als`), `completed`, and in this domain. + - **alignment**: (optional) Output lattice. Against the domain + (`target: \"domain\"`, the default) `resolution` defaults to 1 m — unlike + the raster-backed canopy sources there is no source pixel size to + inherit. Against another grid (`target: \"grid\"`) omitting `resolution` + matches that grid cell-for-cell, and the output covers the target's + extent rather than the domain's; giving one keeps the target's origin at + the new cell size. The target grid must be in this domain's CRS. + `target: \"native\"` is not supported — a point cloud has no pixel anchor + to preserve. + - **name**, **description**, **tags**: (optional) Metadata. + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + build the grid and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreatePointCloudChmRequest): Request to create a canopy height model grid from a + point cloud. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + The point cloud must be airborne (`type: als`) and `completed`. Cell size + comes from `alignment.resolution`, defaulting to 1 m — unlike the + raster-backed canopy sources there is no source pixel size to inherit. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_quicfire_export.py b/fastfuels_sdk/v2/client_library/api/grids/create_quicfire_export.py new file mode 100644 index 0000000..56fcf20 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_quicfire_export.py @@ -0,0 +1,324 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.http_validation_error import HTTPValidationError +from ...models.quicfire_export_request import QuicfireExportRequest +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: QuicfireExportRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/exports/quicfire".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Export.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: QuicfireExportRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export combined fuel + topography grids to QUIC-Fire format + + Bundle surface fuel + canopy fuel + (optional) topography grids into a + QUIC-Fire-loadable zip archive. + + The output zip contains `treesrhof.dat`, `treesmoist.dat`, + `treesfueldepth.dat`, `metadata.json`, and `domain.geojson` always; plus + `topo.dat` when a topography role is provided, plus `treesss.dat` when + both canopy and surface SAVR roles are provided. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (QuicfireExportRequest): Request body for creating a QUIC-Fire combined export. + + Five required roles produce `treesrhof.dat`, `treesmoist.dat`, and + `treesfueldepth.dat`. `topography` (optional) produces `topo.dat`. The + SAVR pair (optional, both-or-neither) produces `treesss.dat`. + + The fire grid is defined by the `alignment` field — either the Domain + bounding box padded to `(dx, dy)` (with `dz` vertical), or the lattice + of an existing grid. Every role grid must be lattice-aligned to this + fire grid and cover its full extent; otherwise the request is rejected. + The exporter only crops oversized roles by integer slicing — it never + resamples or reprojects. + + The output resolution is set here, on the export, via `alignment.dx`/`dy` + (default 2 m, QUIC-Fire's recommended value). It is a separate setting + from the resolution of each grid you built — changing your grids does not + change the export, and vice versa. Because the exporter never resamples, + every role grid must already be built at the fire-grid resolution. To + export at 1 m, for example, set `dx`/`dy` to 1 and build all role grids at + 1 m (2D grids at 1 m via their `alignment.resolution`, and the 3D tree + grid at 1 m via `resolution.horizontal` — 3D grids cannot be resampled). + The same holds vertically: `alignment.dz` must equal the 3D tree grid's + `resolution.vertical`, or the request is rejected with 422. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: QuicfireExportRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export combined fuel + topography grids to QUIC-Fire format + + Bundle surface fuel + canopy fuel + (optional) topography grids into a + QUIC-Fire-loadable zip archive. + + The output zip contains `treesrhof.dat`, `treesmoist.dat`, + `treesfueldepth.dat`, `metadata.json`, and `domain.geojson` always; plus + `topo.dat` when a topography role is provided, plus `treesss.dat` when + both canopy and surface SAVR roles are provided. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (QuicfireExportRequest): Request body for creating a QUIC-Fire combined export. + + Five required roles produce `treesrhof.dat`, `treesmoist.dat`, and + `treesfueldepth.dat`. `topography` (optional) produces `topo.dat`. The + SAVR pair (optional, both-or-neither) produces `treesss.dat`. + + The fire grid is defined by the `alignment` field — either the Domain + bounding box padded to `(dx, dy)` (with `dz` vertical), or the lattice + of an existing grid. Every role grid must be lattice-aligned to this + fire grid and cover its full extent; otherwise the request is rejected. + The exporter only crops oversized roles by integer slicing — it never + resamples or reprojects. + + The output resolution is set here, on the export, via `alignment.dx`/`dy` + (default 2 m, QUIC-Fire's recommended value). It is a separate setting + from the resolution of each grid you built — changing your grids does not + change the export, and vice versa. Because the exporter never resamples, + every role grid must already be built at the fire-grid resolution. To + export at 1 m, for example, set `dx`/`dy` to 1 and build all role grids at + 1 m (2D grids at 1 m via their `alignment.resolution`, and the 3D tree + grid at 1 m via `resolution.horizontal` — 3D grids cannot be resampled). + The same holds vertically: `alignment.dz` must equal the 3D tree grid's + `resolution.vertical`, or the request is rejected with 422. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: QuicfireExportRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export combined fuel + topography grids to QUIC-Fire format + + Bundle surface fuel + canopy fuel + (optional) topography grids into a + QUIC-Fire-loadable zip archive. + + The output zip contains `treesrhof.dat`, `treesmoist.dat`, + `treesfueldepth.dat`, `metadata.json`, and `domain.geojson` always; plus + `topo.dat` when a topography role is provided, plus `treesss.dat` when + both canopy and surface SAVR roles are provided. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (QuicfireExportRequest): Request body for creating a QUIC-Fire combined export. + + Five required roles produce `treesrhof.dat`, `treesmoist.dat`, and + `treesfueldepth.dat`. `topography` (optional) produces `topo.dat`. The + SAVR pair (optional, both-or-neither) produces `treesss.dat`. + + The fire grid is defined by the `alignment` field — either the Domain + bounding box padded to `(dx, dy)` (with `dz` vertical), or the lattice + of an existing grid. Every role grid must be lattice-aligned to this + fire grid and cover its full extent; otherwise the request is rejected. + The exporter only crops oversized roles by integer slicing — it never + resamples or reprojects. + + The output resolution is set here, on the export, via `alignment.dx`/`dy` + (default 2 m, QUIC-Fire's recommended value). It is a separate setting + from the resolution of each grid you built — changing your grids does not + change the export, and vice versa. Because the exporter never resamples, + every role grid must already be built at the fire-grid resolution. To + export at 1 m, for example, set `dx`/`dy` to 1 and build all role grids at + 1 m (2D grids at 1 m via their `alignment.resolution`, and the 3D tree + grid at 1 m via `resolution.horizontal` — 3D grids cannot be resampled). + The same holds vertically: `alignment.dz` must equal the 3D tree grid's + `resolution.vertical`, or the request is rejected with 422. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: QuicfireExportRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export combined fuel + topography grids to QUIC-Fire format + + Bundle surface fuel + canopy fuel + (optional) topography grids into a + QUIC-Fire-loadable zip archive. + + The output zip contains `treesrhof.dat`, `treesmoist.dat`, + `treesfueldepth.dat`, `metadata.json`, and `domain.geojson` always; plus + `topo.dat` when a topography role is provided, plus `treesss.dat` when + both canopy and surface SAVR roles are provided. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to retrieve the + signed download URL. + + Args: + domain_id (str): + body (QuicfireExportRequest): Request body for creating a QUIC-Fire combined export. + + Five required roles produce `treesrhof.dat`, `treesmoist.dat`, and + `treesfueldepth.dat`. `topography` (optional) produces `topo.dat`. The + SAVR pair (optional, both-or-neither) produces `treesss.dat`. + + The fire grid is defined by the `alignment` field — either the Domain + bounding box padded to `(dx, dy)` (with `dz` vertical), or the lattice + of an existing grid. Every role grid must be lattice-aligned to this + fire grid and cover its full extent; otherwise the request is rejected. + The exporter only crops oversized roles by integer slicing — it never + resamples or reprojects. + + The output resolution is set here, on the export, via `alignment.dx`/`dy` + (default 2 m, QUIC-Fire's recommended value). It is a separate setting + from the resolution of each grid you built — changing your grids does not + change the export, and vice versa. Because the exporter never resamples, + every role grid must already be built at the fire-grid resolution. To + export at 1 m, for example, set `dx`/`dy` to 1 and build all role grids at + 1 m (2D grids at 1 m via their `alignment.resolution`, and the 3D tree + grid at 1 m via `resolution.horizontal` — 3D grids cannot be resampled). + The same holds vertically: `alignment.dz` must equal the 3D tree grid's + `resolution.vertical`, or the request is rejected with 422. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_resample.py b/fastfuels_sdk/v2/client_library/api/grids/create_resample.py new file mode 100644 index 0000000..39a84a1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_resample.py @@ -0,0 +1,320 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_resample_request import CreateResampleRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateResampleRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/resample".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateResampleRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by resampling an existing grid + + # Create Resampled Grid + + Resamples an existing grid to a new spatial resolution and/or anchor. + This is the key operation for unifying grids on a common lattice + (e.g., LANDFIRE 30m to 2m for QUIC-Fire input). + + The resampled grid propagates ``domain_id`` and bands from the source grid. + + ## Request Body + + - **source_grid_id**: (required) Grid to resample. Must have status + \"completed\" and a georeference. + - **alignment**: Output alignment target. Default ``target=\"domain\"``. + ``alignment.resolution`` is required for ``target=\"domain\"`` and + ``target=\"native\"``; optional for ``target=\"grid\"`` (defaults to the + target grid's exact transform/shape). + - **method_overrides**: (optional) Per-band resampling method overrides. + - **name**, **description**, **tags**: (optional) + + ## Response + + Returns the created Grid with status \"pending\". The backend performs the + resampling and updates status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateResampleRequest): Request to create a grid by resampling an existing grid. + + Unlike entry-point grid creation requests, ``domain_id`` is not required + because derived grids carry the same domain reference as their source. + + The ``alignment`` field controls the output lattice. ``alignment.resolution`` + is required for ``target="domain"`` and ``target="native"``; for + ``target="grid"`` it is optional (defaults to the target grid's exact + transform/shape; if supplied, keeps the target's CRS and origin and + recomputes shape at the new cell size). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateResampleRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by resampling an existing grid + + # Create Resampled Grid + + Resamples an existing grid to a new spatial resolution and/or anchor. + This is the key operation for unifying grids on a common lattice + (e.g., LANDFIRE 30m to 2m for QUIC-Fire input). + + The resampled grid propagates ``domain_id`` and bands from the source grid. + + ## Request Body + + - **source_grid_id**: (required) Grid to resample. Must have status + \"completed\" and a georeference. + - **alignment**: Output alignment target. Default ``target=\"domain\"``. + ``alignment.resolution`` is required for ``target=\"domain\"`` and + ``target=\"native\"``; optional for ``target=\"grid\"`` (defaults to the + target grid's exact transform/shape). + - **method_overrides**: (optional) Per-band resampling method overrides. + - **name**, **description**, **tags**: (optional) + + ## Response + + Returns the created Grid with status \"pending\". The backend performs the + resampling and updates status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateResampleRequest): Request to create a grid by resampling an existing grid. + + Unlike entry-point grid creation requests, ``domain_id`` is not required + because derived grids carry the same domain reference as their source. + + The ``alignment`` field controls the output lattice. ``alignment.resolution`` + is required for ``target="domain"`` and ``target="native"``; for + ``target="grid"`` it is optional (defaults to the target grid's exact + transform/shape; if supplied, keeps the target's CRS and origin and + recomputes shape at the new cell size). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateResampleRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid by resampling an existing grid + + # Create Resampled Grid + + Resamples an existing grid to a new spatial resolution and/or anchor. + This is the key operation for unifying grids on a common lattice + (e.g., LANDFIRE 30m to 2m for QUIC-Fire input). + + The resampled grid propagates ``domain_id`` and bands from the source grid. + + ## Request Body + + - **source_grid_id**: (required) Grid to resample. Must have status + \"completed\" and a georeference. + - **alignment**: Output alignment target. Default ``target=\"domain\"``. + ``alignment.resolution`` is required for ``target=\"domain\"`` and + ``target=\"native\"``; optional for ``target=\"grid\"`` (defaults to the + target grid's exact transform/shape). + - **method_overrides**: (optional) Per-band resampling method overrides. + - **name**, **description**, **tags**: (optional) + + ## Response + + Returns the created Grid with status \"pending\". The backend performs the + resampling and updates status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateResampleRequest): Request to create a grid by resampling an existing grid. + + Unlike entry-point grid creation requests, ``domain_id`` is not required + because derived grids carry the same domain reference as their source. + + The ``alignment`` field controls the output lattice. ``alignment.resolution`` + is required for ``target="domain"`` and ``target="native"``; for + ``target="grid"`` it is optional (defaults to the target grid's exact + transform/shape; if supplied, keeps the target's CRS and origin and + recomputes shape at the new cell size). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateResampleRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid by resampling an existing grid + + # Create Resampled Grid + + Resamples an existing grid to a new spatial resolution and/or anchor. + This is the key operation for unifying grids on a common lattice + (e.g., LANDFIRE 30m to 2m for QUIC-Fire input). + + The resampled grid propagates ``domain_id`` and bands from the source grid. + + ## Request Body + + - **source_grid_id**: (required) Grid to resample. Must have status + \"completed\" and a georeference. + - **alignment**: Output alignment target. Default ``target=\"domain\"``. + ``alignment.resolution`` is required for ``target=\"domain\"`` and + ``target=\"native\"``; optional for ``target=\"grid\"`` (defaults to the + target grid's exact transform/shape). + - **method_overrides**: (optional) Per-band resampling method overrides. + - **name**, **description**, **tags**: (optional) + + ## Response + + Returns the created Grid with status \"pending\". The backend performs the + resampling and updates status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateResampleRequest): Request to create a grid by resampling an existing grid. + + Unlike entry-point grid creation requests, ``domain_id`` is not required + because derived grids carry the same domain reference as their source. + + The ``alignment`` field controls the output lattice. ``alignment.resolution`` + is required for ``target="domain"`` and ``target="native"``; for + ``target="grid"`` it is optional (defaults to the target grid's exact + transform/shape; if supplied, keeps the target's CRS and origin and + recomputes shape at the new cell size). + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_tree_inventory_grid.py b/fastfuels_sdk/v2/client_library/api/grids/create_tree_inventory_grid.py new file mode 100644 index 0000000..6186421 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_tree_inventory_grid.py @@ -0,0 +1,388 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_tree_inventory_request import CreateTreeInventoryRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateTreeInventoryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/voxelize/inventory/tree".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeInventoryRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a 3D tree fuel grid from a tree inventory + + # Create Tree Inventory Grid + + Voxelizes a tree inventory into a 3D canopy fuel grid. Each tree's crown + is discretized onto the voxel grid using a species-specific crown profile + model, and per-voxel fuel properties (bulk density, moisture, SAV) are + computed from biomass and moisture models. + + This is a 3D grid product — resampling and modifications are not + supported. Apply modifications to the source inventory before voxelizing. + + ## Request Body + + - **source_inventory_id**: (required) ID of a completed tree inventory. + - **resolution**: (optional) Voxel resolution in meters. Defaults to + `{\"horizontal\": 2.0, \"vertical\": 1.0}`. All components must be positive. + - **bands**: (optional) Which output bands to produce. Defaults to + `[\"bulk_density.foliage.live\"]`. Must be non-empty and contain no + duplicates. Branchwood and fine bands are accepted by the API, but + Treevox currently fails those jobs with a not-implemented processing + error. + - **crown_profile_model**: (optional) Crown geometry model. One of + `purves` (default) or `beta`. + - **biomass_source**: (optional) Biomass source and requested components. The + default uses NSVB allometry for foliage. Inventory-column sources must + provide per-tree kg values for each requested direct component. + - **max_crown_radius_source**: (optional) Source of each tree's maximum + crown radius. Defaults to the crown profile model's allometric value; + pass `{\"type\": \"inventory_column\", \"column\": }` to read a per-tree + maximum radius (m) from an inventory column (e.g. derived from LiDAR). + The crown profile model still controls the crown shape — only the peak + radius is rescaled. + - **moisture_model**: (optional) Live/dead fuel moisture configuration. + Required shape: `{\"live\": {\"method\": \"uniform\", \"value\": }}` + and/or `{\"dead\": {\"method\": \"uniform\", \"value\": }}`. + Applied only when matching `fuel_moisture.*` bands are requested. Live + defaults to 100%; dead defaults to 10%. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Response + + Returns the created Grid resource with status `\"pending\"` and + `georeference: null`. The Treevox backend performs voxelization + asynchronously and updates the grid to `\"completed\"` with a + `Georeference3D` when done. + + Args: + domain_id (str): + body (CreateTreeInventoryRequest): Request body for creating a tree fuel grid from a tree + inventory. + + Does not extend CreateGridRequestBase because 3D grids do not support + modifications — modifications must be applied to the inventory before + voxelization, not to the resulting voxel grid. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeInventoryRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a 3D tree fuel grid from a tree inventory + + # Create Tree Inventory Grid + + Voxelizes a tree inventory into a 3D canopy fuel grid. Each tree's crown + is discretized onto the voxel grid using a species-specific crown profile + model, and per-voxel fuel properties (bulk density, moisture, SAV) are + computed from biomass and moisture models. + + This is a 3D grid product — resampling and modifications are not + supported. Apply modifications to the source inventory before voxelizing. + + ## Request Body + + - **source_inventory_id**: (required) ID of a completed tree inventory. + - **resolution**: (optional) Voxel resolution in meters. Defaults to + `{\"horizontal\": 2.0, \"vertical\": 1.0}`. All components must be positive. + - **bands**: (optional) Which output bands to produce. Defaults to + `[\"bulk_density.foliage.live\"]`. Must be non-empty and contain no + duplicates. Branchwood and fine bands are accepted by the API, but + Treevox currently fails those jobs with a not-implemented processing + error. + - **crown_profile_model**: (optional) Crown geometry model. One of + `purves` (default) or `beta`. + - **biomass_source**: (optional) Biomass source and requested components. The + default uses NSVB allometry for foliage. Inventory-column sources must + provide per-tree kg values for each requested direct component. + - **max_crown_radius_source**: (optional) Source of each tree's maximum + crown radius. Defaults to the crown profile model's allometric value; + pass `{\"type\": \"inventory_column\", \"column\": }` to read a per-tree + maximum radius (m) from an inventory column (e.g. derived from LiDAR). + The crown profile model still controls the crown shape — only the peak + radius is rescaled. + - **moisture_model**: (optional) Live/dead fuel moisture configuration. + Required shape: `{\"live\": {\"method\": \"uniform\", \"value\": }}` + and/or `{\"dead\": {\"method\": \"uniform\", \"value\": }}`. + Applied only when matching `fuel_moisture.*` bands are requested. Live + defaults to 100%; dead defaults to 10%. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Response + + Returns the created Grid resource with status `\"pending\"` and + `georeference: null`. The Treevox backend performs voxelization + asynchronously and updates the grid to `\"completed\"` with a + `Georeference3D` when done. + + Args: + domain_id (str): + body (CreateTreeInventoryRequest): Request body for creating a tree fuel grid from a tree + inventory. + + Does not extend CreateGridRequestBase because 3D grids do not support + modifications — modifications must be applied to the inventory before + voxelization, not to the resulting voxel grid. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeInventoryRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a 3D tree fuel grid from a tree inventory + + # Create Tree Inventory Grid + + Voxelizes a tree inventory into a 3D canopy fuel grid. Each tree's crown + is discretized onto the voxel grid using a species-specific crown profile + model, and per-voxel fuel properties (bulk density, moisture, SAV) are + computed from biomass and moisture models. + + This is a 3D grid product — resampling and modifications are not + supported. Apply modifications to the source inventory before voxelizing. + + ## Request Body + + - **source_inventory_id**: (required) ID of a completed tree inventory. + - **resolution**: (optional) Voxel resolution in meters. Defaults to + `{\"horizontal\": 2.0, \"vertical\": 1.0}`. All components must be positive. + - **bands**: (optional) Which output bands to produce. Defaults to + `[\"bulk_density.foliage.live\"]`. Must be non-empty and contain no + duplicates. Branchwood and fine bands are accepted by the API, but + Treevox currently fails those jobs with a not-implemented processing + error. + - **crown_profile_model**: (optional) Crown geometry model. One of + `purves` (default) or `beta`. + - **biomass_source**: (optional) Biomass source and requested components. The + default uses NSVB allometry for foliage. Inventory-column sources must + provide per-tree kg values for each requested direct component. + - **max_crown_radius_source**: (optional) Source of each tree's maximum + crown radius. Defaults to the crown profile model's allometric value; + pass `{\"type\": \"inventory_column\", \"column\": }` to read a per-tree + maximum radius (m) from an inventory column (e.g. derived from LiDAR). + The crown profile model still controls the crown shape — only the peak + radius is rescaled. + - **moisture_model**: (optional) Live/dead fuel moisture configuration. + Required shape: `{\"live\": {\"method\": \"uniform\", \"value\": }}` + and/or `{\"dead\": {\"method\": \"uniform\", \"value\": }}`. + Applied only when matching `fuel_moisture.*` bands are requested. Live + defaults to 100%; dead defaults to 10%. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Response + + Returns the created Grid resource with status `\"pending\"` and + `georeference: null`. The Treevox backend performs voxelization + asynchronously and updates the grid to `\"completed\"` with a + `Georeference3D` when done. + + Args: + domain_id (str): + body (CreateTreeInventoryRequest): Request body for creating a tree fuel grid from a tree + inventory. + + Does not extend CreateGridRequestBase because 3D grids do not support + modifications — modifications must be applied to the inventory before + voxelization, not to the resulting voxel grid. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeInventoryRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a 3D tree fuel grid from a tree inventory + + # Create Tree Inventory Grid + + Voxelizes a tree inventory into a 3D canopy fuel grid. Each tree's crown + is discretized onto the voxel grid using a species-specific crown profile + model, and per-voxel fuel properties (bulk density, moisture, SAV) are + computed from biomass and moisture models. + + This is a 3D grid product — resampling and modifications are not + supported. Apply modifications to the source inventory before voxelizing. + + ## Request Body + + - **source_inventory_id**: (required) ID of a completed tree inventory. + - **resolution**: (optional) Voxel resolution in meters. Defaults to + `{\"horizontal\": 2.0, \"vertical\": 1.0}`. All components must be positive. + - **bands**: (optional) Which output bands to produce. Defaults to + `[\"bulk_density.foliage.live\"]`. Must be non-empty and contain no + duplicates. Branchwood and fine bands are accepted by the API, but + Treevox currently fails those jobs with a not-implemented processing + error. + - **crown_profile_model**: (optional) Crown geometry model. One of + `purves` (default) or `beta`. + - **biomass_source**: (optional) Biomass source and requested components. The + default uses NSVB allometry for foliage. Inventory-column sources must + provide per-tree kg values for each requested direct component. + - **max_crown_radius_source**: (optional) Source of each tree's maximum + crown radius. Defaults to the crown profile model's allometric value; + pass `{\"type\": \"inventory_column\", \"column\": }` to read a per-tree + maximum radius (m) from an inventory column (e.g. derived from LiDAR). + The crown profile model still controls the crown shape — only the peak + radius is rescaled. + - **moisture_model**: (optional) Live/dead fuel moisture configuration. + Required shape: `{\"live\": {\"method\": \"uniform\", \"value\": }}` + and/or `{\"dead\": {\"method\": \"uniform\", \"value\": }}`. + Applied only when matching `fuel_moisture.*` bands are requested. Live + defaults to 100%; dead defaults to 10%. + - **name**, **description**, **tags**: (optional) Standard metadata. + + ## Response + + Returns the created Grid resource with status `\"pending\"` and + `georeference: null`. The Treevox backend performs voxelization + asynchronously and updates the grid to `\"completed\"` with a + `Georeference3D` when done. + + Args: + domain_id (str): + body (CreateTreeInventoryRequest): Request body for creating a tree fuel grid from a tree + inventory. + + Does not extend CreateGridRequestBase because 3D grids do not support + modifications — modifications must be applied to the inventory before + voxelization, not to the resulting voxel grid. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_treemap.py b/fastfuels_sdk/v2/client_library/api/grids/create_treemap.py new file mode 100644 index 0000000..48e5949 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_treemap.py @@ -0,0 +1,300 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_tree_map_request import CreateTreeMapRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateTreeMapRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/pim/treemap".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeMapRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from TreeMap + + # Create TreeMap Grid + + Creates a grid with plot imputation data from TreeMap at 30m resolution. + + Each pixel contains a plot ID that maps to FIA tree records. Available bands: + - **tm_id**: TreeMap raster pixel values (small integers, 1-70K) + - **plt_cn**: FIA plot condition number (large integers, derived from tree table) + + By default only `tm_id` is included. Use the `bands` field to also request + `plt_cn`. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: [\"tm_id\"]. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) TreeMap version. Default: \"2022\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateTreeMapRequest): Request to create a grid from TreeMap. + + Returns a grid with one or two categorical bands: + - tm_id: TreeMap raster pixel values (always available) + - plt_cn: FIA plot condition number (optional, derived from tree table) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeMapRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from TreeMap + + # Create TreeMap Grid + + Creates a grid with plot imputation data from TreeMap at 30m resolution. + + Each pixel contains a plot ID that maps to FIA tree records. Available bands: + - **tm_id**: TreeMap raster pixel values (small integers, 1-70K) + - **plt_cn**: FIA plot condition number (large integers, derived from tree table) + + By default only `tm_id` is included. Use the `bands` field to also request + `plt_cn`. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: [\"tm_id\"]. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) TreeMap version. Default: \"2022\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateTreeMapRequest): Request to create a grid from TreeMap. + + Returns a grid with one or two categorical bands: + - tm_id: TreeMap raster pixel values (always available) + - plt_cn: FIA plot condition number (optional, derived from tree table) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeMapRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a grid from TreeMap + + # Create TreeMap Grid + + Creates a grid with plot imputation data from TreeMap at 30m resolution. + + Each pixel contains a plot ID that maps to FIA tree records. Available bands: + - **tm_id**: TreeMap raster pixel values (small integers, 1-70K) + - **plt_cn**: FIA plot condition number (large integers, derived from tree table) + + By default only `tm_id` is included. Use the `bands` field to also request + `plt_cn`. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: [\"tm_id\"]. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) TreeMap version. Default: \"2022\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateTreeMapRequest): Request to create a grid from TreeMap. + + Returns a grid with one or two categorical bands: + - tm_id: TreeMap raster pixel values (always available) + - plt_cn: FIA plot condition number (optional, derived from tree table) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateTreeMapRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a grid from TreeMap + + # Create TreeMap Grid + + Creates a grid with plot imputation data from TreeMap at 30m resolution. + + Each pixel contains a plot ID that maps to FIA tree records. Available bands: + - **tm_id**: TreeMap raster pixel values (small integers, 1-70K) + - **plt_cn**: FIA plot condition number (large integers, derived from tree table) + + By default only `tm_id` is included. Use the `bands` field to also request + `plt_cn`. + + ## Request Body + + - **bands**: (optional) Which bands to include. Default: [\"tm_id\"]. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + - **version**: (optional) TreeMap version. Default: \"2022\". + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + fetch the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateTreeMapRequest): Request to create a grid from TreeMap. + + Returns a grid with one or two categorical bands: + - tm_id: TreeMap raster pixel values (always available) + - plt_cn: FIA plot condition number (optional, derived from tree table) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/create_uniform_grid.py b/fastfuels_sdk/v2/client_library/api/grids/create_uniform_grid.py new file mode 100644 index 0000000..db97ef2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/create_uniform_grid.py @@ -0,0 +1,336 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_uniform_request import CreateUniformRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateUniformRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/uniform".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateUniformRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a uniform (constant-value) grid + + # Create Uniform Grid + + Creates a grid where every cell is filled with a constant value for each + specified band. Useful for fuel moisture scenarios, constant fuel loads, + and other spatially-uniform inputs. + + ## Request Body + + - **resolution**: (required) Grid resolution in meters (>= 1). No default + since uniform grids have no \"native resolution.\" + - **bands**: (required) One or more bands, each with a key and value. + Band keys must be unique. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Available Bands + + **Fuel moisture** (unit: %): `fuel_moisture.1hr`, `fuel_moisture.10hr`, + `fuel_moisture.100hr`, `fuel_moisture.live_herb`, `fuel_moisture.live_woody` + + **Curing** (unit: %): `curing` + + **Fuel load** (unit: kg/m**2): `fuel_load.1hr`, `fuel_load.10hr`, + `fuel_load.100hr`, `fuel_load.live_herb`, `fuel_load.live_woody` + + **Fuel depth** (unit: m): `fuel_depth` + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + generate the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateUniformRequest): Request to create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the specified + resolution. No default resolution — it must be explicitly provided since + uniform grids have no "native resolution." + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateUniformRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a uniform (constant-value) grid + + # Create Uniform Grid + + Creates a grid where every cell is filled with a constant value for each + specified band. Useful for fuel moisture scenarios, constant fuel loads, + and other spatially-uniform inputs. + + ## Request Body + + - **resolution**: (required) Grid resolution in meters (>= 1). No default + since uniform grids have no \"native resolution.\" + - **bands**: (required) One or more bands, each with a key and value. + Band keys must be unique. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Available Bands + + **Fuel moisture** (unit: %): `fuel_moisture.1hr`, `fuel_moisture.10hr`, + `fuel_moisture.100hr`, `fuel_moisture.live_herb`, `fuel_moisture.live_woody` + + **Curing** (unit: %): `curing` + + **Fuel load** (unit: kg/m**2): `fuel_load.1hr`, `fuel_load.10hr`, + `fuel_load.100hr`, `fuel_load.live_herb`, `fuel_load.live_woody` + + **Fuel depth** (unit: m): `fuel_depth` + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + generate the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateUniformRequest): Request to create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the specified + resolution. No default resolution — it must be explicitly provided since + uniform grids have no "native resolution." + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateUniformRequest, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Create a uniform (constant-value) grid + + # Create Uniform Grid + + Creates a grid where every cell is filled with a constant value for each + specified band. Useful for fuel moisture scenarios, constant fuel loads, + and other spatially-uniform inputs. + + ## Request Body + + - **resolution**: (required) Grid resolution in meters (>= 1). No default + since uniform grids have no \"native resolution.\" + - **bands**: (required) One or more bands, each with a key and value. + Band keys must be unique. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Available Bands + + **Fuel moisture** (unit: %): `fuel_moisture.1hr`, `fuel_moisture.10hr`, + `fuel_moisture.100hr`, `fuel_moisture.live_herb`, `fuel_moisture.live_woody` + + **Curing** (unit: %): `curing` + + **Fuel load** (unit: kg/m**2): `fuel_load.1hr`, `fuel_load.10hr`, + `fuel_load.100hr`, `fuel_load.live_herb`, `fuel_load.live_woody` + + **Fuel depth** (unit: m): `fuel_depth` + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + generate the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateUniformRequest): Request to create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the specified + resolution. No default resolution — it must be explicitly provided since + uniform grids have no "native resolution." + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateUniformRequest, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Create a uniform (constant-value) grid + + # Create Uniform Grid + + Creates a grid where every cell is filled with a constant value for each + specified band. Useful for fuel moisture scenarios, constant fuel loads, + and other spatially-uniform inputs. + + ## Request Body + + - **resolution**: (required) Grid resolution in meters (>= 1). No default + since uniform grids have no \"native resolution.\" + - **bands**: (required) One or more bands, each with a key and value. + Band keys must be unique. + - **name**: (optional) Name for the grid. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing grids. + + ## Available Bands + + **Fuel moisture** (unit: %): `fuel_moisture.1hr`, `fuel_moisture.10hr`, + `fuel_moisture.100hr`, `fuel_moisture.live_herb`, `fuel_moisture.live_woody` + + **Curing** (unit: %): `curing` + + **Fuel load** (unit: kg/m**2): `fuel_load.1hr`, `fuel_load.10hr`, + `fuel_load.100hr`, `fuel_load.live_herb`, `fuel_load.live_woody` + + **Fuel depth** (unit: m): `fuel_depth` + + ## Response + + Returns the created Grid resource with status \"pending\". The backend will + generate the data and update status to \"completed\" when ready. + + Args: + domain_id (str): + body (CreateUniformRequest): Request to create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the specified + resolution. No default resolution — it must be explicitly provided since + uniform grids have no "native resolution." + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/delete_grid.py b/fastfuels_sdk/v2/client_library/api/grids/delete_grid.py new file mode 100644 index 0000000..075883b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/delete_grid.py @@ -0,0 +1,245 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/domains/{domain_id}/grids/{grid_id}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a grid + + # Delete Grid Endpoint + + Permanently deletes a grid resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a grid + + # Delete Grid Endpoint + + Permanently deletes a grid resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a grid + + # Delete Grid Endpoint + + Permanently deletes a grid resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a grid + + # Delete Grid Endpoint + + Permanently deletes a grid resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/duplicate_grid.py b/fastfuels_sdk/v2/client_library/api/grids/duplicate_grid.py new file mode 100644 index 0000000..761d9ed --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/duplicate_grid.py @@ -0,0 +1,369 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.duplicate_grid_request import DuplicateGridRequest +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + grid_id: str, + *, + body: DuplicateGridRequest | None | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/grids/{grid_id}/duplicate".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + ), + } + + if isinstance(body, DuplicateGridRequest): + _kwargs["json"] = body.to_dict() + else: + _kwargs["json"] = body + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Grid.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: DuplicateGridRequest | None | Unset = UNSET, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Duplicate a grid + + # Duplicate a Grid + + Creates an independent **copy** of a completed grid under a new ID. Use + this to branch a scenario: duplicate, then edit the copy while the + original stays untouched. + + This is a true clone, not a re-derivation. The finished data is + byte-copied; no regeneration is performed and the upstream source is never + re-fetched, so the copy is exact even if the upstream product has been + updated since the original was built. The copy carries over the source's + `source`, `modifications`, `bands`, `georeference`, `chunks`, and + `checksum` verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Grid with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/chunks`, `/data`) become + available only after the copy completes. The source grid is unchanged. + + ## Error Responses + + - **404 Not Found**: The source grid does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source grid exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (DuplicateGridRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: DuplicateGridRequest | None | Unset = UNSET, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Duplicate a grid + + # Duplicate a Grid + + Creates an independent **copy** of a completed grid under a new ID. Use + this to branch a scenario: duplicate, then edit the copy while the + original stays untouched. + + This is a true clone, not a re-derivation. The finished data is + byte-copied; no regeneration is performed and the upstream source is never + re-fetched, so the copy is exact even if the upstream product has been + updated since the original was built. The copy carries over the source's + `source`, `modifications`, `bands`, `georeference`, `chunks`, and + `checksum` verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Grid with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/chunks`, `/data`) become + available only after the copy completes. The source grid is unchanged. + + ## Error Responses + + - **404 Not Found**: The source grid does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source grid exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (DuplicateGridRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: DuplicateGridRequest | None | Unset = UNSET, +) -> Response[Grid | HTTPValidationError | QuotaExceededDetail]: + r"""Duplicate a grid + + # Duplicate a Grid + + Creates an independent **copy** of a completed grid under a new ID. Use + this to branch a scenario: duplicate, then edit the copy while the + original stays untouched. + + This is a true clone, not a re-derivation. The finished data is + byte-copied; no regeneration is performed and the upstream source is never + re-fetched, so the copy is exact even if the upstream product has been + updated since the original was built. The copy carries over the source's + `source`, `modifications`, `bands`, `georeference`, `chunks`, and + `checksum` verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Grid with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/chunks`, `/data`) become + available only after the copy completes. The source grid is unchanged. + + ## Error Responses + + - **404 Not Found**: The source grid does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source grid exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (DuplicateGridRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: DuplicateGridRequest | None | Unset = UNSET, +) -> Grid | HTTPValidationError | QuotaExceededDetail | None: + r"""Duplicate a grid + + # Duplicate a Grid + + Creates an independent **copy** of a completed grid under a new ID. Use + this to branch a scenario: duplicate, then edit the copy while the + original stays untouched. + + This is a true clone, not a re-derivation. The finished data is + byte-copied; no regeneration is performed and the upstream source is never + re-fetched, so the copy is exact even if the upstream product has been + updated since the original was built. The copy carries over the source's + `source`, `modifications`, `bands`, `georeference`, `chunks`, and + `checksum` verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Grid with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/chunks`, `/data`) become + available only after the copy completes. The source grid is unchanged. + + ## Error Responses + + - **404 Not Found**: The source grid does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source grid exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active grid jobs in progress + (your `max_active_grids` quota). Wait for jobs to complete or delete + unneeded grids, then retry. The response detail names the exact `quota` + and includes a `Retry-After` header. + + Args: + domain_id (str): + grid_id (str): + body (DuplicateGridRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/get_chunk_metadata.py b/fastfuels_sdk/v2/client_library/api/grids/get_chunk_metadata.py new file mode 100644 index 0000000..1355624 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/get_chunk_metadata.py @@ -0,0 +1,313 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid_data_chunk_metadata import GridDataChunkMetadata +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, + chunk_index: int, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids/{grid_id}/chunks/{chunk_index}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + chunk_index=quote(str(chunk_index), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> GridDataChunkMetadata | HTTPValidationError | None: + if response.status_code == 200: + response_200 = GridDataChunkMetadata.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[GridDataChunkMetadata | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + chunk_index: int, + *, + client: AuthenticatedClient, +) -> Response[GridDataChunkMetadata | HTTPValidationError]: + """Get chunk metadata + + # Get Chunk Metadata Endpoint + + Retrieves the shape, pixel offset, and affine transform for a single 2D or + 3D chunk of a completed grid. This is a lightweight call — no raster data + is read. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + - **chunk_index**: (integer) Zero-based flat chunk index. 2D grids use + y,x order. 3D grids use z,y,x order. + + ## Response + + Returns chunk metadata: + + - **index**: The chunk index. + - **shape**: 2D `(height, width)` or 3D `(z, height, width)`. Edge chunks + may be smaller than the grid's chunk shape. + - **offset**: 2D `(row, column)` or 3D `(z, row, column)` pixel offset of + the chunk within the full grid. + - **transform**: Six-element affine transform for the chunk's spatial extent. + - **z_origin**, **z_resolution**: Present only for 3D grids. + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not completed, or the user + does not have access. + - **422 Unprocessable Entity**: The chunk index is out of range. + + Args: + domain_id (str): + grid_id (str): + chunk_index (int): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridDataChunkMetadata | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + chunk_index=chunk_index, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + chunk_index: int, + *, + client: AuthenticatedClient, +) -> GridDataChunkMetadata | HTTPValidationError | None: + """Get chunk metadata + + # Get Chunk Metadata Endpoint + + Retrieves the shape, pixel offset, and affine transform for a single 2D or + 3D chunk of a completed grid. This is a lightweight call — no raster data + is read. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + - **chunk_index**: (integer) Zero-based flat chunk index. 2D grids use + y,x order. 3D grids use z,y,x order. + + ## Response + + Returns chunk metadata: + + - **index**: The chunk index. + - **shape**: 2D `(height, width)` or 3D `(z, height, width)`. Edge chunks + may be smaller than the grid's chunk shape. + - **offset**: 2D `(row, column)` or 3D `(z, row, column)` pixel offset of + the chunk within the full grid. + - **transform**: Six-element affine transform for the chunk's spatial extent. + - **z_origin**, **z_resolution**: Present only for 3D grids. + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not completed, or the user + does not have access. + - **422 Unprocessable Entity**: The chunk index is out of range. + + Args: + domain_id (str): + grid_id (str): + chunk_index (int): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridDataChunkMetadata | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + chunk_index=chunk_index, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + chunk_index: int, + *, + client: AuthenticatedClient, +) -> Response[GridDataChunkMetadata | HTTPValidationError]: + """Get chunk metadata + + # Get Chunk Metadata Endpoint + + Retrieves the shape, pixel offset, and affine transform for a single 2D or + 3D chunk of a completed grid. This is a lightweight call — no raster data + is read. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + - **chunk_index**: (integer) Zero-based flat chunk index. 2D grids use + y,x order. 3D grids use z,y,x order. + + ## Response + + Returns chunk metadata: + + - **index**: The chunk index. + - **shape**: 2D `(height, width)` or 3D `(z, height, width)`. Edge chunks + may be smaller than the grid's chunk shape. + - **offset**: 2D `(row, column)` or 3D `(z, row, column)` pixel offset of + the chunk within the full grid. + - **transform**: Six-element affine transform for the chunk's spatial extent. + - **z_origin**, **z_resolution**: Present only for 3D grids. + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not completed, or the user + does not have access. + - **422 Unprocessable Entity**: The chunk index is out of range. + + Args: + domain_id (str): + grid_id (str): + chunk_index (int): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridDataChunkMetadata | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + chunk_index=chunk_index, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + chunk_index: int, + *, + client: AuthenticatedClient, +) -> GridDataChunkMetadata | HTTPValidationError | None: + """Get chunk metadata + + # Get Chunk Metadata Endpoint + + Retrieves the shape, pixel offset, and affine transform for a single 2D or + 3D chunk of a completed grid. This is a lightweight call — no raster data + is read. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + - **chunk_index**: (integer) Zero-based flat chunk index. 2D grids use + y,x order. 3D grids use z,y,x order. + + ## Response + + Returns chunk metadata: + + - **index**: The chunk index. + - **shape**: 2D `(height, width)` or 3D `(z, height, width)`. Edge chunks + may be smaller than the grid's chunk shape. + - **offset**: 2D `(row, column)` or 3D `(z, row, column)` pixel offset of + the chunk within the full grid. + - **transform**: Six-element affine transform for the chunk's spatial extent. + - **z_origin**, **z_resolution**: Present only for 3D grids. + + ## Error Responses + + - **404 Not Found**: The grid does not exist, is not completed, or the user + does not have access. + - **422 Unprocessable Entity**: The chunk index is out of range. + + Args: + domain_id (str): + grid_id (str): + chunk_index (int): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridDataChunkMetadata | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + chunk_index=chunk_index, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/get_grid.py b/fastfuels_sdk/v2/client_library/api/grids/get_grid.py new file mode 100644 index 0000000..3ea6ce3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/get_grid.py @@ -0,0 +1,243 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids/{grid_id}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Grid.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Response[Grid | HTTPValidationError]: + """Get a grid by ID + + # Get Grid Endpoint + + Retrieves a specific grid resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique 32-character hex identifier of the grid. + + ## Response + + Returns the grid resource. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Grid | HTTPValidationError | None: + """Get a grid by ID + + # Get Grid Endpoint + + Retrieves a specific grid resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique 32-character hex identifier of the grid. + + ## Response + + Returns the grid resource. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Response[Grid | HTTPValidationError]: + """Get a grid by ID + + # Get Grid Endpoint + + Retrieves a specific grid resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique 32-character hex identifier of the grid. + + ## Response + + Returns the grid resource. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, +) -> Grid | HTTPValidationError | None: + """Get a grid by ID + + # Get Grid Endpoint + + Retrieves a specific grid resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique 32-character hex identifier of the grid. + + ## Response + + Returns the grid resource. + + ## Error Responses + + - **404 Not Found**: The grid does not exist or the user does not have access. + + Args: + domain_id (str): + grid_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_binary.py b/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_binary.py new file mode 100644 index 0000000..3e7b93f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_binary.py @@ -0,0 +1,515 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid_data_array_format import GridDataArrayFormat +from ...models.grid_data_order import GridDataOrder +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + json_array_format: str | Unset = UNSET + if not isinstance(array_format, Unset): + json_array_format = array_format.value + + params["array_format"] = json_array_format + + json_order: str | Unset = UNSET + if not isinstance(order, Unset): + json_order = order.value + + params["order"] = json_order + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids/{grid_id}/data/{band}/{chunk_index}/binary".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + band=quote(str(band), safe=""), + chunk_index=quote(str(chunk_index), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | str | None: + if response.status_code == 200: + response_200 = cast(str, response.content) + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> Response[HTTPValidationError | str]: + """Get band data for a chunk (binary) + + # Get Grid Data (binary) + + Returns the values of a single band within a single chunk of a completed + grid as raw bytes, with shape and type metadata in `X-Data-*` response + headers. Use this when you need the most compact wire format and intend + to deserialize into a typed array on the client. + + For a structured JSON payload, use the JSON variant of this endpoint + (drop the trailing `/binary`). + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + All variants return `application/octet-stream` with these common headers: + + - `X-Data-Shape`: comma-separated chunk dimensions (e.g., `47,61`). + - `X-Data-Order`: flattening order used (`C` or `F`). + - `X-Data-Format`: `dense` or `sparse`. + - `X-Data-Offset`: comma-separated pixel offset of this chunk within + the full grid (2D: `row,col`; 3D: `z,row,col`). + - `X-Data-Transform`: comma-separated six-element affine transform for + the chunk's spatial extent. + - `X-Data-Z-Origin`, `X-Data-Z-Resolution`: present only for 3D grids. + + **Dense** (`array_format=dense`): body is the flattened cells as raw + bytes. + + - `X-Data-Dtype`: numeric type of the cells (e.g., `float32`). + + **Sparse** (`array_format=sparse`): body is the index array bytes + immediately followed by the value array bytes. + + - `X-Data-NNZ`: number of non-fill entries (length of both arrays). + - `X-Data-Index-Dtype`: numeric type of the index array (`int32`). + - `X-Data-Value-Dtype`: numeric type of the value array. + - `X-Data-Fill-Value`: the band's fill value (stringified). Omitted when + the band does not define a fill value, in which case every cell is + listed and no compression has been applied. + + Slice the response body at `NNZ * sizeof(index_dtype)` to separate + indices from values. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse` + or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | str] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + array_format=array_format, + order=order, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> HTTPValidationError | str | None: + """Get band data for a chunk (binary) + + # Get Grid Data (binary) + + Returns the values of a single band within a single chunk of a completed + grid as raw bytes, with shape and type metadata in `X-Data-*` response + headers. Use this when you need the most compact wire format and intend + to deserialize into a typed array on the client. + + For a structured JSON payload, use the JSON variant of this endpoint + (drop the trailing `/binary`). + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + All variants return `application/octet-stream` with these common headers: + + - `X-Data-Shape`: comma-separated chunk dimensions (e.g., `47,61`). + - `X-Data-Order`: flattening order used (`C` or `F`). + - `X-Data-Format`: `dense` or `sparse`. + - `X-Data-Offset`: comma-separated pixel offset of this chunk within + the full grid (2D: `row,col`; 3D: `z,row,col`). + - `X-Data-Transform`: comma-separated six-element affine transform for + the chunk's spatial extent. + - `X-Data-Z-Origin`, `X-Data-Z-Resolution`: present only for 3D grids. + + **Dense** (`array_format=dense`): body is the flattened cells as raw + bytes. + + - `X-Data-Dtype`: numeric type of the cells (e.g., `float32`). + + **Sparse** (`array_format=sparse`): body is the index array bytes + immediately followed by the value array bytes. + + - `X-Data-NNZ`: number of non-fill entries (length of both arrays). + - `X-Data-Index-Dtype`: numeric type of the index array (`int32`). + - `X-Data-Value-Dtype`: numeric type of the value array. + - `X-Data-Fill-Value`: the band's fill value (stringified). Omitted when + the band does not define a fill value, in which case every cell is + listed and no compression has been applied. + + Slice the response body at `NNZ * sizeof(index_dtype)` to separate + indices from values. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse` + or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | str + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + client=client, + array_format=array_format, + order=order, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> Response[HTTPValidationError | str]: + """Get band data for a chunk (binary) + + # Get Grid Data (binary) + + Returns the values of a single band within a single chunk of a completed + grid as raw bytes, with shape and type metadata in `X-Data-*` response + headers. Use this when you need the most compact wire format and intend + to deserialize into a typed array on the client. + + For a structured JSON payload, use the JSON variant of this endpoint + (drop the trailing `/binary`). + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + All variants return `application/octet-stream` with these common headers: + + - `X-Data-Shape`: comma-separated chunk dimensions (e.g., `47,61`). + - `X-Data-Order`: flattening order used (`C` or `F`). + - `X-Data-Format`: `dense` or `sparse`. + - `X-Data-Offset`: comma-separated pixel offset of this chunk within + the full grid (2D: `row,col`; 3D: `z,row,col`). + - `X-Data-Transform`: comma-separated six-element affine transform for + the chunk's spatial extent. + - `X-Data-Z-Origin`, `X-Data-Z-Resolution`: present only for 3D grids. + + **Dense** (`array_format=dense`): body is the flattened cells as raw + bytes. + + - `X-Data-Dtype`: numeric type of the cells (e.g., `float32`). + + **Sparse** (`array_format=sparse`): body is the index array bytes + immediately followed by the value array bytes. + + - `X-Data-NNZ`: number of non-fill entries (length of both arrays). + - `X-Data-Index-Dtype`: numeric type of the index array (`int32`). + - `X-Data-Value-Dtype`: numeric type of the value array. + - `X-Data-Fill-Value`: the band's fill value (stringified). Omitted when + the band does not define a fill value, in which case every cell is + listed and no compression has been applied. + + Slice the response body at `NNZ * sizeof(index_dtype)` to separate + indices from values. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse` + or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | str] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + array_format=array_format, + order=order, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> HTTPValidationError | str | None: + """Get band data for a chunk (binary) + + # Get Grid Data (binary) + + Returns the values of a single band within a single chunk of a completed + grid as raw bytes, with shape and type metadata in `X-Data-*` response + headers. Use this when you need the most compact wire format and intend + to deserialize into a typed array on the client. + + For a structured JSON payload, use the JSON variant of this endpoint + (drop the trailing `/binary`). + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + All variants return `application/octet-stream` with these common headers: + + - `X-Data-Shape`: comma-separated chunk dimensions (e.g., `47,61`). + - `X-Data-Order`: flattening order used (`C` or `F`). + - `X-Data-Format`: `dense` or `sparse`. + - `X-Data-Offset`: comma-separated pixel offset of this chunk within + the full grid (2D: `row,col`; 3D: `z,row,col`). + - `X-Data-Transform`: comma-separated six-element affine transform for + the chunk's spatial extent. + - `X-Data-Z-Origin`, `X-Data-Z-Resolution`: present only for 3D grids. + + **Dense** (`array_format=dense`): body is the flattened cells as raw + bytes. + + - `X-Data-Dtype`: numeric type of the cells (e.g., `float32`). + + **Sparse** (`array_format=sparse`): body is the index array bytes + immediately followed by the value array bytes. + + - `X-Data-NNZ`: number of non-fill entries (length of both arrays). + - `X-Data-Index-Dtype`: numeric type of the index array (`int32`). + - `X-Data-Value-Dtype`: numeric type of the value array. + - `X-Data-Fill-Value`: the band's fill value (stringified). Omitted when + the band does not define a fill value, in which case every cell is + listed and no compression has been applied. + + Slice the response body at `NNZ * sizeof(index_dtype)` to separate + indices from values. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse` + or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | str + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + client=client, + array_format=array_format, + order=order, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_json.py b/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_json.py new file mode 100644 index 0000000..e8b50c1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/get_grid_data_json.py @@ -0,0 +1,457 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid_data_array_format import GridDataArrayFormat +from ...models.grid_data_order import GridDataOrder +from ...models.grid_data_response import GridDataResponse +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + json_array_format: str | Unset = UNSET + if not isinstance(array_format, Unset): + json_array_format = array_format.value + + params["array_format"] = json_array_format + + json_order: str | Unset = UNSET + if not isinstance(order, Unset): + json_order = order.value + + params["order"] = json_order + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids/{grid_id}/data/{band}/{chunk_index}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + band=quote(str(band), safe=""), + chunk_index=quote(str(chunk_index), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> GridDataResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = GridDataResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[GridDataResponse | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> Response[GridDataResponse | HTTPValidationError]: + r"""Get band data for a chunk (JSON) + + # Get Grid Data (JSON) + + Returns the values of a single band within a single chunk of a completed + grid as a JSON payload — either dense values or a sparse representation, + selected via `array_format`. + + For raw bytes (smaller, faster to parse), use the `/binary` variant of + this endpoint. + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + Both variants share `shape` (chunk dimensions), `order` (flattening + order used for the values), and `metadata` (the chunk's index, shape, + offset, and affine transform — and `z_origin`/`z_resolution` for 3D + grids). The CRS does not vary per chunk; read it from the grid's + `georeference.crs`. + + **Dense** (`array_format=dense`): `data.format = \"dense\"`, `data.values` + is a flat list of all cells. + + **Sparse** (`array_format=sparse`): `data.format = \"sparse\"`, + `data.indices` are flat positions of non-fill cells, `data.values` are + their values. `data.fill_value` is the band's fill value, or `null` if + the band does not define one — in which case every cell is listed and no + compression has been applied. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse`, + the `/binary` variant, or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridDataResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + array_format=array_format, + order=order, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> GridDataResponse | HTTPValidationError | None: + r"""Get band data for a chunk (JSON) + + # Get Grid Data (JSON) + + Returns the values of a single band within a single chunk of a completed + grid as a JSON payload — either dense values or a sparse representation, + selected via `array_format`. + + For raw bytes (smaller, faster to parse), use the `/binary` variant of + this endpoint. + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + Both variants share `shape` (chunk dimensions), `order` (flattening + order used for the values), and `metadata` (the chunk's index, shape, + offset, and affine transform — and `z_origin`/`z_resolution` for 3D + grids). The CRS does not vary per chunk; read it from the grid's + `georeference.crs`. + + **Dense** (`array_format=dense`): `data.format = \"dense\"`, `data.values` + is a flat list of all cells. + + **Sparse** (`array_format=sparse`): `data.format = \"sparse\"`, + `data.indices` are flat positions of non-fill cells, `data.values` are + their values. `data.fill_value` is the band's fill value, or `null` if + the band does not define one — in which case every cell is listed and no + compression has been applied. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse`, + the `/binary` variant, or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridDataResponse | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + client=client, + array_format=array_format, + order=order, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> Response[GridDataResponse | HTTPValidationError]: + r"""Get band data for a chunk (JSON) + + # Get Grid Data (JSON) + + Returns the values of a single band within a single chunk of a completed + grid as a JSON payload — either dense values or a sparse representation, + selected via `array_format`. + + For raw bytes (smaller, faster to parse), use the `/binary` variant of + this endpoint. + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + Both variants share `shape` (chunk dimensions), `order` (flattening + order used for the values), and `metadata` (the chunk's index, shape, + offset, and affine transform — and `z_origin`/`z_resolution` for 3D + grids). The CRS does not vary per chunk; read it from the grid's + `georeference.crs`. + + **Dense** (`array_format=dense`): `data.format = \"dense\"`, `data.values` + is a flat list of all cells. + + **Sparse** (`array_format=sparse`): `data.format = \"sparse\"`, + `data.indices` are flat positions of non-fill cells, `data.values` are + their values. `data.fill_value` is the band's fill value, or `null` if + the band does not define one — in which case every cell is listed and no + compression has been applied. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse`, + the `/binary` variant, or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GridDataResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + array_format=array_format, + order=order, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + band: str, + chunk_index: int, + *, + client: AuthenticatedClient, + array_format: GridDataArrayFormat | Unset = UNSET, + order: GridDataOrder | Unset = UNSET, +) -> GridDataResponse | HTTPValidationError | None: + r"""Get band data for a chunk (JSON) + + # Get Grid Data (JSON) + + Returns the values of a single band within a single chunk of a completed + grid as a JSON payload — either dense values or a sparse representation, + selected via `array_format`. + + For raw bytes (smaller, faster to parse), use the `/binary` variant of + this endpoint. + + ## Path Parameters + + - **domain_id**: The domain the grid belongs to. + - **grid_id**: The grid identifier. + - **band**: Band key to read (must be present in the grid's `bands`). + - **chunk_index**: Zero-based flat chunk index. 2D grids index in (y, x); + 3D grids index in (z, y, x). The chunk's shape, offset, and affine + transform are returned alongside the data — no separate metadata call + is required. (`GET …/chunks/{chunk_index}` is still available for + clients that want to lay out chunks before fetching data.) + + ## Query Parameters + + - **array_format**: `dense` (default) or `sparse`. Sparse compresses out + cells equal to the band's fill value, returning only the non-fill + entries. + - **order**: Flattening order — `C` (row-major, default) or `F` + (column-major). + + ## Response + + Both variants share `shape` (chunk dimensions), `order` (flattening + order used for the values), and `metadata` (the chunk's index, shape, + offset, and affine transform — and `z_origin`/`z_resolution` for 3D + grids). The CRS does not vary per chunk; read it from the grid's + `georeference.crs`. + + **Dense** (`array_format=dense`): `data.format = \"dense\"`, `data.values` + is a flat list of all cells. + + **Sparse** (`array_format=sparse`): `data.format = \"sparse\"`, + `data.indices` are flat positions of non-fill cells, `data.values` are + their values. `data.fill_value` is the band's fill value, or `null` if + the band does not define one — in which case every cell is listed and no + compression has been applied. + + ## Errors + + - **404**: Grid not found, not completed, or not accessible. + - **422**: Band does not exist on this grid, or chunk index out of range. + - **413**: Response would exceed the size limit. Try `array_format=sparse`, + the `/binary` variant, or a smaller chunk. + + Args: + domain_id (str): + grid_id (str): + band (str): + chunk_index (int): + array_format (GridDataArrayFormat | Unset): + order (GridDataOrder | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GridDataResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + band=band, + chunk_index=chunk_index, + client=client, + array_format=array_format, + order=order, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/list_grids.py b/fastfuels_sdk/v2/client_library/api/grids/list_grids.py new file mode 100644 index 0000000..a2fae31 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/list_grids.py @@ -0,0 +1,403 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid_sort_field import GridSortField +from ...models.http_validation_error import HTTPValidationError +from ...models.list_grids_response import ListGridsResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, GridSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_product: None | str | Unset + if isinstance(product, Unset): + json_product = UNSET + else: + json_product = product + params["product"] = json_product + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/grids".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListGridsResponse | None: + if response.status_code == 200: + response_200 = ListGridsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListGridsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListGridsResponse]: + """List all grids + + # List Grids Endpoint + + Retrieves a paginated list of all grids within a domain belonging to the + authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list grids for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListGridsResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListGridsResponse | None: + """List all grids + + # List Grids Endpoint + + Retrieves a paginated list of all grids within a domain belonging to the + authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list grids for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListGridsResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListGridsResponse]: + """List all grids + + # List Grids Endpoint + + Retrieves a paginated list of all grids within a domain belonging to the + authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list grids for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListGridsResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListGridsResponse | None: + """List all grids + + # List Grids Endpoint + + Retrieves a paginated list of all grids within a domain belonging to the + authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list grids for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListGridsResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/list_grids_cross_domain.py b/fastfuels_sdk/v2/client_library/api/grids/list_grids_cross_domain.py new file mode 100644 index 0000000..94ec944 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/list_grids_cross_domain.py @@ -0,0 +1,371 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid_sort_field import GridSortField +from ...models.http_validation_error import HTTPValidationError +from ...models.list_grids_response import ListGridsResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, GridSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_product: None | str | Unset + if isinstance(product, Unset): + json_product = UNSET + else: + json_product = product + params["product"] = json_product + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/-/grids", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListGridsResponse | None: + if response.status_code == 200: + response_200 = ListGridsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListGridsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListGridsResponse]: + """List grids across all domains + + # List Grids Across All Domains Endpoint + + Retrieves a paginated list of all grids across all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListGridsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListGridsResponse | None: + """List grids across all domains + + # List Grids Across All Domains Endpoint + + Retrieves a paginated list of all grids across all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListGridsResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListGridsResponse]: + """List grids across all domains + + # List Grids Across All Domains Endpoint + + Retrieves a paginated list of all grids across all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListGridsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: GridSortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + source: None | str | Unset = UNSET, + product: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListGridsResponse | None: + """List grids across all domains + + # List Grids Across All Domains Endpoint + + Retrieves a paginated list of all grids across all domains belonging to the + authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **source**: (string, optional) Filter grids by source name (e.g., `landfire`, `3dep`). + - **product**: (string, optional) Filter grids by source product (e.g., `fbfm40`, `topography`). + - **tag**: (string, optional) Filter grids that contain this tag. + + ## Response + + Returns a paginated list of grids with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of grids to retrieve per page. Default: 100. + sort_by (GridSortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + source (None | str | Unset): Filter grids by source name (e.g., 'landfire', '3dep'). + product (None | str | Unset): Filter grids by source product (e.g., 'fbfm40', + 'topography'). Requires source filter. + tag (None | str | Unset): Filter grids that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListGridsResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + source=source, + product=product, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/grids/update_grid.py b/fastfuels_sdk/v2/client_library/api/grids/update_grid.py new file mode 100644 index 0000000..9f57cd8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/grids/update_grid.py @@ -0,0 +1,328 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.grid import Grid +from ...models.http_validation_error import HTTPValidationError +from ...models.update_grid_request_body import UpdateGridRequestBody +from ...types import Response + + +def _get_kwargs( + domain_id: str, + grid_id: str, + *, + body: UpdateGridRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/domains/{domain_id}/grids/{grid_id}".format( + domain_id=quote(str(domain_id), safe=""), + grid_id=quote(str(grid_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Grid | HTTPValidationError | None: + if response.status_code == 200: + response_200 = Grid.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Grid | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: UpdateGridRequestBody, +) -> Response[Grid | HTTPValidationError]: + """Update a grid + + # Update Grid Endpoint + + Updates the metadata of an existing grid resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the grid. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **source**, **modifications**, **bands**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the grid's content is rebuilt, never via + metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated grid resource. + + Args: + domain_id (str): + grid_id (str): + body (UpdateGridRequestBody): Request body for updating grid metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: UpdateGridRequestBody, +) -> Grid | HTTPValidationError | None: + """Update a grid + + # Update Grid Endpoint + + Updates the metadata of an existing grid resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the grid. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **source**, **modifications**, **bands**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the grid's content is rebuilt, never via + metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated grid resource. + + Args: + domain_id (str): + grid_id (str): + body (UpdateGridRequestBody): Request body for updating grid metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: UpdateGridRequestBody, +) -> Response[Grid | HTTPValidationError]: + """Update a grid + + # Update Grid Endpoint + + Updates the metadata of an existing grid resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the grid. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **source**, **modifications**, **bands**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the grid's content is rebuilt, never via + metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated grid resource. + + Args: + domain_id (str): + grid_id (str): + body (UpdateGridRequestBody): Request body for updating grid metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Grid | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + grid_id=grid_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + grid_id: str, + *, + client: AuthenticatedClient, + body: UpdateGridRequestBody, +) -> Grid | HTTPValidationError | None: + """Update a grid + + # Update Grid Endpoint + + Updates the metadata of an existing grid resource. Only the fields provided + in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the grid belongs to. + - **grid_id**: (string) The unique identifier of the grid. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the grid. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **source**, **modifications**, **bands**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the grid's content is rebuilt, never via + metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated grid resource. + + Args: + domain_id (str): + grid_id (str): + body (UpdateGridRequestBody): Request body for updating grid metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Grid | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + grid_id=grid_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/index/__init__.py b/fastfuels_sdk/v2/client_library/api/index/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/index/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/index/index.py b/fastfuels_sdk/v2/client_library/api/index/index.py new file mode 100644 index 0000000..7a87374 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/index/index.py @@ -0,0 +1,91 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """Welcome / service metadata + + Returns service metadata, documentation links, and the current deployment status. Useful as a + liveness check and as a starting point for discovering the API. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """Welcome / service metadata + + Returns service metadata, documentation links, and the current deployment status. Useful as a + liveness check and as a starting point for discovering the API. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/fastfuels_sdk/v2/client_library/api/inventories/__init__.py b/fastfuels_sdk/v2/client_library/api/inventories/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/inventories/apply_modifications.py b/fastfuels_sdk/v2/client_library/api/inventories/apply_modifications.py new file mode 100644 index 0000000..650e823 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/apply_modifications.py @@ -0,0 +1,550 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.apply_modifications_request import ApplyModificationsRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + *, + body: ApplyModificationsRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/{inventory_id}/modifications".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 200: + response_200 = Inventory.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyModificationsRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Apply modifications to an inventory in place + + # Apply Modifications to an Inventory (in place) + + Applies modifications to **this** inventory in place — the inventory keeps + its ID and the submitted rules are appended to its cumulative + `modifications` list, then the tree data is re-derived asynchronously. To + keep the original data instead, duplicate the inventory first + (`POST .../{inventory_id}/duplicate`) and modify the copy. + + Modifications filter trees by conditions and apply actions (remove, + multiply, divide, add, subtract, replace) to matching rows. Conditions + within a single rule are ANDed together; multiple rules are evaluated + independently in order. + + ## Conditions + + **Attribute conditions** compare a single tree attribute against a value: + - `attribute`: one of `dbh`, `height`, `crown_ratio`, `fia_species_code` + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`fia_species_code` only supports `eq`/`ne`) + - `value`: number, string, or list for `eq`/`ne` + - `unit`: (optional) pint-compatible unit string (e.g., `\"in\"`, `\"ft\"`) + + **Expression conditions** use a boolean expression: + - `expression`: e.g., `\"dbh < 5 and height < 2\"` + - Only `dbh`, `height`, `crown_ratio` are allowed in expressions + - Expressions always use native units (cm, m, 0-1 fraction) + + **Spatial conditions** test each tree's location (a point) against a + geometry. Two variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this inventory and be in `completed` status; + cross-domain, missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. Effectively required for + linestring features (e.g. roads) because a tree point almost never + intersects a bare linestring. + + Spatial conditions have **no `target` field** — trees are points, so + the test is always point-in-(optionally-buffered)-geometry. + + Spatial and attribute conditions can be combined in a single rule + (AND semantics). For example: `{conditions: [feature within road + buffer, dbh > 30], actions: [remove]}` removes only large trees that + fall inside the buffered road. + + ## Actions + + - `{\"modifier\": \"remove\"}` — remove matching trees (must be sole action) + - `{\"attribute\": \"...\", \"modifier\": \"multiply|divide|add|subtract|replace\", \"value\": ...}` + - `unit` on actions converts the value before applying + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted rules appear in the inventory's + `modifications` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed modification); or a referenced `feature_id` + is missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyModificationsRequest): Request body for applying modifications to an inventory + in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyModificationsRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Apply modifications to an inventory in place + + # Apply Modifications to an Inventory (in place) + + Applies modifications to **this** inventory in place — the inventory keeps + its ID and the submitted rules are appended to its cumulative + `modifications` list, then the tree data is re-derived asynchronously. To + keep the original data instead, duplicate the inventory first + (`POST .../{inventory_id}/duplicate`) and modify the copy. + + Modifications filter trees by conditions and apply actions (remove, + multiply, divide, add, subtract, replace) to matching rows. Conditions + within a single rule are ANDed together; multiple rules are evaluated + independently in order. + + ## Conditions + + **Attribute conditions** compare a single tree attribute against a value: + - `attribute`: one of `dbh`, `height`, `crown_ratio`, `fia_species_code` + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`fia_species_code` only supports `eq`/`ne`) + - `value`: number, string, or list for `eq`/`ne` + - `unit`: (optional) pint-compatible unit string (e.g., `\"in\"`, `\"ft\"`) + + **Expression conditions** use a boolean expression: + - `expression`: e.g., `\"dbh < 5 and height < 2\"` + - Only `dbh`, `height`, `crown_ratio` are allowed in expressions + - Expressions always use native units (cm, m, 0-1 fraction) + + **Spatial conditions** test each tree's location (a point) against a + geometry. Two variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this inventory and be in `completed` status; + cross-domain, missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. Effectively required for + linestring features (e.g. roads) because a tree point almost never + intersects a bare linestring. + + Spatial conditions have **no `target` field** — trees are points, so + the test is always point-in-(optionally-buffered)-geometry. + + Spatial and attribute conditions can be combined in a single rule + (AND semantics). For example: `{conditions: [feature within road + buffer, dbh > 30], actions: [remove]}` removes only large trees that + fall inside the buffered road. + + ## Actions + + - `{\"modifier\": \"remove\"}` — remove matching trees (must be sole action) + - `{\"attribute\": \"...\", \"modifier\": \"multiply|divide|add|subtract|replace\", \"value\": ...}` + - `unit` on actions converts the value before applying + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted rules appear in the inventory's + `modifications` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed modification); or a referenced `feature_id` + is missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyModificationsRequest): Request body for applying modifications to an inventory + in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyModificationsRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Apply modifications to an inventory in place + + # Apply Modifications to an Inventory (in place) + + Applies modifications to **this** inventory in place — the inventory keeps + its ID and the submitted rules are appended to its cumulative + `modifications` list, then the tree data is re-derived asynchronously. To + keep the original data instead, duplicate the inventory first + (`POST .../{inventory_id}/duplicate`) and modify the copy. + + Modifications filter trees by conditions and apply actions (remove, + multiply, divide, add, subtract, replace) to matching rows. Conditions + within a single rule are ANDed together; multiple rules are evaluated + independently in order. + + ## Conditions + + **Attribute conditions** compare a single tree attribute against a value: + - `attribute`: one of `dbh`, `height`, `crown_ratio`, `fia_species_code` + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`fia_species_code` only supports `eq`/`ne`) + - `value`: number, string, or list for `eq`/`ne` + - `unit`: (optional) pint-compatible unit string (e.g., `\"in\"`, `\"ft\"`) + + **Expression conditions** use a boolean expression: + - `expression`: e.g., `\"dbh < 5 and height < 2\"` + - Only `dbh`, `height`, `crown_ratio` are allowed in expressions + - Expressions always use native units (cm, m, 0-1 fraction) + + **Spatial conditions** test each tree's location (a point) against a + geometry. Two variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this inventory and be in `completed` status; + cross-domain, missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. Effectively required for + linestring features (e.g. roads) because a tree point almost never + intersects a bare linestring. + + Spatial conditions have **no `target` field** — trees are points, so + the test is always point-in-(optionally-buffered)-geometry. + + Spatial and attribute conditions can be combined in a single rule + (AND semantics). For example: `{conditions: [feature within road + buffer, dbh > 30], actions: [remove]}` removes only large trees that + fall inside the buffered road. + + ## Actions + + - `{\"modifier\": \"remove\"}` — remove matching trees (must be sole action) + - `{\"attribute\": \"...\", \"modifier\": \"multiply|divide|add|subtract|replace\", \"value\": ...}` + - `unit` on actions converts the value before applying + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted rules appear in the inventory's + `modifications` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed modification); or a referenced `feature_id` + is missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyModificationsRequest): Request body for applying modifications to an inventory + in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyModificationsRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Apply modifications to an inventory in place + + # Apply Modifications to an Inventory (in place) + + Applies modifications to **this** inventory in place — the inventory keeps + its ID and the submitted rules are appended to its cumulative + `modifications` list, then the tree data is re-derived asynchronously. To + keep the original data instead, duplicate the inventory first + (`POST .../{inventory_id}/duplicate`) and modify the copy. + + Modifications filter trees by conditions and apply actions (remove, + multiply, divide, add, subtract, replace) to matching rows. Conditions + within a single rule are ANDed together; multiple rules are evaluated + independently in order. + + ## Conditions + + **Attribute conditions** compare a single tree attribute against a value: + - `attribute`: one of `dbh`, `height`, `crown_ratio`, `fia_species_code` + - `operator`: `eq`, `ne`, `gt`, `lt`, `ge`, `le` + (`fia_species_code` only supports `eq`/`ne`) + - `value`: number, string, or list for `eq`/`ne` + - `unit`: (optional) pint-compatible unit string (e.g., `\"in\"`, `\"ft\"`) + + **Expression conditions** use a boolean expression: + - `expression`: e.g., `\"dbh < 5 and height < 2\"` + - Only `dbh`, `height`, `crown_ratio` are allowed in expressions + - Expressions always use native units (cm, m, 0-1 fraction) + + **Spatial conditions** test each tree's location (a point) against a + geometry. Two variants discriminated by the required `source` field: + + - `source: \"geometry\"` — supply GeoJSON directly via `geometry` (plus + optional `crs`; defaults to the domain CRS). + - `source: \"feature\"` — reference a persisted Feature resource by + `feature_id` (road, water, layerset). The Feature must belong to the + same domain as this inventory and be in `completed` status; + cross-domain, missing, or unfinished references are rejected with 422. + + Both spatial variants accept: + - `operator`: `within`, `outside`, or `intersects` + - `buffer_m`: (optional, meters) expands the geometry outward in the + domain's projected CRS before testing. Effectively required for + linestring features (e.g. roads) because a tree point almost never + intersects a bare linestring. + + Spatial conditions have **no `target` field** — trees are points, so + the test is always point-in-(optionally-buffered)-geometry. + + Spatial and attribute conditions can be combined in a single rule + (AND semantics). For example: `{conditions: [feature within road + buffer, dbh > 30], actions: [remove]}` removes only large trees that + fall inside the buffered road. + + ## Actions + + - `{\"modifier\": \"remove\"}` — remove matching trees (must be sole action) + - `{\"attribute\": \"...\", \"modifier\": \"multiply|divide|add|subtract|replace\", \"value\": ...}` + - `unit` on actions converts the value before applying + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted rules appear in the inventory's + `modifications` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued rules are retained — + submit another POST to retry (the new rules are applied together with the + retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed modification); or a referenced `feature_id` + is missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyModificationsRequest): Request body for applying modifications to an inventory + in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/apply_treatments.py b/fastfuels_sdk/v2/client_library/api/inventories/apply_treatments.py new file mode 100644 index 0000000..58dceec --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/apply_treatments.py @@ -0,0 +1,534 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.apply_treatments_request import ApplyTreatmentsRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + *, + body: ApplyTreatmentsRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/{inventory_id}/treatments".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 200: + response_200 = Inventory.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyTreatmentsRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Apply treatments to an inventory in place + + # Apply Treatments to an Inventory (in place) + + Applies silvicultural treatments to **this** inventory in place — the + inventory keeps its ID and the submitted treatments are appended to its + cumulative `treatments` list, then the tree data is re-derived + asynchronously. To keep the original data instead, duplicate the inventory + first (`POST .../{inventory_id}/duplicate`) and treat the copy. + + A treatment thins the stand toward a target metric using a tree-selection + method. Treatments compose: each is applied to the result of the previous. + + ## Metrics + + Each treatment is discriminated by its `metric`: + + - `diameter` — thin to a diameter-at-breast-height limit (in cm unless + `unit` is set). `from_below` removes trees smaller than the limit; + `from_above` removes trees larger than it. + - `basal_area` — thin to a residual basal area (in `m**2/ha` unless `unit` + is set). `from_below`/`from_above` remove the smallest/largest trees first + until the target is reached; `proportional` removes across all diameter + classes, preserving the diameter distribution. + + `proportional` is only valid for a basal-area target — it is not an option + for a diameter limit. + + ## Units + + `value` uses the metric's native unit (`cm` for diameter, `m**2/ha` for + basal area) unless an optional `unit` is supplied. A supplied `unit` must be + canonical and dimensionally compatible with the native unit; it is converted + before the treatment is applied. + + ## Spatial scoping + + An optional `conditions` list restricts the treatment to a region + (`within`/`outside`/`intersects` a geometry or a referenced Feature, with an + optional `buffer_m`). An empty/omitted list treats the entire inventory. A + referenced Feature must belong to the same domain as this inventory and be + in `completed` status; cross-domain, missing, or unfinished references are + rejected with 422. + + Because a basal-area treatment holds its entire treated population in memory + at once, an inventory-wide basal-area treatment over a very large domain is + rejected with 422 — scope it with a spatial condition. + + ## Requirements + + Treatments thin against tree diameter, so the inventory must have a `dbh` + column. Inventories derived from a canopy height model (CHM) carry only + height and position, so treatments cannot be applied to them (422). + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted treatments appear in the inventory's + `treatments` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued treatments are + retained — submit another POST to retry (the new treatments are applied + together with the retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed treatment); the inventory has no `dbh` + column to thin against (e.g. CHM-derived); an inventory-wide basal-area + treatment over a very large domain; or a referenced `feature_id` is + missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyTreatmentsRequest): Request body for applying treatments to an inventory in + place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyTreatmentsRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Apply treatments to an inventory in place + + # Apply Treatments to an Inventory (in place) + + Applies silvicultural treatments to **this** inventory in place — the + inventory keeps its ID and the submitted treatments are appended to its + cumulative `treatments` list, then the tree data is re-derived + asynchronously. To keep the original data instead, duplicate the inventory + first (`POST .../{inventory_id}/duplicate`) and treat the copy. + + A treatment thins the stand toward a target metric using a tree-selection + method. Treatments compose: each is applied to the result of the previous. + + ## Metrics + + Each treatment is discriminated by its `metric`: + + - `diameter` — thin to a diameter-at-breast-height limit (in cm unless + `unit` is set). `from_below` removes trees smaller than the limit; + `from_above` removes trees larger than it. + - `basal_area` — thin to a residual basal area (in `m**2/ha` unless `unit` + is set). `from_below`/`from_above` remove the smallest/largest trees first + until the target is reached; `proportional` removes across all diameter + classes, preserving the diameter distribution. + + `proportional` is only valid for a basal-area target — it is not an option + for a diameter limit. + + ## Units + + `value` uses the metric's native unit (`cm` for diameter, `m**2/ha` for + basal area) unless an optional `unit` is supplied. A supplied `unit` must be + canonical and dimensionally compatible with the native unit; it is converted + before the treatment is applied. + + ## Spatial scoping + + An optional `conditions` list restricts the treatment to a region + (`within`/`outside`/`intersects` a geometry or a referenced Feature, with an + optional `buffer_m`). An empty/omitted list treats the entire inventory. A + referenced Feature must belong to the same domain as this inventory and be + in `completed` status; cross-domain, missing, or unfinished references are + rejected with 422. + + Because a basal-area treatment holds its entire treated population in memory + at once, an inventory-wide basal-area treatment over a very large domain is + rejected with 422 — scope it with a spatial condition. + + ## Requirements + + Treatments thin against tree diameter, so the inventory must have a `dbh` + column. Inventories derived from a canopy height model (CHM) carry only + height and position, so treatments cannot be applied to them (422). + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted treatments appear in the inventory's + `treatments` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued treatments are + retained — submit another POST to retry (the new treatments are applied + together with the retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed treatment); the inventory has no `dbh` + column to thin against (e.g. CHM-derived); an inventory-wide basal-area + treatment over a very large domain; or a referenced `feature_id` is + missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyTreatmentsRequest): Request body for applying treatments to an inventory in + place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyTreatmentsRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Apply treatments to an inventory in place + + # Apply Treatments to an Inventory (in place) + + Applies silvicultural treatments to **this** inventory in place — the + inventory keeps its ID and the submitted treatments are appended to its + cumulative `treatments` list, then the tree data is re-derived + asynchronously. To keep the original data instead, duplicate the inventory + first (`POST .../{inventory_id}/duplicate`) and treat the copy. + + A treatment thins the stand toward a target metric using a tree-selection + method. Treatments compose: each is applied to the result of the previous. + + ## Metrics + + Each treatment is discriminated by its `metric`: + + - `diameter` — thin to a diameter-at-breast-height limit (in cm unless + `unit` is set). `from_below` removes trees smaller than the limit; + `from_above` removes trees larger than it. + - `basal_area` — thin to a residual basal area (in `m**2/ha` unless `unit` + is set). `from_below`/`from_above` remove the smallest/largest trees first + until the target is reached; `proportional` removes across all diameter + classes, preserving the diameter distribution. + + `proportional` is only valid for a basal-area target — it is not an option + for a diameter limit. + + ## Units + + `value` uses the metric's native unit (`cm` for diameter, `m**2/ha` for + basal area) unless an optional `unit` is supplied. A supplied `unit` must be + canonical and dimensionally compatible with the native unit; it is converted + before the treatment is applied. + + ## Spatial scoping + + An optional `conditions` list restricts the treatment to a region + (`within`/`outside`/`intersects` a geometry or a referenced Feature, with an + optional `buffer_m`). An empty/omitted list treats the entire inventory. A + referenced Feature must belong to the same domain as this inventory and be + in `completed` status; cross-domain, missing, or unfinished references are + rejected with 422. + + Because a basal-area treatment holds its entire treated population in memory + at once, an inventory-wide basal-area treatment over a very large domain is + rejected with 422 — scope it with a spatial condition. + + ## Requirements + + Treatments thin against tree diameter, so the inventory must have a `dbh` + column. Inventories derived from a canopy height model (CHM) carry only + height and position, so treatments cannot be applied to them (422). + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted treatments appear in the inventory's + `treatments` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued treatments are + retained — submit another POST to retry (the new treatments are applied + together with the retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed treatment); the inventory has no `dbh` + column to thin against (e.g. CHM-derived); an inventory-wide basal-area + treatment over a very large domain; or a referenced `feature_id` is + missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyTreatmentsRequest): Request body for applying treatments to an inventory in + place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: ApplyTreatmentsRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Apply treatments to an inventory in place + + # Apply Treatments to an Inventory (in place) + + Applies silvicultural treatments to **this** inventory in place — the + inventory keeps its ID and the submitted treatments are appended to its + cumulative `treatments` list, then the tree data is re-derived + asynchronously. To keep the original data instead, duplicate the inventory + first (`POST .../{inventory_id}/duplicate`) and treat the copy. + + A treatment thins the stand toward a target metric using a tree-selection + method. Treatments compose: each is applied to the result of the previous. + + ## Metrics + + Each treatment is discriminated by its `metric`: + + - `diameter` — thin to a diameter-at-breast-height limit (in cm unless + `unit` is set). `from_below` removes trees smaller than the limit; + `from_above` removes trees larger than it. + - `basal_area` — thin to a residual basal area (in `m**2/ha` unless `unit` + is set). `from_below`/`from_above` remove the smallest/largest trees first + until the target is reached; `proportional` removes across all diameter + classes, preserving the diameter distribution. + + `proportional` is only valid for a basal-area target — it is not an option + for a diameter limit. + + ## Units + + `value` uses the metric's native unit (`cm` for diameter, `m**2/ha` for + basal area) unless an optional `unit` is supplied. A supplied `unit` must be + canonical and dimensionally compatible with the native unit; it is converted + before the treatment is applied. + + ## Spatial scoping + + An optional `conditions` list restricts the treatment to a region + (`within`/`outside`/`intersects` a geometry or a referenced Feature, with an + optional `buffer_m`). An empty/omitted list treats the entire inventory. A + referenced Feature must belong to the same domain as this inventory and be + in `completed` status; cross-domain, missing, or unfinished references are + rejected with 422. + + Because a basal-area treatment holds its entire treated population in memory + at once, an inventory-wide basal-area treatment over a very large domain is + rejected with 422 — scope it with a spatial condition. + + ## Requirements + + Treatments thin against tree diameter, so the inventory must have a `dbh` + column. Inventories derived from a canopy height model (CHM) carry only + height and position, so treatments cannot be applied to them (422). + + ## Response + + Returns this inventory (same ID) with status `\"pending\"`. Its `checksum` + changes immediately, so any resource derived from it can detect that the + source has changed. The submitted treatments appear in the inventory's + `treatments` list once processing completes — poll the inventory until + status returns to `\"completed\"`. + + If processing fails, the inventory's status becomes `\"failed\"` with error + details, the stored data is unchanged, and the queued treatments are + retained — submit another POST to retry (the new treatments are applied + together with the retained ones). + + ## Error Responses + + - **404 Not Found**: The inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The inventory is not in `completed` status + (and is not a retryable failed treatment); the inventory has no `dbh` + column to thin against (e.g. CHM-derived); an inventory-wide basal-area + treatment over a very large domain; or a referenced `feature_id` is + missing, cross-domain, or not completed. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (ApplyTreatmentsRequest): Request body for applying treatments to an inventory in + place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/create_chm_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/create_chm_inventory.py new file mode 100644 index 0000000..a549b76 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/create_chm_inventory.py @@ -0,0 +1,296 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_chm_inventory_request import CreateChmInventoryRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateChmInventoryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/tree/chm".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Inventory.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateChmInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory from a Canopy Height Model (CHM) + + # Create CHM Extraction Inventory + + Extracts individual tree records from a Canopy Height Model (CHM) grid + using a specified stem isolation algorithm. + + Currently supports two algorithms: + 1. **Local Maximum Filtering (LMF)**: Sweeps a fixed circular window across the CHM. + 2. **Variable Window Filtering (VWF)**: Sweeps a dynamic window that scales in size based on the + height of the canopy, allowing for better detection of mixed stand structures. + + ## Request Body + + - **source_chm_grid_id**: (required) ID of a completed CHM grid. + - **algorithm**: (optional) Configuration for the stem isolation algorithm. Must specify `\"name\": + \"lmf\"` or `\"name\": \"vwf\"`. Defaults to LMF. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateChmInventoryRequest): Request body for creating an inventory via CHM + extraction. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateChmInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory from a Canopy Height Model (CHM) + + # Create CHM Extraction Inventory + + Extracts individual tree records from a Canopy Height Model (CHM) grid + using a specified stem isolation algorithm. + + Currently supports two algorithms: + 1. **Local Maximum Filtering (LMF)**: Sweeps a fixed circular window across the CHM. + 2. **Variable Window Filtering (VWF)**: Sweeps a dynamic window that scales in size based on the + height of the canopy, allowing for better detection of mixed stand structures. + + ## Request Body + + - **source_chm_grid_id**: (required) ID of a completed CHM grid. + - **algorithm**: (optional) Configuration for the stem isolation algorithm. Must specify `\"name\": + \"lmf\"` or `\"name\": \"vwf\"`. Defaults to LMF. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateChmInventoryRequest): Request body for creating an inventory via CHM + extraction. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateChmInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory from a Canopy Height Model (CHM) + + # Create CHM Extraction Inventory + + Extracts individual tree records from a Canopy Height Model (CHM) grid + using a specified stem isolation algorithm. + + Currently supports two algorithms: + 1. **Local Maximum Filtering (LMF)**: Sweeps a fixed circular window across the CHM. + 2. **Variable Window Filtering (VWF)**: Sweeps a dynamic window that scales in size based on the + height of the canopy, allowing for better detection of mixed stand structures. + + ## Request Body + + - **source_chm_grid_id**: (required) ID of a completed CHM grid. + - **algorithm**: (optional) Configuration for the stem isolation algorithm. Must specify `\"name\": + \"lmf\"` or `\"name\": \"vwf\"`. Defaults to LMF. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateChmInventoryRequest): Request body for creating an inventory via CHM + extraction. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateChmInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory from a Canopy Height Model (CHM) + + # Create CHM Extraction Inventory + + Extracts individual tree records from a Canopy Height Model (CHM) grid + using a specified stem isolation algorithm. + + Currently supports two algorithms: + 1. **Local Maximum Filtering (LMF)**: Sweeps a fixed circular window across the CHM. + 2. **Variable Window Filtering (VWF)**: Sweeps a dynamic window that scales in size based on the + height of the canopy, allowing for better detection of mixed stand structures. + + ## Request Body + + - **source_chm_grid_id**: (required) ID of a completed CHM grid. + - **algorithm**: (optional) Configuration for the stem isolation algorithm. Must specify `\"name\": + \"lmf\"` or `\"name\": \"vwf\"`. Defaults to LMF. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the extraction asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreateChmInventoryRequest): Request body for creating an inventory via CHM + extraction. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/create_gdam_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/create_gdam_inventory.py new file mode 100644 index 0000000..26306a6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/create_gdam_inventory.py @@ -0,0 +1,392 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_gdam_inventory_request import CreateGdamInventoryRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateGdamInventoryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/tree/allometry/gdam".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Inventory.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGdamInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory by filling in another via GDAM + + # Create GDAM Allometry Inventory + + **GDAM (Generalized Dendro Allometric Model)** is a machine-learning model + that predicts tree morphology — diameter at breast height, live crown ratio, + and species — from simple stem metrics (position and height). It replaces + legacy region-specific allometric equations with a single generative model: + each tree is routed to a region-specific model by geography, and a masked + tabular autoencoder fills in every missing field in one pass while preserving + any values you already supply. + + This endpoint creates a new tree inventory by calling the GDAM API to fill in + the missing morphology columns (diameter, crown ratio, species) of an existing + tree inventory. + + The typical input is an uploaded **position + height** inventory (`x`, `y`, + `height`). GDAM predicts the missing fields; any values already present are + preserved and passed to GDAM as conditioning inputs. + + ## Request Body + + - **source_tree_inventory_id**: (required) ID of a completed tree inventory to + fill in. + - **impute_columns**: (optional) Which morphology columns to impute. Defaults + to all of ``dbh``, ``crown_ratio``, ``fia_species_code``. Narrow it (e.g. + ``[\"fia_species_code\"]``) to impute fewer columns and write less to disk; + columns left out are not imputed. Must be non-empty with no duplicates. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Columns + + **Required on the source inventory** (the typical uploaded position+height set): + + - **x**, **y**: tree position, in the domain CRS. + - **height**: tree height, in metres (``m``). + + **Imputable by GDAM** (select via ``impute_columns``) — filled only where + missing; existing values are preserved: + + - **dbh**: diameter at breast height, in centimetres (``cm``). + - **crown_ratio**: live crown ratio, as a 0–1 fraction. + - **fia_species_code**: FIA species code. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The backend + (Standgen) calls GDAM asynchronously and updates status to ``\"completed\"`` when + ready. + + Args: + domain_id (str): + body (CreateGdamInventoryRequest): Request body for creating an inventory via GDAM + allometry imputation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGdamInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory by filling in another via GDAM + + # Create GDAM Allometry Inventory + + **GDAM (Generalized Dendro Allometric Model)** is a machine-learning model + that predicts tree morphology — diameter at breast height, live crown ratio, + and species — from simple stem metrics (position and height). It replaces + legacy region-specific allometric equations with a single generative model: + each tree is routed to a region-specific model by geography, and a masked + tabular autoencoder fills in every missing field in one pass while preserving + any values you already supply. + + This endpoint creates a new tree inventory by calling the GDAM API to fill in + the missing morphology columns (diameter, crown ratio, species) of an existing + tree inventory. + + The typical input is an uploaded **position + height** inventory (`x`, `y`, + `height`). GDAM predicts the missing fields; any values already present are + preserved and passed to GDAM as conditioning inputs. + + ## Request Body + + - **source_tree_inventory_id**: (required) ID of a completed tree inventory to + fill in. + - **impute_columns**: (optional) Which morphology columns to impute. Defaults + to all of ``dbh``, ``crown_ratio``, ``fia_species_code``. Narrow it (e.g. + ``[\"fia_species_code\"]``) to impute fewer columns and write less to disk; + columns left out are not imputed. Must be non-empty with no duplicates. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Columns + + **Required on the source inventory** (the typical uploaded position+height set): + + - **x**, **y**: tree position, in the domain CRS. + - **height**: tree height, in metres (``m``). + + **Imputable by GDAM** (select via ``impute_columns``) — filled only where + missing; existing values are preserved: + + - **dbh**: diameter at breast height, in centimetres (``cm``). + - **crown_ratio**: live crown ratio, as a 0–1 fraction. + - **fia_species_code**: FIA species code. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The backend + (Standgen) calls GDAM asynchronously and updates status to ``\"completed\"`` when + ready. + + Args: + domain_id (str): + body (CreateGdamInventoryRequest): Request body for creating an inventory via GDAM + allometry imputation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGdamInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory by filling in another via GDAM + + # Create GDAM Allometry Inventory + + **GDAM (Generalized Dendro Allometric Model)** is a machine-learning model + that predicts tree morphology — diameter at breast height, live crown ratio, + and species — from simple stem metrics (position and height). It replaces + legacy region-specific allometric equations with a single generative model: + each tree is routed to a region-specific model by geography, and a masked + tabular autoencoder fills in every missing field in one pass while preserving + any values you already supply. + + This endpoint creates a new tree inventory by calling the GDAM API to fill in + the missing morphology columns (diameter, crown ratio, species) of an existing + tree inventory. + + The typical input is an uploaded **position + height** inventory (`x`, `y`, + `height`). GDAM predicts the missing fields; any values already present are + preserved and passed to GDAM as conditioning inputs. + + ## Request Body + + - **source_tree_inventory_id**: (required) ID of a completed tree inventory to + fill in. + - **impute_columns**: (optional) Which morphology columns to impute. Defaults + to all of ``dbh``, ``crown_ratio``, ``fia_species_code``. Narrow it (e.g. + ``[\"fia_species_code\"]``) to impute fewer columns and write less to disk; + columns left out are not imputed. Must be non-empty with no duplicates. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Columns + + **Required on the source inventory** (the typical uploaded position+height set): + + - **x**, **y**: tree position, in the domain CRS. + - **height**: tree height, in metres (``m``). + + **Imputable by GDAM** (select via ``impute_columns``) — filled only where + missing; existing values are preserved: + + - **dbh**: diameter at breast height, in centimetres (``cm``). + - **crown_ratio**: live crown ratio, as a 0–1 fraction. + - **fia_species_code**: FIA species code. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The backend + (Standgen) calls GDAM asynchronously and updates status to ``\"completed\"`` when + ready. + + Args: + domain_id (str): + body (CreateGdamInventoryRequest): Request body for creating an inventory via GDAM + allometry imputation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateGdamInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory by filling in another via GDAM + + # Create GDAM Allometry Inventory + + **GDAM (Generalized Dendro Allometric Model)** is a machine-learning model + that predicts tree morphology — diameter at breast height, live crown ratio, + and species — from simple stem metrics (position and height). It replaces + legacy region-specific allometric equations with a single generative model: + each tree is routed to a region-specific model by geography, and a masked + tabular autoencoder fills in every missing field in one pass while preserving + any values you already supply. + + This endpoint creates a new tree inventory by calling the GDAM API to fill in + the missing morphology columns (diameter, crown ratio, species) of an existing + tree inventory. + + The typical input is an uploaded **position + height** inventory (`x`, `y`, + `height`). GDAM predicts the missing fields; any values already present are + preserved and passed to GDAM as conditioning inputs. + + ## Request Body + + - **source_tree_inventory_id**: (required) ID of a completed tree inventory to + fill in. + - **impute_columns**: (optional) Which morphology columns to impute. Defaults + to all of ``dbh``, ``crown_ratio``, ``fia_species_code``. Narrow it (e.g. + ``[\"fia_species_code\"]``) to impute fewer columns and write less to disk; + columns left out are not imputed. Must be non-empty with no duplicates. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Columns + + **Required on the source inventory** (the typical uploaded position+height set): + + - **x**, **y**: tree position, in the domain CRS. + - **height**: tree height, in metres (``m``). + + **Imputable by GDAM** (select via ``impute_columns``) — filled only where + missing; existing values are preserved: + + - **dbh**: diameter at breast height, in centimetres (``cm``). + - **crown_ratio**: live crown ratio, as a 0–1 fraction. + - **fia_species_code**: FIA species code. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The backend + (Standgen) calls GDAM asynchronously and updates status to ``\"completed\"`` when + ready. + + Args: + domain_id (str): + body (CreateGdamInventoryRequest): Request body for creating an inventory via GDAM + allometry imputation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_export.py b/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_export.py new file mode 100644 index 0000000..d71d8c6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_export.py @@ -0,0 +1,273 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.export import Export +from ...models.export_inventory_request import ExportInventoryRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_export_format import InventoryExportFormat +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + format_: InventoryExportFormat, + *, + body: ExportInventoryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/{inventory_id}/exports/{format_}".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + format_=quote(str(format_), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Export.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + format_: InventoryExportFormat, + *, + client: AuthenticatedClient, + body: ExportInventoryRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export an inventory + + Export an inventory to the specified format. + + Supported formats: `parquet` (zipped), `csv`, `geojson`, `geopackage`. + + The inventory must belong to this domain and have status `completed`. + If `columns` is specified, only those columns are included; otherwise + all columns are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + inventory_id (str): + format_ (InventoryExportFormat): Supported inventory export formats. + body (ExportInventoryRequest): Request body for creating an inventory export. + + Used at: POST /domains/{domain_id}/inventories/{inventory_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + format_=format_, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + format_: InventoryExportFormat, + *, + client: AuthenticatedClient, + body: ExportInventoryRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export an inventory + + Export an inventory to the specified format. + + Supported formats: `parquet` (zipped), `csv`, `geojson`, `geopackage`. + + The inventory must belong to this domain and have status `completed`. + If `columns` is specified, only those columns are included; otherwise + all columns are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + inventory_id (str): + format_ (InventoryExportFormat): Supported inventory export formats. + body (ExportInventoryRequest): Request body for creating an inventory export. + + Used at: POST /domains/{domain_id}/inventories/{inventory_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + format_=format_, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + format_: InventoryExportFormat, + *, + client: AuthenticatedClient, + body: ExportInventoryRequest, +) -> Response[Export | HTTPValidationError | QuotaExceededDetail]: + """Export an inventory + + Export an inventory to the specified format. + + Supported formats: `parquet` (zipped), `csv`, `geojson`, `geopackage`. + + The inventory must belong to this domain and have status `completed`. + If `columns` is specified, only those columns are included; otherwise + all columns are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + inventory_id (str): + format_ (InventoryExportFormat): Supported inventory export formats. + body (ExportInventoryRequest): Request body for creating an inventory export. + + Used at: POST /domains/{domain_id}/inventories/{inventory_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Export | HTTPValidationError | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + format_=format_, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + format_: InventoryExportFormat, + *, + client: AuthenticatedClient, + body: ExportInventoryRequest, +) -> Export | HTTPValidationError | QuotaExceededDetail | None: + """Export an inventory + + Export an inventory to the specified format. + + Supported formats: `parquet` (zipped), `csv`, `geojson`, `geopackage`. + + The inventory must belong to this domain and have status `completed`. + If `columns` is specified, only those columns are included; otherwise + all columns are exported. + + Returns an Export resource with status `pending`. Poll + `GET /exports/{export_id}` until status is `completed` to get the + signed download URL. + + Args: + domain_id (str): + inventory_id (str): + format_ (InventoryExportFormat): Supported inventory export formats. + body (ExportInventoryRequest): Request body for creating an inventory export. + + Used at: POST /domains/{domain_id}/inventories/{inventory_id}/exports/{format} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Export | HTTPValidationError | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + format_=format_, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_upload.py b/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_upload.py new file mode 100644 index 0000000..250e051 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/create_inventory_upload.py @@ -0,0 +1,318 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_inventory_upload_request import CreateInventoryUploadRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_upload_created_response import InventoryUploadCreatedResponse +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateInventoryUploadRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/tree/upload".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = InventoryUploadCreatedResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ + HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail +]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateInventoryUploadRequest, +) -> Response[ + HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail +]: + r"""Create an inventory from a direct file upload + + # Create Upload Inventory + + Creates an inventory resource and returns a signed URL for uploading the + source file directly to GCS. Upload with HTTP PUT, sending **every header + in the response's `upload.headers`** exactly as given — the signed URL + commits to them, and the upload is rejected if any is missing or altered. + For example: + + ```bash + curl -X PUT --upload-file trees.csv -H \"Content-Type: text/csv\" -H \"x-goog-content- + length-range: 0,524288000\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the inventory status to + `completed` (or `failed` on error). + + ## Supported Formats + + - **csv**: Comma-separated values. Coordinates must already be in the + domain's CRS. + - **geojson**: GeoJSON FeatureCollection with Point or MultiPoint + geometries. Reprojected to domain CRS automatically. + - **geopackage**: OGC GeoPackage. Reprojected to domain CRS automatically. + + ## Column Mapping + + Use the `columns` field to map v2 column names to the column names in + your file. Omit entries where the file already uses v2 names. Required + in the file: `x`, `y`, `height`. + + Args: + domain_id (str): + body (CreateInventoryUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateInventoryUploadRequest, +) -> HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail | None: + r"""Create an inventory from a direct file upload + + # Create Upload Inventory + + Creates an inventory resource and returns a signed URL for uploading the + source file directly to GCS. Upload with HTTP PUT, sending **every header + in the response's `upload.headers`** exactly as given — the signed URL + commits to them, and the upload is rejected if any is missing or altered. + For example: + + ```bash + curl -X PUT --upload-file trees.csv -H \"Content-Type: text/csv\" -H \"x-goog-content- + length-range: 0,524288000\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the inventory status to + `completed` (or `failed` on error). + + ## Supported Formats + + - **csv**: Comma-separated values. Coordinates must already be in the + domain's CRS. + - **geojson**: GeoJSON FeatureCollection with Point or MultiPoint + geometries. Reprojected to domain CRS automatically. + - **geopackage**: OGC GeoPackage. Reprojected to domain CRS automatically. + + ## Column Mapping + + Use the `columns` field to map v2 column names to the column names in + your file. Omit entries where the file already uses v2 names. Required + in the file: `x`, `y`, `height`. + + Args: + domain_id (str): + body (CreateInventoryUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateInventoryUploadRequest, +) -> Response[ + HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail +]: + r"""Create an inventory from a direct file upload + + # Create Upload Inventory + + Creates an inventory resource and returns a signed URL for uploading the + source file directly to GCS. Upload with HTTP PUT, sending **every header + in the response's `upload.headers`** exactly as given — the signed URL + commits to them, and the upload is rejected if any is missing or altered. + For example: + + ```bash + curl -X PUT --upload-file trees.csv -H \"Content-Type: text/csv\" -H \"x-goog-content- + length-range: 0,524288000\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the inventory status to + `completed` (or `failed` on error). + + ## Supported Formats + + - **csv**: Comma-separated values. Coordinates must already be in the + domain's CRS. + - **geojson**: GeoJSON FeatureCollection with Point or MultiPoint + geometries. Reprojected to domain CRS automatically. + - **geopackage**: OGC GeoPackage. Reprojected to domain CRS automatically. + + ## Column Mapping + + Use the `columns` field to map v2 column names to the column names in + your file. Omit entries where the file already uses v2 names. Required + in the file: `x`, `y`, `height`. + + Args: + domain_id (str): + body (CreateInventoryUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateInventoryUploadRequest, +) -> HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail | None: + r"""Create an inventory from a direct file upload + + # Create Upload Inventory + + Creates an inventory resource and returns a signed URL for uploading the + source file directly to GCS. Upload with HTTP PUT, sending **every header + in the response's `upload.headers`** exactly as given — the signed URL + commits to them, and the upload is rejected if any is missing or altered. + For example: + + ```bash + curl -X PUT --upload-file trees.csv -H \"Content-Type: text/csv\" -H \"x-goog-content- + length-range: 0,524288000\" \"\" + ``` + + When the upload completes, the uploader service processes the file + automatically via Eventarc and updates the inventory status to + `completed` (or `failed` on error). + + ## Supported Formats + + - **csv**: Comma-separated values. Coordinates must already be in the + domain's CRS. + - **geojson**: GeoJSON FeatureCollection with Point or MultiPoint + geometries. Reprojected to domain CRS automatically. + - **geopackage**: OGC GeoPackage. Reprojected to domain CRS automatically. + + ## Column Mapping + + Use the `columns` field to map v2 column names to the column names in + your file. Omit entries where the file already uses v2 names. Required + in the file: `x`, `y`, `height`. + + Args: + domain_id (str): + body (CreateInventoryUploadRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryUploadCreatedResponse | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/create_pim_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/create_pim_inventory.py new file mode 100644 index 0000000..e5b0ea7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/create_pim_inventory.py @@ -0,0 +1,372 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_pim_inventory_request import CreatePimInventoryRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreatePimInventoryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/tree/pim".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Inventory.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePimInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory from PIM expansion + + # Create PIM Expansion Inventory + + Expands a Plot Imputation Map (PIM) grid into individual tree records + with spatial coordinates. + + A PIM grid maps each 30m cell to an FIA plot ID. This endpoint takes + that mapping and generates a full tree inventory using an inhomogeneous + Poisson point process: + + 1. Tree density (trees per area) is interpolated from plot-level data + onto a sub-cell grid (15m resolution) + 2. Plot IDs are assigned to each sub-cell via nearest-neighbor + interpolation (Voronoi tessellation) + 3. For each sub-cell, a Poisson-distributed random count of trees is + drawn from the local density + 4. Trees are sampled from the assigned plot's tree list, weighted by + trees-per-area (TPA) + 5. Each tree receives a random coordinate within its sub-cell + + The result is a spatially explicit tree inventory that preserves the + species composition and size distributions of the FIA plots while + producing realistic spatial patterns. + + The PIM endpoint is source-agnostic: it works the same regardless of + whether the source grid is from TreeMap, BIGMAP, or FSE. The grid's + own ``source`` field carries that lineage. + + ## Request Body + + - **source_pim_grid_id**: (required) ID of a completed PIM grid. + - **seed**: (optional) Random seed for reproducibility. Generated + randomly if omitted. + - **point_process**: (optional) Spatial point process for coordinate + assignment. Default: ``\"inhomogeneous_poisson\"``. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the expansion asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreatePimInventoryRequest): Request body for creating an inventory via PIM + expansion. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePimInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory from PIM expansion + + # Create PIM Expansion Inventory + + Expands a Plot Imputation Map (PIM) grid into individual tree records + with spatial coordinates. + + A PIM grid maps each 30m cell to an FIA plot ID. This endpoint takes + that mapping and generates a full tree inventory using an inhomogeneous + Poisson point process: + + 1. Tree density (trees per area) is interpolated from plot-level data + onto a sub-cell grid (15m resolution) + 2. Plot IDs are assigned to each sub-cell via nearest-neighbor + interpolation (Voronoi tessellation) + 3. For each sub-cell, a Poisson-distributed random count of trees is + drawn from the local density + 4. Trees are sampled from the assigned plot's tree list, weighted by + trees-per-area (TPA) + 5. Each tree receives a random coordinate within its sub-cell + + The result is a spatially explicit tree inventory that preserves the + species composition and size distributions of the FIA plots while + producing realistic spatial patterns. + + The PIM endpoint is source-agnostic: it works the same regardless of + whether the source grid is from TreeMap, BIGMAP, or FSE. The grid's + own ``source`` field carries that lineage. + + ## Request Body + + - **source_pim_grid_id**: (required) ID of a completed PIM grid. + - **seed**: (optional) Random seed for reproducibility. Generated + randomly if omitted. + - **point_process**: (optional) Spatial point process for coordinate + assignment. Default: ``\"inhomogeneous_poisson\"``. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the expansion asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreatePimInventoryRequest): Request body for creating an inventory via PIM + expansion. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePimInventoryRequest, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Create an inventory from PIM expansion + + # Create PIM Expansion Inventory + + Expands a Plot Imputation Map (PIM) grid into individual tree records + with spatial coordinates. + + A PIM grid maps each 30m cell to an FIA plot ID. This endpoint takes + that mapping and generates a full tree inventory using an inhomogeneous + Poisson point process: + + 1. Tree density (trees per area) is interpolated from plot-level data + onto a sub-cell grid (15m resolution) + 2. Plot IDs are assigned to each sub-cell via nearest-neighbor + interpolation (Voronoi tessellation) + 3. For each sub-cell, a Poisson-distributed random count of trees is + drawn from the local density + 4. Trees are sampled from the assigned plot's tree list, weighted by + trees-per-area (TPA) + 5. Each tree receives a random coordinate within its sub-cell + + The result is a spatially explicit tree inventory that preserves the + species composition and size distributions of the FIA plots while + producing realistic spatial patterns. + + The PIM endpoint is source-agnostic: it works the same regardless of + whether the source grid is from TreeMap, BIGMAP, or FSE. The grid's + own ``source`` field carries that lineage. + + ## Request Body + + - **source_pim_grid_id**: (required) ID of a completed PIM grid. + - **seed**: (optional) Random seed for reproducibility. Generated + randomly if omitted. + - **point_process**: (optional) Spatial point process for coordinate + assignment. Default: ``\"inhomogeneous_poisson\"``. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the expansion asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreatePimInventoryRequest): Request body for creating an inventory via PIM + expansion. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePimInventoryRequest, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Create an inventory from PIM expansion + + # Create PIM Expansion Inventory + + Expands a Plot Imputation Map (PIM) grid into individual tree records + with spatial coordinates. + + A PIM grid maps each 30m cell to an FIA plot ID. This endpoint takes + that mapping and generates a full tree inventory using an inhomogeneous + Poisson point process: + + 1. Tree density (trees per area) is interpolated from plot-level data + onto a sub-cell grid (15m resolution) + 2. Plot IDs are assigned to each sub-cell via nearest-neighbor + interpolation (Voronoi tessellation) + 3. For each sub-cell, a Poisson-distributed random count of trees is + drawn from the local density + 4. Trees are sampled from the assigned plot's tree list, weighted by + trees-per-area (TPA) + 5. Each tree receives a random coordinate within its sub-cell + + The result is a spatially explicit tree inventory that preserves the + species composition and size distributions of the FIA plots while + producing realistic spatial patterns. + + The PIM endpoint is source-agnostic: it works the same regardless of + whether the source grid is from TreeMap, BIGMAP, or FSE. The grid's + own ``source`` field carries that lineage. + + ## Request Body + + - **source_pim_grid_id**: (required) ID of a completed PIM grid. + - **seed**: (optional) Random seed for reproducibility. Generated + randomly if omitted. + - **point_process**: (optional) Spatial point process for coordinate + assignment. Default: ``\"inhomogeneous_poisson\"``. + - **type**: (optional) Entity type. Default: ``\"tree\"``. + - **name**: (optional) Name for the inventory. + - **description**: (optional) Description. + - **tags**: (optional) Tags for organizing inventories. + + ## Response + + Returns the created Inventory resource with status ``\"pending\"``. The + backend (Standgen) will process the expansion asynchronously and update + status to ``\"completed\"`` when ready. + + Args: + domain_id (str): + body (CreatePimInventoryRequest): Request body for creating an inventory via PIM + expansion. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/delete_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/delete_inventory.py new file mode 100644 index 0000000..e759207 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/delete_inventory.py @@ -0,0 +1,245 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/domains/{domain_id}/inventories/{inventory_id}".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an inventory + + # Delete Inventory Endpoint + + Permanently deletes an inventory resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an inventory + + # Delete Inventory Endpoint + + Permanently deletes an inventory resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete an inventory + + # Delete Inventory Endpoint + + Permanently deletes an inventory resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete an inventory + + # Delete Inventory Endpoint + + Permanently deletes an inventory resource by its unique identifier. + This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/duplicate_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/duplicate_inventory.py new file mode 100644 index 0000000..1902763 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/duplicate_inventory.py @@ -0,0 +1,361 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.duplicate_inventory_request import DuplicateInventoryRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + *, + body: DuplicateInventoryRequest | None | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/inventories/{inventory_id}/duplicate".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + if isinstance(body, DuplicateInventoryRequest): + _kwargs["json"] = body.to_dict() + else: + _kwargs["json"] = body + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = Inventory.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: DuplicateInventoryRequest | None | Unset = UNSET, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Duplicate an inventory + + # Duplicate an Inventory + + Creates an independent **copy** of a completed inventory under a new ID. + Use this to branch a scenario: duplicate, then edit the copy in place while + the original stays untouched. + + This is a true clone, not a re-derivation. The finished data is byte-copied; + no regeneration is performed. The copy carries over the source's `source`, + `modifications`, `treatments`, `columns`, `georeference`, and `checksum` + verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Inventory with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/data`) become available + only after the copy completes. The source inventory is unchanged. + + ## Error Responses + + - **404 Not Found**: The source inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source inventory exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (DuplicateInventoryRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: DuplicateInventoryRequest | None | Unset = UNSET, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Duplicate an inventory + + # Duplicate an Inventory + + Creates an independent **copy** of a completed inventory under a new ID. + Use this to branch a scenario: duplicate, then edit the copy in place while + the original stays untouched. + + This is a true clone, not a re-derivation. The finished data is byte-copied; + no regeneration is performed. The copy carries over the source's `source`, + `modifications`, `treatments`, `columns`, `georeference`, and `checksum` + verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Inventory with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/data`) become available + only after the copy completes. The source inventory is unchanged. + + ## Error Responses + + - **404 Not Found**: The source inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source inventory exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (DuplicateInventoryRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: DuplicateInventoryRequest | None | Unset = UNSET, +) -> Response[HTTPValidationError | Inventory | QuotaExceededDetail]: + r"""Duplicate an inventory + + # Duplicate an Inventory + + Creates an independent **copy** of a completed inventory under a new ID. + Use this to branch a scenario: duplicate, then edit the copy in place while + the original stays untouched. + + This is a true clone, not a re-derivation. The finished data is byte-copied; + no regeneration is performed. The copy carries over the source's `source`, + `modifications`, `treatments`, `columns`, `georeference`, and `checksum` + verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Inventory with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/data`) become available + only after the copy completes. The source inventory is unchanged. + + ## Error Responses + + - **404 Not Found**: The source inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source inventory exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (DuplicateInventoryRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: DuplicateInventoryRequest | None | Unset = UNSET, +) -> HTTPValidationError | Inventory | QuotaExceededDetail | None: + r"""Duplicate an inventory + + # Duplicate an Inventory + + Creates an independent **copy** of a completed inventory under a new ID. + Use this to branch a scenario: duplicate, then edit the copy in place while + the original stays untouched. + + This is a true clone, not a re-derivation. The finished data is byte-copied; + no regeneration is performed. The copy carries over the source's `source`, + `modifications`, `treatments`, `columns`, `georeference`, and `checksum` + verbatim — only its `id` and timestamps differ. + + ## Request Body (optional) + + All fields are optional. Any field omitted is carried over from the source. + + - **name**: Name for the copy. + - **description**: Description for the copy. + - **tags**: Tags for the copy. + + Send no body at all to copy the metadata unchanged. + + ## Response + + Returns the new Inventory with status `\"pending\"`. The data is copied in the + background; the status transitions to `\"completed\"` once the copy finishes + (or `\"failed\"` if it does not). Data endpoints (`/data`) become available + only after the copy completes. The source inventory is unchanged. + + ## Error Responses + + - **404 Not Found**: The source inventory does not exist, is not owned by the + caller, or is not in this domain. + - **422 Unprocessable Content**: The source inventory exists but is not yet + `completed`, so there is no finished artifact to copy. + - **429 Too Many Requests**: You have too many active inventory jobs in + progress (your `max_active_inventories` quota). Wait for jobs to complete + or delete unneeded inventories, then retry. The response detail names the + exact `quota` and includes a `Retry-After` header. + + Args: + domain_id (str): + inventory_id (str): + body (DuplicateInventoryRequest | None | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/get_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory.py new file mode 100644 index 0000000..cca238a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory.py @@ -0,0 +1,243 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/inventories/{inventory_id}".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | None: + if response.status_code == 200: + response_200 = Inventory.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Inventory]: + """Get an inventory by ID + + # Get Inventory Endpoint + + Retrieves a specific inventory resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique 32-character hex identifier of the inventory. + + ## Response + + Returns the inventory resource. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Inventory | None: + """Get an inventory by ID + + # Get Inventory Endpoint + + Retrieves a specific inventory resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique 32-character hex identifier of the inventory. + + ## Response + + Returns the inventory resource. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Inventory]: + """Get an inventory by ID + + # Get Inventory Endpoint + + Retrieves a specific inventory resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique 32-character hex identifier of the inventory. + + ## Response + + Returns the inventory resource. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Inventory | None: + """Get an inventory by ID + + # Get Inventory Endpoint + + Retrieves a specific inventory resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique 32-character hex identifier of the inventory. + + ## Response + + Returns the inventory resource. + + ## Error Responses + + - **404 Not Found**: The inventory does not exist or the user does not have access. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_csv.py b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_csv.py new file mode 100644 index 0000000..1acca14 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_csv.py @@ -0,0 +1,349 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + columns: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + json_columns: None | str | Unset + if isinstance(columns, Unset): + json_columns = UNSET + else: + json_columns = columns + params["columns"] = json_columns + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/inventories/{inventory_id}/data/{partition_index}/csv".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + partition_index=quote(str(partition_index), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | str | None: + if response.status_code == 200: + response_200 = response.text + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + columns: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | str]: + """Get inventory data for a partition (CSV) + + # Get Inventory Data (CSV) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a `text/csv` body with a header row. Use this + when you want to hand the response straight to a CSV reader. + + For a structured JSON payload, use the JSON variant of this endpoint (drop + the trailing `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **columns**: Comma-separated column subset (default: all). + + ## Response + + A `text/csv` body, with partition metadata in these response headers: + + - `X-Partition-Index`: the partition this body came from. + - `X-Row-Count`: rows in this partition. + - `X-Total-Rows`: rows across all partitions of the inventory. + - `X-Num-Partitions`: total number of partitions. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | str] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + columns=columns, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + columns: None | str | Unset = UNSET, +) -> HTTPValidationError | str | None: + """Get inventory data for a partition (CSV) + + # Get Inventory Data (CSV) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a `text/csv` body with a header row. Use this + when you want to hand the response straight to a CSV reader. + + For a structured JSON payload, use the JSON variant of this endpoint (drop + the trailing `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **columns**: Comma-separated column subset (default: all). + + ## Response + + A `text/csv` body, with partition metadata in these response headers: + + - `X-Partition-Index`: the partition this body came from. + - `X-Row-Count`: rows in this partition. + - `X-Total-Rows`: rows across all partitions of the inventory. + - `X-Num-Partitions`: total number of partitions. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | str + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + client=client, + columns=columns, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + columns: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | str]: + """Get inventory data for a partition (CSV) + + # Get Inventory Data (CSV) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a `text/csv` body with a header row. Use this + when you want to hand the response straight to a CSV reader. + + For a structured JSON payload, use the JSON variant of this endpoint (drop + the trailing `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **columns**: Comma-separated column subset (default: all). + + ## Response + + A `text/csv` body, with partition metadata in these response headers: + + - `X-Partition-Index`: the partition this body came from. + - `X-Row-Count`: rows in this partition. + - `X-Total-Rows`: rows across all partitions of the inventory. + - `X-Num-Partitions`: total number of partitions. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | str] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + columns=columns, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + columns: None | str | Unset = UNSET, +) -> HTTPValidationError | str | None: + """Get inventory data for a partition (CSV) + + # Get Inventory Data (CSV) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a `text/csv` body with a header row. Use this + when you want to hand the response straight to a CSV reader. + + For a structured JSON payload, use the JSON variant of this endpoint (drop + the trailing `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **columns**: Comma-separated column subset (default: all). + + ## Response + + A `text/csv` body, with partition metadata in these response headers: + + - `X-Partition-Index`: the partition this body came from. + - `X-Row-Count`: rows in this partition. + - `X-Total-Rows`: rows across all partitions of the inventory. + - `X-Num-Partitions`: total number of partitions. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | str + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + client=client, + columns=columns, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_json.py b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_json.py new file mode 100644 index 0000000..968bcb4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_json.py @@ -0,0 +1,363 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_data_response import InventoryDataResponse +from ...models.inventory_json_orientation import InventoryJsonOrientation +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + json_orientation: InventoryJsonOrientation | Unset = UNSET, + columns: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + json_json_orientation: str | Unset = UNSET + if not isinstance(json_orientation, Unset): + json_json_orientation = json_orientation.value + + params["json_orientation"] = json_json_orientation + + json_columns: None | str | Unset + if isinstance(columns, Unset): + json_columns = UNSET + else: + json_columns = columns + params["columns"] = json_columns + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/inventories/{inventory_id}/data/{partition_index}".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + partition_index=quote(str(partition_index), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | InventoryDataResponse | None: + if response.status_code == 200: + response_200 = InventoryDataResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | InventoryDataResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + json_orientation: InventoryJsonOrientation | Unset = UNSET, + columns: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | InventoryDataResponse]: + """Get inventory data for a partition (JSON) + + # Get Inventory Data (JSON) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a JSON payload — either a compact columnar + layout or self-describing row objects, selected via `json_orientation`. + + For a CSV body, use the CSV variant of this endpoint (append `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **json_orientation**: JSON layout: `split` (default, compact) or + `records` (self-describing). + - **columns**: Comma-separated column subset (default: all). + + ## Response + + **split** (default): column names + 2D array of values. + + **records**: list of row objects. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + json_orientation (InventoryJsonOrientation | Unset): + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryDataResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + json_orientation=json_orientation, + columns=columns, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + json_orientation: InventoryJsonOrientation | Unset = UNSET, + columns: None | str | Unset = UNSET, +) -> HTTPValidationError | InventoryDataResponse | None: + """Get inventory data for a partition (JSON) + + # Get Inventory Data (JSON) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a JSON payload — either a compact columnar + layout or self-describing row objects, selected via `json_orientation`. + + For a CSV body, use the CSV variant of this endpoint (append `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **json_orientation**: JSON layout: `split` (default, compact) or + `records` (self-describing). + - **columns**: Comma-separated column subset (default: all). + + ## Response + + **split** (default): column names + 2D array of values. + + **records**: list of row objects. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + json_orientation (InventoryJsonOrientation | Unset): + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryDataResponse + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + client=client, + json_orientation=json_orientation, + columns=columns, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + json_orientation: InventoryJsonOrientation | Unset = UNSET, + columns: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | InventoryDataResponse]: + """Get inventory data for a partition (JSON) + + # Get Inventory Data (JSON) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a JSON payload — either a compact columnar + layout or self-describing row objects, selected via `json_orientation`. + + For a CSV body, use the CSV variant of this endpoint (append `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **json_orientation**: JSON layout: `split` (default, compact) or + `records` (self-describing). + - **columns**: Comma-separated column subset (default: all). + + ## Response + + **split** (default): column names + 2D array of values. + + **records**: list of row objects. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + json_orientation (InventoryJsonOrientation | Unset): + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryDataResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + json_orientation=json_orientation, + columns=columns, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + partition_index: int, + *, + client: AuthenticatedClient, + json_orientation: InventoryJsonOrientation | Unset = UNSET, + columns: None | str | Unset = UNSET, +) -> HTTPValidationError | InventoryDataResponse | None: + """Get inventory data for a partition (JSON) + + # Get Inventory Data (JSON) + + Reads a single partition of a completed inventory's Parquet data on GCS and + returns the tree records as a JSON payload — either a compact columnar + layout or self-describing row objects, selected via `json_orientation`. + + For a CSV body, use the CSV variant of this endpoint (append `/csv`). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + - **partition_index**: Zero-based partition index. + + ## Query Parameters + + - **json_orientation**: JSON layout: `split` (default, compact) or + `records` (self-describing). + - **columns**: Comma-separated column subset (default: all). + + ## Response + + **split** (default): column names + 2D array of values. + + **records**: list of row objects. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory not completed, partition index + out of range, invalid column names, or metadata not available. + + Args: + domain_id (str): + inventory_id (str): + partition_index (int): Zero-based partition index. + json_orientation (InventoryJsonOrientation | Unset): + columns (None | str | Unset): Comma-separated column subset. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryDataResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + client=client, + json_orientation=json_orientation, + columns=columns, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_metadata.py b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_metadata.py new file mode 100644 index 0000000..5e59aff --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/get_inventory_data_metadata.py @@ -0,0 +1,275 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_data_metadata import InventoryDataMetadata +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/inventories/{inventory_id}/data/metadata".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | InventoryDataMetadata | None: + if response.status_code == 200: + response_200 = InventoryDataMetadata.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | InventoryDataMetadata]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | InventoryDataMetadata]: + """Get inventory data metadata + + # Get Inventory Data Metadata + + Returns partition count, total rows, per-partition row counts, and column + names for a completed inventory. Reads only the `_metadata` file from GCS + (cached after first access). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + + ## Response + + - **inventory_id**: The inventory ID. + - **num_partitions**: Number of Parquet partitions. + - **total_rows**: Total row count across all partitions. + - **columns**: List of column names. + - **partitions**: Per-partition index and row count. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory is not completed, or metadata + file is not available. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryDataMetadata] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | InventoryDataMetadata | None: + """Get inventory data metadata + + # Get Inventory Data Metadata + + Returns partition count, total rows, per-partition row counts, and column + names for a completed inventory. Reads only the `_metadata` file from GCS + (cached after first access). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + + ## Response + + - **inventory_id**: The inventory ID. + - **num_partitions**: Number of Parquet partitions. + - **total_rows**: Total row count across all partitions. + - **columns**: List of column names. + - **partitions**: Per-partition index and row count. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory is not completed, or metadata + file is not available. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryDataMetadata + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | InventoryDataMetadata]: + """Get inventory data metadata + + # Get Inventory Data Metadata + + Returns partition count, total rows, per-partition row counts, and column + names for a completed inventory. Reads only the `_metadata` file from GCS + (cached after first access). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + + ## Response + + - **inventory_id**: The inventory ID. + - **num_partitions**: Number of Parquet partitions. + - **total_rows**: Total row count across all partitions. + - **columns**: List of column names. + - **partitions**: Per-partition index and row count. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory is not completed, or metadata + file is not available. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | InventoryDataMetadata] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | InventoryDataMetadata | None: + """Get inventory data metadata + + # Get Inventory Data Metadata + + Returns partition count, total rows, per-partition row counts, and column + names for a completed inventory. Reads only the `_metadata` file from GCS + (cached after first access). + + ## Path Parameters + + - **domain_id**: The domain the inventory belongs to. + - **inventory_id**: The unique identifier of the inventory. + + ## Response + + - **inventory_id**: The inventory ID. + - **num_partitions**: Number of Parquet partitions. + - **total_rows**: Total row count across all partitions. + - **columns**: List of column names. + - **partitions**: Per-partition index and row count. + + ## Error Responses + + - **404 Not Found**: Inventory does not exist or user does not have access. + - **422 Unprocessable Entity**: Inventory is not completed, or metadata + file is not available. + + Args: + domain_id (str): + inventory_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | InventoryDataMetadata + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/list_inventories.py b/fastfuels_sdk/v2/client_library/api/inventories/list_inventories.py new file mode 100644 index 0000000..4d2dc87 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/list_inventories.py @@ -0,0 +1,402 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_sort_field import InventorySortField +from ...models.inventory_type import InventoryType +from ...models.list_inventories_response import ListInventoriesResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, InventorySortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, InventoryType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/inventories".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListInventoriesResponse | None: + if response.status_code == 200: + response_200 = ListInventoriesResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListInventoriesResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListInventoriesResponse]: + """List all inventories + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list inventories for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListInventoriesResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListInventoriesResponse | None: + """List all inventories + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list inventories for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListInventoriesResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListInventoriesResponse]: + """List all inventories + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list inventories for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListInventoriesResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListInventoriesResponse | None: + """List all inventories + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories within a domain belonging to + the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list inventories for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListInventoriesResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/list_inventories_cross_domain.py b/fastfuels_sdk/v2/client_library/api/inventories/list_inventories_cross_domain.py new file mode 100644 index 0000000..1168015 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/list_inventories_cross_domain.py @@ -0,0 +1,370 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory_sort_field import InventorySortField +from ...models.inventory_type import InventoryType +from ...models.list_inventories_response import ListInventoriesResponse +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, InventorySortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, InventoryType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/-/inventories", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListInventoriesResponse | None: + if response.status_code == 200: + response_200 = ListInventoriesResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListInventoriesResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListInventoriesResponse]: + """List inventories across all domains + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListInventoriesResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListInventoriesResponse | None: + """List inventories across all domains + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListInventoriesResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListInventoriesResponse]: + """List inventories across all domains + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListInventoriesResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: InventorySortField | None | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: InventoryType | None | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListInventoriesResponse | None: + """List inventories across all domains + + # List Inventories Endpoint + + Retrieves a paginated list of all inventories across all domains belonging to + the authenticated user. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by entity type (e.g., `tree`). + - **source**: (string, optional) Filter by source name (e.g., `pim`). + - **tag**: (string, optional) Filter inventories that contain this tag. + + ## Response + + Returns a paginated list of inventories with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of inventories to retrieve per page. Default: 100. + sort_by (InventorySortField | None | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (InventoryType | None | Unset): Filter inventories by entity type (e.g., 'tree'). + source (None | str | Unset): Filter inventories by source name (e.g., 'pim'). + tag (None | str | Unset): Filter inventories that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListInventoriesResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/inventories/update_inventory.py b/fastfuels_sdk/v2/client_library/api/inventories/update_inventory.py new file mode 100644 index 0000000..de02648 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/inventories/update_inventory.py @@ -0,0 +1,328 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.inventory import Inventory +from ...models.update_inventory_request_body import UpdateInventoryRequestBody +from ...types import Response + + +def _get_kwargs( + domain_id: str, + inventory_id: str, + *, + body: UpdateInventoryRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/domains/{domain_id}/inventories/{inventory_id}".format( + domain_id=quote(str(domain_id), safe=""), + inventory_id=quote(str(inventory_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Inventory | None: + if response.status_code == 200: + response_200 = Inventory.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Inventory]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: UpdateInventoryRequestBody, +) -> Response[HTTPValidationError | Inventory]: + """Update an inventory + + # Update Inventory Endpoint + + Updates the metadata of an existing inventory resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the inventory. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **modifications**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the inventory's content is rebuilt, never + via metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated inventory resource. + + Args: + domain_id (str): + inventory_id (str): + body (UpdateInventoryRequestBody): Request body for updating inventory metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: UpdateInventoryRequestBody, +) -> HTTPValidationError | Inventory | None: + """Update an inventory + + # Update Inventory Endpoint + + Updates the metadata of an existing inventory resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the inventory. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **modifications**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the inventory's content is rebuilt, never + via metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated inventory resource. + + Args: + domain_id (str): + inventory_id (str): + body (UpdateInventoryRequestBody): Request body for updating inventory metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory + """ + + return sync_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: UpdateInventoryRequestBody, +) -> Response[HTTPValidationError | Inventory]: + """Update an inventory + + # Update Inventory Endpoint + + Updates the metadata of an existing inventory resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the inventory. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **modifications**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the inventory's content is rebuilt, never + via metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated inventory resource. + + Args: + domain_id (str): + inventory_id (str): + body (UpdateInventoryRequestBody): Request body for updating inventory metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Inventory] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + inventory_id=inventory_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + inventory_id: str, + *, + client: AuthenticatedClient, + body: UpdateInventoryRequestBody, +) -> HTTPValidationError | Inventory | None: + """Update an inventory + + # Update Inventory Endpoint + + Updates the metadata of an existing inventory resource. Only the fields + provided in the request body will be modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the inventory belongs to. + - **inventory_id**: (string) The unique identifier of the inventory. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the inventory. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following fields are immutable: + + - **id**, **domain_id**, **type**, **source**, **modifications**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the inventory's content is rebuilt, never + via metadata updates) + + The **modified_on** field is automatically updated. + + ## Response + + Returns the updated inventory resource. + + Args: + domain_id (str): + inventory_id (str): + body (UpdateInventoryRequestBody): Request body for updating inventory metadata. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Inventory + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + inventory_id=inventory_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/keys/__init__.py b/fastfuels_sdk/v2/client_library/api/keys/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/keys/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/keys/create_key.py b/fastfuels_sdk/v2/client_library/api/keys/create_key.py new file mode 100644 index 0000000..a2e2cec --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/keys/create_key.py @@ -0,0 +1,186 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_key_request import CreateKeyRequest +from ...models.create_key_response import CreateKeyResponse +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + *, + body: CreateKeyRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/keys", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> CreateKeyResponse | HTTPValidationError | None: + if response.status_code == 201: + response_201 = CreateKeyResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[CreateKeyResponse | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: CreateKeyRequest, +) -> Response[CreateKeyResponse | HTTPValidationError]: + """Create API key + + Create a new API key for programmatic access. + + Returns the key secret exactly once. The secret cannot be retrieved again — + only its SHA-256 hash (the key ID) is stored. + + Args: + body (CreateKeyRequest): Request body for creating an API key. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CreateKeyResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: CreateKeyRequest, +) -> CreateKeyResponse | HTTPValidationError | None: + """Create API key + + Create a new API key for programmatic access. + + Returns the key secret exactly once. The secret cannot be retrieved again — + only its SHA-256 hash (the key ID) is stored. + + Args: + body (CreateKeyRequest): Request body for creating an API key. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CreateKeyResponse | HTTPValidationError + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: CreateKeyRequest, +) -> Response[CreateKeyResponse | HTTPValidationError]: + """Create API key + + Create a new API key for programmatic access. + + Returns the key secret exactly once. The secret cannot be retrieved again — + only its SHA-256 hash (the key ID) is stored. + + Args: + body (CreateKeyRequest): Request body for creating an API key. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CreateKeyResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: CreateKeyRequest, +) -> CreateKeyResponse | HTTPValidationError | None: + """Create API key + + Create a new API key for programmatic access. + + Returns the key secret exactly once. The secret cannot be retrieved again — + only its SHA-256 hash (the key ID) is stored. + + Args: + body (CreateKeyRequest): Request body for creating an API key. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CreateKeyResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/keys/delete_key.py b/fastfuels_sdk/v2/client_library/api/keys/delete_key.py new file mode 100644 index 0000000..2c00d17 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/keys/delete_key.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + key_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/keys/{key_id}".format( + key_id=quote(str(key_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + key_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete API key + + Delete an API key with ownership check. Clears the auth cache. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + key_id=key_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + key_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete API key + + Delete an API key with ownership check. Clears the auth cache. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + key_id=key_id, + client=client, + ).parsed + + +async def asyncio_detailed( + key_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete API key + + Delete an API key with ownership check. Clears the auth cache. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + key_id=key_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + key_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete API key + + Delete an API key with ownership check. Clears the auth cache. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + key_id=key_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/keys/get_key_by_id.py b/fastfuels_sdk/v2/client_library/api/keys/get_key_by_id.py new file mode 100644 index 0000000..e9af2b8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/keys/get_key_by_id.py @@ -0,0 +1,169 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.key import Key +from ...types import Response + + +def _get_kwargs( + key_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/keys/{key_id}".format( + key_id=quote(str(key_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | Key | None: + if response.status_code == 200: + response_200 = Key.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | Key]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + key_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Key]: + """Get API key + + Get an API key by ID with two-tier ownership check. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Key] + """ + + kwargs = _get_kwargs( + key_id=key_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + key_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Key | None: + """Get API key + + Get an API key by ID with two-tier ownership check. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Key + """ + + return sync_detailed( + key_id=key_id, + client=client, + ).parsed + + +async def asyncio_detailed( + key_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | Key]: + """Get API key + + Get an API key by ID with two-tier ownership check. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | Key] + """ + + kwargs = _get_kwargs( + key_id=key_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + key_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | Key | None: + """Get API key + + Get an API key by ID with two-tier ownership check. + + Args: + key_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | Key + """ + + return ( + await asyncio_detailed( + key_id=key_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/keys/list_keys.py b/fastfuels_sdk/v2/client_library/api/keys/list_keys.py new file mode 100644 index 0000000..73729da --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/keys/list_keys.py @@ -0,0 +1,189 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.list_keys_response import ListKeysResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/keys", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListKeysResponse | None: + if response.status_code == 200: + response_200 = ListKeysResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListKeysResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> Response[HTTPValidationError | ListKeysResponse]: + """List API keys + + List API keys accessible to the authenticated user or application. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListKeysResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> HTTPValidationError | ListKeysResponse | None: + """List API keys + + List API keys accessible to the authenticated user or application. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListKeysResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> Response[HTTPValidationError | ListKeysResponse]: + """List API keys + + List API keys accessible to the authenticated user or application. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListKeysResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, +) -> HTTPValidationError | ListKeysResponse | None: + """List API keys + + List API keys accessible to the authenticated user or application. + + Args: + page (int | Unset): Default: 0. + size (int | Unset): Default: 100. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListKeysResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/__init__.py b/fastfuels_sdk/v2/client_library/api/point_clouds/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/check_3dep_point_cloud_coverage.py b/fastfuels_sdk/v2/client_library/api/point_clouds/check_3dep_point_cloud_coverage.py new file mode 100644 index 0000000..3150a88 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/check_3dep_point_cloud_coverage.py @@ -0,0 +1,259 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.point_cloud_three_dep_coverage_response import ( + PointCloudThreeDepCoverageResponse, +) +from ...types import Response + + +def _get_kwargs( + domain_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/pointclouds/3dep/coverage".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PointCloudThreeDepCoverageResponse | None: + if response.status_code == 200: + response_200 = PointCloudThreeDepCoverageResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | PointCloudThreeDepCoverageResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PointCloudThreeDepCoverageResponse]: + """Check 3DEP lidar coverage for a domain + + # Check 3DEP Lidar Coverage + + Immediate pre-flight check reporting which USGS 3DEP lidar surveys are + available for this domain, how much of it they cover, and roughly how many + points a fetch would return. Use it before creating a 3DEP point cloud to + avoid waiting on a background job only to find a coverage gap — 3DEP is + regional, and survey boundaries are irregular. + + This checks lidar point clouds. Elevation raster coverage is a separate + product with its own check at + `GET /domains/{domain_id}/grids/topography/3dep/coverage`. + + ## Response + + Reports whether any lidar is available, the fraction of the domain covered, + the surveys that would be read with what each contributes, and the + estimated point count against the per-fetch budget. `datasets[].name` + values can be passed as `datasets` when creating the point cloud to pin the + fetch. + + ## Error Responses + + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloudThreeDepCoverageResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PointCloudThreeDepCoverageResponse | None: + """Check 3DEP lidar coverage for a domain + + # Check 3DEP Lidar Coverage + + Immediate pre-flight check reporting which USGS 3DEP lidar surveys are + available for this domain, how much of it they cover, and roughly how many + points a fetch would return. Use it before creating a 3DEP point cloud to + avoid waiting on a background job only to find a coverage gap — 3DEP is + regional, and survey boundaries are irregular. + + This checks lidar point clouds. Elevation raster coverage is a separate + product with its own check at + `GET /domains/{domain_id}/grids/topography/3dep/coverage`. + + ## Response + + Reports whether any lidar is available, the fraction of the domain covered, + the surveys that would be read with what each contributes, and the + estimated point count against the per-fetch budget. `datasets[].name` + values can be passed as `datasets` when creating the point cloud to pin the + fetch. + + ## Error Responses + + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloudThreeDepCoverageResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PointCloudThreeDepCoverageResponse]: + """Check 3DEP lidar coverage for a domain + + # Check 3DEP Lidar Coverage + + Immediate pre-flight check reporting which USGS 3DEP lidar surveys are + available for this domain, how much of it they cover, and roughly how many + points a fetch would return. Use it before creating a 3DEP point cloud to + avoid waiting on a background job only to find a coverage gap — 3DEP is + regional, and survey boundaries are irregular. + + This checks lidar point clouds. Elevation raster coverage is a separate + product with its own check at + `GET /domains/{domain_id}/grids/topography/3dep/coverage`. + + ## Response + + Reports whether any lidar is available, the fraction of the domain covered, + the surveys that would be read with what each contributes, and the + estimated point count against the per-fetch budget. `datasets[].name` + values can be passed as `datasets` when creating the point cloud to pin the + fetch. + + ## Error Responses + + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloudThreeDepCoverageResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PointCloudThreeDepCoverageResponse | None: + """Check 3DEP lidar coverage for a domain + + # Check 3DEP Lidar Coverage + + Immediate pre-flight check reporting which USGS 3DEP lidar surveys are + available for this domain, how much of it they cover, and roughly how many + points a fetch would return. Use it before creating a 3DEP point cloud to + avoid waiting on a background job only to find a coverage gap — 3DEP is + regional, and survey boundaries are irregular. + + This checks lidar point clouds. Elevation raster coverage is a separate + product with its own check at + `GET /domains/{domain_id}/grids/topography/3dep/coverage`. + + ## Response + + Reports whether any lidar is available, the fraction of the domain covered, + the surveys that would be read with what each contributes, and the + estimated point count against the per-fetch budget. `datasets[].name` + values can be passed as `datasets` when creating the point cloud to pin the + fetch. + + ## Error Responses + + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloudThreeDepCoverageResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/create_3dep_point_cloud.py b/fastfuels_sdk/v2/client_library/api/point_clouds/create_3dep_point_cloud.py new file mode 100644 index 0000000..68ef124 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/create_3dep_point_cloud.py @@ -0,0 +1,418 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_three_dep_point_cloud_request import ( + CreateThreeDepPointCloudRequest, +) +from ...models.http_validation_error import HTTPValidationError +from ...models.point_cloud import PointCloud +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreateThreeDepPointCloudRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/pointclouds/3dep".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PointCloud | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = PointCloud.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | PointCloud | QuotaExceededDetail]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepPointCloudRequest, +) -> Response[HTTPValidationError | PointCloud | QuotaExceededDetail]: + """Create a point cloud from USGS 3DEP + + # Create a Point Cloud from USGS 3DEP + + Fetches public airborne lidar from the USGS 3D Elevation Program for this + domain. The points are clipped to the domain, reprojected to the domain's + coordinate reference system, and stored as a point cloud you can build on — + most directly as a canopy height model, which in turn feeds a tree + inventory. + + The point cloud is returned immediately with `status` = `pending` and is + fetched in the background: `status` becomes `running`, then `completed` once + the points are stored and `georeference` and `summary` are filled in — or + `failed` if the fetch cannot be completed. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + 3DEP is airborne, so the resulting point cloud is always type `als`. There + is no acquisition type to choose. + + ## Choosing acquisitions + + 3DEP is published as separate surveys, which overlap and differ in age and + point density. By default the backend prefers a single survey that covers + the whole domain, and otherwise combines the fewest surveys that fill it — + each additional survey introduces a seam between flights of different dates + and densities. Pass `datasets` to pin the fetch to specific surveys + instead; check the coverage endpoint first to see what is available. + + Survey boundaries are irregular, so a domain is often covered to + 99-point-something percent rather than exactly 100. Any coverage above zero + produces a point cloud, and the fraction actually covered is recorded on the + result as `source.coverage_fraction` — check it if a gap would matter, since + `summary.density` is measured over the points that exist and looks healthy + either way. Use the coverage endpoint to see the shortfall before creating + anything. + + ## Request Body + + - **name**: (optional) Human-readable name. + - **description**: (optional) Longer free-text description. + - **tags**: (optional) Tags for organizing and filtering. + - **datasets**: (optional) Acquisition names to read, in priority order. + Omit to choose automatically. + + ## Coordinate reference system + + Points are reprojected to the domain's CRS. Only horizontal coordinates are + transformed — elevations are stored exactly as USGS published them, never + converted between reference surfaces. + + ## Error Responses + + - **422**: No 3DEP lidar covers this domain, a pinned acquisition is + unknown or does not overlap the domain, or the fetch would exceed the + point budget. + - **429**: A quota was exceeded. + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + body (CreateThreeDepPointCloudRequest): Request body for fetching a point cloud from USGS + 3DEP. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepPointCloudRequest, +) -> HTTPValidationError | PointCloud | QuotaExceededDetail | None: + """Create a point cloud from USGS 3DEP + + # Create a Point Cloud from USGS 3DEP + + Fetches public airborne lidar from the USGS 3D Elevation Program for this + domain. The points are clipped to the domain, reprojected to the domain's + coordinate reference system, and stored as a point cloud you can build on — + most directly as a canopy height model, which in turn feeds a tree + inventory. + + The point cloud is returned immediately with `status` = `pending` and is + fetched in the background: `status` becomes `running`, then `completed` once + the points are stored and `georeference` and `summary` are filled in — or + `failed` if the fetch cannot be completed. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + 3DEP is airborne, so the resulting point cloud is always type `als`. There + is no acquisition type to choose. + + ## Choosing acquisitions + + 3DEP is published as separate surveys, which overlap and differ in age and + point density. By default the backend prefers a single survey that covers + the whole domain, and otherwise combines the fewest surveys that fill it — + each additional survey introduces a seam between flights of different dates + and densities. Pass `datasets` to pin the fetch to specific surveys + instead; check the coverage endpoint first to see what is available. + + Survey boundaries are irregular, so a domain is often covered to + 99-point-something percent rather than exactly 100. Any coverage above zero + produces a point cloud, and the fraction actually covered is recorded on the + result as `source.coverage_fraction` — check it if a gap would matter, since + `summary.density` is measured over the points that exist and looks healthy + either way. Use the coverage endpoint to see the shortfall before creating + anything. + + ## Request Body + + - **name**: (optional) Human-readable name. + - **description**: (optional) Longer free-text description. + - **tags**: (optional) Tags for organizing and filtering. + - **datasets**: (optional) Acquisition names to read, in priority order. + Omit to choose automatically. + + ## Coordinate reference system + + Points are reprojected to the domain's CRS. Only horizontal coordinates are + transformed — elevations are stored exactly as USGS published them, never + converted between reference surfaces. + + ## Error Responses + + - **422**: No 3DEP lidar covers this domain, a pinned acquisition is + unknown or does not overlap the domain, or the fetch would exceed the + point budget. + - **429**: A quota was exceeded. + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + body (CreateThreeDepPointCloudRequest): Request body for fetching a point cloud from USGS + 3DEP. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepPointCloudRequest, +) -> Response[HTTPValidationError | PointCloud | QuotaExceededDetail]: + """Create a point cloud from USGS 3DEP + + # Create a Point Cloud from USGS 3DEP + + Fetches public airborne lidar from the USGS 3D Elevation Program for this + domain. The points are clipped to the domain, reprojected to the domain's + coordinate reference system, and stored as a point cloud you can build on — + most directly as a canopy height model, which in turn feeds a tree + inventory. + + The point cloud is returned immediately with `status` = `pending` and is + fetched in the background: `status` becomes `running`, then `completed` once + the points are stored and `georeference` and `summary` are filled in — or + `failed` if the fetch cannot be completed. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + 3DEP is airborne, so the resulting point cloud is always type `als`. There + is no acquisition type to choose. + + ## Choosing acquisitions + + 3DEP is published as separate surveys, which overlap and differ in age and + point density. By default the backend prefers a single survey that covers + the whole domain, and otherwise combines the fewest surveys that fill it — + each additional survey introduces a seam between flights of different dates + and densities. Pass `datasets` to pin the fetch to specific surveys + instead; check the coverage endpoint first to see what is available. + + Survey boundaries are irregular, so a domain is often covered to + 99-point-something percent rather than exactly 100. Any coverage above zero + produces a point cloud, and the fraction actually covered is recorded on the + result as `source.coverage_fraction` — check it if a gap would matter, since + `summary.density` is measured over the points that exist and looks healthy + either way. Use the coverage endpoint to see the shortfall before creating + anything. + + ## Request Body + + - **name**: (optional) Human-readable name. + - **description**: (optional) Longer free-text description. + - **tags**: (optional) Tags for organizing and filtering. + - **datasets**: (optional) Acquisition names to read, in priority order. + Omit to choose automatically. + + ## Coordinate reference system + + Points are reprojected to the domain's CRS. Only horizontal coordinates are + transformed — elevations are stored exactly as USGS published them, never + converted between reference surfaces. + + ## Error Responses + + - **422**: No 3DEP lidar covers this domain, a pinned acquisition is + unknown or does not overlap the domain, or the fetch would exceed the + point budget. + - **429**: A quota was exceeded. + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + body (CreateThreeDepPointCloudRequest): Request body for fetching a point cloud from USGS + 3DEP. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreateThreeDepPointCloudRequest, +) -> HTTPValidationError | PointCloud | QuotaExceededDetail | None: + """Create a point cloud from USGS 3DEP + + # Create a Point Cloud from USGS 3DEP + + Fetches public airborne lidar from the USGS 3D Elevation Program for this + domain. The points are clipped to the domain, reprojected to the domain's + coordinate reference system, and stored as a point cloud you can build on — + most directly as a canopy height model, which in turn feeds a tree + inventory. + + The point cloud is returned immediately with `status` = `pending` and is + fetched in the background: `status` becomes `running`, then `completed` once + the points are stored and `georeference` and `summary` are filled in — or + `failed` if the fetch cannot be completed. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + 3DEP is airborne, so the resulting point cloud is always type `als`. There + is no acquisition type to choose. + + ## Choosing acquisitions + + 3DEP is published as separate surveys, which overlap and differ in age and + point density. By default the backend prefers a single survey that covers + the whole domain, and otherwise combines the fewest surveys that fill it — + each additional survey introduces a seam between flights of different dates + and densities. Pass `datasets` to pin the fetch to specific surveys + instead; check the coverage endpoint first to see what is available. + + Survey boundaries are irregular, so a domain is often covered to + 99-point-something percent rather than exactly 100. Any coverage above zero + produces a point cloud, and the fraction actually covered is recorded on the + result as `source.coverage_fraction` — check it if a gap would matter, since + `summary.density` is measured over the points that exist and looks healthy + either way. Use the coverage endpoint to see the shortfall before creating + anything. + + ## Request Body + + - **name**: (optional) Human-readable name. + - **description**: (optional) Longer free-text description. + - **tags**: (optional) Tags for organizing and filtering. + - **datasets**: (optional) Acquisition names to read, in priority order. + Omit to choose automatically. + + ## Coordinate reference system + + Points are reprojected to the domain's CRS. Only horizontal coordinates are + transformed — elevations are stored exactly as USGS published them, never + converted between reference surfaces. + + ## Error Responses + + - **422**: No 3DEP lidar covers this domain, a pinned acquisition is + unknown or does not overlap the domain, or the fetch would exceed the + point budget. + - **429**: A quota was exceeded. + - **503**: The USGS 3DEP catalog is temporarily unreachable. + + Args: + domain_id (str): + body (CreateThreeDepPointCloudRequest): Request body for fetching a point cloud from USGS + 3DEP. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/create_point_cloud_upload.py b/fastfuels_sdk/v2/client_library/api/point_clouds/create_point_cloud_upload.py new file mode 100644 index 0000000..4b9fc79 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/create_point_cloud_upload.py @@ -0,0 +1,356 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.create_point_cloud_upload_request import CreatePointCloudUploadRequest +from ...models.http_validation_error import HTTPValidationError +from ...models.point_cloud_upload_created_response import ( + PointCloudUploadCreatedResponse, +) +from ...models.quota_exceeded_detail import QuotaExceededDetail +from ...types import Response + + +def _get_kwargs( + domain_id: str, + *, + body: CreatePointCloudUploadRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/domains/{domain_id}/pointclouds/upload".format( + domain_id=quote(str(domain_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail | None: + if response.status_code == 201: + response_201 = PointCloudUploadCreatedResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = QuotaExceededDetail.from_dict(response.json()) + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ + HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail +]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudUploadRequest, +) -> Response[ + HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail +]: + r"""Create a point cloud from a direct file upload + + # Upload a Point Cloud + + Creates a point cloud resource and returns a signed URL for uploading the + source file directly to storage. Upload is a two-step flow: + + 1. **POST** this request to create the point cloud and receive an `upload` + spec containing a signed URL. + 2. **PUT** your file to `upload.url`, sending **every header in + `upload.headers`** exactly as given — the signed URL commits to them, + and the upload is rejected if any is missing or altered. The file must + not exceed `upload.max_size_bytes`, and the upload must complete before + `upload.expires_at`. For example: + + ```bash + curl -X PUT --upload-file cloud.laz -H \"Content-Type: application/octet-stream\" + -H \"x-goog-content-length-range: 0,1073741824\" \"\" + ``` + + The point cloud is returned immediately with `status` = `pending`. Once the + file finishes uploading it is ingested in the background: `status` becomes + `running`, then `completed` after the cloud is validated and its + `georeference` and `summary` are filled in — or `failed` if the file cannot + be read as a point cloud with a coordinate reference system. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + ## Supported formats + + Upload an uncompressed **LAS** or compressed **LAZ** file (including Cloud + Optimized Point Clouds, which are valid LAZ). The format is detected from + the file itself — there is nothing to declare. + + ## Coordinate reference system + + The file must carry a coordinate reference system; uploads without one are + rejected during ingestion. A cloud in a different CRS than its domain is + automatically reprojected to the domain CRS (horizontal coordinates only — + elevations are preserved as-is), so the stored cloud is always in the + domain CRS. + + Args: + domain_id (str): + body (CreatePointCloudUploadRequest): Request body for creating a point cloud from a + direct file upload. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudUploadRequest, +) -> HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail | None: + r"""Create a point cloud from a direct file upload + + # Upload a Point Cloud + + Creates a point cloud resource and returns a signed URL for uploading the + source file directly to storage. Upload is a two-step flow: + + 1. **POST** this request to create the point cloud and receive an `upload` + spec containing a signed URL. + 2. **PUT** your file to `upload.url`, sending **every header in + `upload.headers`** exactly as given — the signed URL commits to them, + and the upload is rejected if any is missing or altered. The file must + not exceed `upload.max_size_bytes`, and the upload must complete before + `upload.expires_at`. For example: + + ```bash + curl -X PUT --upload-file cloud.laz -H \"Content-Type: application/octet-stream\" + -H \"x-goog-content-length-range: 0,1073741824\" \"\" + ``` + + The point cloud is returned immediately with `status` = `pending`. Once the + file finishes uploading it is ingested in the background: `status` becomes + `running`, then `completed` after the cloud is validated and its + `georeference` and `summary` are filled in — or `failed` if the file cannot + be read as a point cloud with a coordinate reference system. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + ## Supported formats + + Upload an uncompressed **LAS** or compressed **LAZ** file (including Cloud + Optimized Point Clouds, which are valid LAZ). The format is detected from + the file itself — there is nothing to declare. + + ## Coordinate reference system + + The file must carry a coordinate reference system; uploads without one are + rejected during ingestion. A cloud in a different CRS than its domain is + automatically reprojected to the domain CRS (horizontal coordinates only — + elevations are preserved as-is), so the stored cloud is always in the + domain CRS. + + Args: + domain_id (str): + body (CreatePointCloudUploadRequest): Request body for creating a point cloud from a + direct file upload. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudUploadRequest, +) -> Response[ + HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail +]: + r"""Create a point cloud from a direct file upload + + # Upload a Point Cloud + + Creates a point cloud resource and returns a signed URL for uploading the + source file directly to storage. Upload is a two-step flow: + + 1. **POST** this request to create the point cloud and receive an `upload` + spec containing a signed URL. + 2. **PUT** your file to `upload.url`, sending **every header in + `upload.headers`** exactly as given — the signed URL commits to them, + and the upload is rejected if any is missing or altered. The file must + not exceed `upload.max_size_bytes`, and the upload must complete before + `upload.expires_at`. For example: + + ```bash + curl -X PUT --upload-file cloud.laz -H \"Content-Type: application/octet-stream\" + -H \"x-goog-content-length-range: 0,1073741824\" \"\" + ``` + + The point cloud is returned immediately with `status` = `pending`. Once the + file finishes uploading it is ingested in the background: `status` becomes + `running`, then `completed` after the cloud is validated and its + `georeference` and `summary` are filled in — or `failed` if the file cannot + be read as a point cloud with a coordinate reference system. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + ## Supported formats + + Upload an uncompressed **LAS** or compressed **LAZ** file (including Cloud + Optimized Point Clouds, which are valid LAZ). The format is detected from + the file itself — there is nothing to declare. + + ## Coordinate reference system + + The file must carry a coordinate reference system; uploads without one are + rejected during ingestion. A cloud in a different CRS than its domain is + automatically reprojected to the domain CRS (horizontal coordinates only — + elevations are preserved as-is), so the stored cloud is always in the + domain CRS. + + Args: + domain_id (str): + body (CreatePointCloudUploadRequest): Request body for creating a point cloud from a + direct file upload. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + body: CreatePointCloudUploadRequest, +) -> HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail | None: + r"""Create a point cloud from a direct file upload + + # Upload a Point Cloud + + Creates a point cloud resource and returns a signed URL for uploading the + source file directly to storage. Upload is a two-step flow: + + 1. **POST** this request to create the point cloud and receive an `upload` + spec containing a signed URL. + 2. **PUT** your file to `upload.url`, sending **every header in + `upload.headers`** exactly as given — the signed URL commits to them, + and the upload is rejected if any is missing or altered. The file must + not exceed `upload.max_size_bytes`, and the upload must complete before + `upload.expires_at`. For example: + + ```bash + curl -X PUT --upload-file cloud.laz -H \"Content-Type: application/octet-stream\" + -H \"x-goog-content-length-range: 0,1073741824\" \"\" + ``` + + The point cloud is returned immediately with `status` = `pending`. Once the + file finishes uploading it is ingested in the background: `status` becomes + `running`, then `completed` after the cloud is validated and its + `georeference` and `summary` are filled in — or `failed` if the file cannot + be read as a point cloud with a coordinate reference system. Poll + `GET /domains/{domain_id}/pointclouds/{id}` to follow progress. + + ## Supported formats + + Upload an uncompressed **LAS** or compressed **LAZ** file (including Cloud + Optimized Point Clouds, which are valid LAZ). The format is detected from + the file itself — there is nothing to declare. + + ## Coordinate reference system + + The file must carry a coordinate reference system; uploads without one are + rejected during ingestion. A cloud in a different CRS than its domain is + automatically reprojected to the domain CRS (horizontal coordinates only — + elevations are preserved as-is), so the stored cloud is always in the + domain CRS. + + Args: + domain_id (str): + body (CreatePointCloudUploadRequest): Request body for creating a point cloud from a + direct file upload. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloudUploadCreatedResponse | QuotaExceededDetail + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/delete_point_cloud.py b/fastfuels_sdk/v2/client_library/api/point_clouds/delete_point_cloud.py new file mode 100644 index 0000000..5837222 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/delete_point_cloud.py @@ -0,0 +1,245 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + domain_id: str, + point_cloud_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/domains/{domain_id}/pointclouds/{point_cloud_id}".format( + domain_id=quote(str(domain_id), safe=""), + point_cloud_id=quote(str(point_cloud_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a point cloud + + # Delete Point Cloud + + Permanently deletes a point cloud by its unique identifier, including the + stored point data in GCS. This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a point cloud + + # Delete Point Cloud + + Permanently deletes a point cloud by its unique identifier, including the + stored point data in GCS. This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | HTTPValidationError]: + """Delete a point cloud + + # Delete Point Cloud + + Permanently deletes a point cloud by its unique identifier, including the + stored point data in GCS. This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Any | HTTPValidationError | None: + """Delete a point cloud + + # Delete Point Cloud + + Permanently deletes a point cloud by its unique identifier, including the + stored point data in GCS. This action cannot be undone. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Response + + Returns HTTP 204 No Content with an empty response body. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/get_point_cloud.py b/fastfuels_sdk/v2/client_library/api/point_clouds/get_point_cloud.py new file mode 100644 index 0000000..25df4e8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/get_point_cloud.py @@ -0,0 +1,243 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.point_cloud import PointCloud +from ...types import Response + + +def _get_kwargs( + domain_id: str, + point_cloud_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/pointclouds/{point_cloud_id}".format( + domain_id=quote(str(domain_id), safe=""), + point_cloud_id=quote(str(point_cloud_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PointCloud | None: + if response.status_code == 200: + response_200 = PointCloud.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | PointCloud]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PointCloud]: + """Get a point cloud by ID + + # Get Point Cloud + + Retrieves a specific point cloud resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique 32-character hex identifier. + + ## Response + + Returns the point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PointCloud | None: + """Get a point cloud by ID + + # Get Point Cloud + + Retrieves a specific point cloud resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique 32-character hex identifier. + + ## Response + + Returns the point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud + """ + + return sync_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PointCloud]: + """Get a point cloud by ID + + # Get Point Cloud + + Retrieves a specific point cloud resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique 32-character hex identifier. + + ## Response + + Returns the point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PointCloud | None: + """Get a point cloud by ID + + # Get Point Cloud + + Retrieves a specific point cloud resource by its unique identifier. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique 32-character hex identifier. + + ## Response + + Returns the point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds.py b/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds.py new file mode 100644 index 0000000..39fe51a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds.py @@ -0,0 +1,406 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.list_point_clouds_response import ListPointCloudsResponse +from ...models.point_cloud_sort_field import PointCloudSortField +from ...models.point_cloud_type import PointCloudType +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + domain_id: str, + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, PointCloudSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, PointCloudType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/{domain_id}/pointclouds".format( + domain_id=quote(str(domain_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListPointCloudsResponse | None: + if response.status_code == 200: + response_200 = ListPointCloudsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + """List point clouds in a domain + + # List Point Clouds (Domain) + + Retrieves a paginated list of the point clouds within a single domain + belonging to the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list point clouds for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListPointCloudsResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListPointCloudsResponse | None: + """List point clouds in a domain + + # List Point Clouds (Domain) + + Retrieves a paginated list of the point clouds within a single domain + belonging to the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list point clouds for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListPointCloudsResponse + """ + + return sync_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + """List point clouds in a domain + + # List Point Clouds (Domain) + + Retrieves a paginated list of the point clouds within a single domain + belonging to the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list point clouds for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListPointCloudsResponse] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListPointCloudsResponse | None: + """List point clouds in a domain + + # List Point Clouds (Domain) + + Retrieves a paginated list of the point clouds within a single domain + belonging to the authenticated user. + + ## Path Parameters + + - **domain_id**: (string) The domain to list point clouds for. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + domain_id (str): + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListPointCloudsResponse + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds_cross_domain.py b/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds_cross_domain.py new file mode 100644 index 0000000..5315597 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/list_point_clouds_cross_domain.py @@ -0,0 +1,374 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.list_point_clouds_response import ListPointCloudsResponse +from ...models.point_cloud_sort_field import PointCloudSortField +from ...models.point_cloud_type import PointCloudType +from ...models.sort_order import SortOrder +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["size"] = size + + json_sort_by: None | str | Unset + if isinstance(sort_by, Unset): + json_sort_by = UNSET + elif isinstance(sort_by, PointCloudSortField): + json_sort_by = sort_by.value + else: + json_sort_by = sort_by + params["sort_by"] = json_sort_by + + json_sort_order: None | str | Unset + if isinstance(sort_order, Unset): + json_sort_order = UNSET + elif isinstance(sort_order, SortOrder): + json_sort_order = sort_order.value + else: + json_sort_order = sort_order + params["sort_order"] = json_sort_order + + json_type_: None | str | Unset + if isinstance(type_, Unset): + json_type_ = UNSET + elif isinstance(type_, PointCloudType): + json_type_ = type_.value + else: + json_type_ = type_ + params["type"] = json_type_ + + json_source: None | str | Unset + if isinstance(source, Unset): + json_source = UNSET + else: + json_source = source + params["source"] = json_source + + json_tag: None | str | Unset + if isinstance(tag, Unset): + json_tag = UNSET + else: + json_tag = tag + params["tag"] = json_tag + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/domains/-/pointclouds", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | ListPointCloudsResponse | None: + if response.status_code == 200: + response_200 = ListPointCloudsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + """List point clouds across all domains + + # List Point Clouds (All Domains) + + Retrieves a paginated list of every point cloud belonging to the + authenticated user, across all of their domains. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListPointCloudsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListPointCloudsResponse | None: + """List point clouds across all domains + + # List Point Clouds (All Domains) + + Retrieves a paginated list of every point cloud belonging to the + authenticated user, across all of their domains. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListPointCloudsResponse + """ + + return sync_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> Response[HTTPValidationError | ListPointCloudsResponse]: + """List point clouds across all domains + + # List Point Clouds (All Domains) + + Retrieves a paginated list of every point cloud belonging to the + authenticated user, across all of their domains. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | ListPointCloudsResponse] + """ + + kwargs = _get_kwargs( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + page: int | Unset = 0, + size: int | Unset = 100, + sort_by: None | PointCloudSortField | Unset = UNSET, + sort_order: None | SortOrder | Unset = UNSET, + type_: None | PointCloudType | Unset = UNSET, + source: None | str | Unset = UNSET, + tag: None | str | Unset = UNSET, +) -> HTTPValidationError | ListPointCloudsResponse | None: + """List point clouds across all domains + + # List Point Clouds (All Domains) + + Retrieves a paginated list of every point cloud belonging to the + authenticated user, across all of their domains. + + ## Query Parameters + + - **page**: (integer, optional) Page number (zero-indexed). Default: 0. + - **size**: (integer, optional) Items per page (1-1000). Default: 100. + - **sort_by**: (string, optional) Field to sort by: `created_on`, `modified_on`, `name`. + - **sort_order**: (string, optional) Sort direction: `ascending`, `descending`. + - **type**: (string, optional) Filter by acquisition type: `als` or `tls`. + - **source**: (string, optional) Filter by source name (e.g., `3dep`, `upload`). + - **tag**: (string, optional) Filter point clouds that contain this tag. + + ## Response + + Returns a paginated list of point clouds with metadata. + + Args: + page (int | Unset): The page number to retrieve (zero-indexed). Default: 0. + size (int | Unset): The number of point clouds to retrieve per page. Default: 100. + sort_by (None | PointCloudSortField | Unset): The field to sort results by. + sort_order (None | SortOrder | Unset): The order to sort results (ascending or + descending). + type_ (None | PointCloudType | Unset): Filter point clouds by acquisition type (`als` or + `tls`). + source (None | str | Unset): Filter point clouds by source name (e.g., `3dep`, `upload`). + tag (None | str | Unset): Filter point clouds that contain this tag. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | ListPointCloudsResponse + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order, + type_=type_, + source=source, + tag=tag, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/point_clouds/update_point_cloud.py b/fastfuels_sdk/v2/client_library/api/point_clouds/update_point_cloud.py new file mode 100644 index 0000000..b18f008 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/point_clouds/update_point_cloud.py @@ -0,0 +1,360 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...models.point_cloud import PointCloud +from ...models.update_point_cloud_request_body import UpdatePointCloudRequestBody +from ...types import Response + + +def _get_kwargs( + domain_id: str, + point_cloud_id: str, + *, + body: UpdatePointCloudRequestBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/domains/{domain_id}/pointclouds/{point_cloud_id}".format( + domain_id=quote(str(domain_id), safe=""), + point_cloud_id=quote(str(point_cloud_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PointCloud | None: + if response.status_code == 200: + response_200 = PointCloud.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HTTPValidationError | PointCloud]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, + body: UpdatePointCloudRequestBody, +) -> Response[HTTPValidationError | PointCloud]: + """Update a point cloud + + # Update Point Cloud + + Updates the metadata of an existing point cloud. Only the fields provided in + the request body are modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the point cloud. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following are immutable through this endpoint: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the point cloud's content is rebuilt, never + via metadata updates) + + The **modified_on** field is updated automatically. + + ## Response + + Returns the updated point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + body (UpdatePointCloudRequestBody): Request body for updating point cloud metadata. + + Only metadata is mutable. The point cloud's content, source, and derived + fields cannot be changed through this endpoint, so updates never alter the + `checksum`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, + body: UpdatePointCloudRequestBody, +) -> HTTPValidationError | PointCloud | None: + """Update a point cloud + + # Update Point Cloud + + Updates the metadata of an existing point cloud. Only the fields provided in + the request body are modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the point cloud. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following are immutable through this endpoint: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the point cloud's content is rebuilt, never + via metadata updates) + + The **modified_on** field is updated automatically. + + ## Response + + Returns the updated point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + body (UpdatePointCloudRequestBody): Request body for updating point cloud metadata. + + Only metadata is mutable. The point cloud's content, source, and derived + fields cannot be changed through this endpoint, so updates never alter the + `checksum`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud + """ + + return sync_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, + body: UpdatePointCloudRequestBody, +) -> Response[HTTPValidationError | PointCloud]: + """Update a point cloud + + # Update Point Cloud + + Updates the metadata of an existing point cloud. Only the fields provided in + the request body are modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the point cloud. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following are immutable through this endpoint: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the point cloud's content is rebuilt, never + via metadata updates) + + The **modified_on** field is updated automatically. + + ## Response + + Returns the updated point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + body (UpdatePointCloudRequestBody): Request body for updating point cloud metadata. + + Only metadata is mutable. The point cloud's content, source, and derived + fields cannot be changed through this endpoint, so updates never alter the + `checksum`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HTTPValidationError | PointCloud] + """ + + kwargs = _get_kwargs( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + domain_id: str, + point_cloud_id: str, + *, + client: AuthenticatedClient, + body: UpdatePointCloudRequestBody, +) -> HTTPValidationError | PointCloud | None: + """Update a point cloud + + # Update Point Cloud + + Updates the metadata of an existing point cloud. Only the fields provided in + the request body are modified. + + ## Path Parameters + + - **domain_id**: (string) The domain the point cloud belongs to. + - **point_cloud_id**: (string) The unique identifier of the point cloud. + + ## Request Body + + All fields are optional: + + - **name**: (string) New name for the point cloud. + - **description**: (string) New description. + - **tags**: (array of strings) New tags (replaces existing). + + ## What Cannot Be Updated + + The following are immutable through this endpoint: + + - **id**, **domain_id**, **type**, **source**, **georeference** + - **created_on** (creation timestamp is permanent) + - **checksum** (changes only when the point cloud's content is rebuilt, never + via metadata updates) + + The **modified_on** field is updated automatically. + + ## Response + + Returns the updated point cloud resource. + + ## Error Responses + + - **404 Not Found**: The point cloud does not exist or the user does not have access. + + Args: + domain_id (str): + point_cloud_id (str): + body (UpdatePointCloudRequestBody): Request body for updating point cloud metadata. + + Only metadata is mutable. The point cloud's content, source, and derived + fields cannot be changed through this endpoint, so updates never alter the + `checksum`. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HTTPValidationError | PointCloud + """ + + return ( + await asyncio_detailed( + domain_id=domain_id, + point_cloud_id=point_cloud_id, + client=client, + body=body, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/users/__init__.py b/fastfuels_sdk/v2/client_library/api/users/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/users/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fastfuels_sdk/v2/client_library/api/users/get_me.py b/fastfuels_sdk/v2/client_library/api/users/get_me.py new file mode 100644 index 0000000..367f883 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/users/get_me.py @@ -0,0 +1,136 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.user_me_response import UserMeResponse +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/users/me", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> UserMeResponse | None: + if response.status_code == 200: + response_200 = UserMeResponse.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[UserMeResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, +) -> Response[UserMeResponse]: + """Get the authenticated owner + + Return the authenticated owner's identity, tier, and resolved quotas. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UserMeResponse] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, +) -> UserMeResponse | None: + """Get the authenticated owner + + Return the authenticated owner's identity, tier, and resolved quotas. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UserMeResponse + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, +) -> Response[UserMeResponse]: + """Get the authenticated owner + + Return the authenticated owner's identity, tier, and resolved quotas. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UserMeResponse] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, +) -> UserMeResponse | None: + """Get the authenticated owner + + Return the authenticated owner's identity, tier, and resolved quotas. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UserMeResponse + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/api/users/get_me_usage.py b/fastfuels_sdk/v2/client_library/api/users/get_me_usage.py new file mode 100644 index 0000000..57ae66d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/api/users/get_me_usage.py @@ -0,0 +1,136 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.usage import Usage +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/users/me/usage", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Usage | None: + if response.status_code == 200: + response_200 = Usage.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Usage]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, +) -> Response[Usage]: + """Get the authenticated owner's usage + + Return current usage against the owner's resolved limits, per resource type. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Usage] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, +) -> Usage | None: + """Get the authenticated owner's usage + + Return current usage against the owner's resolved limits, per resource type. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Usage + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, +) -> Response[Usage]: + """Get the authenticated owner's usage + + Return current usage against the owner's resolved limits, per resource type. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Usage] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, +) -> Usage | None: + """Get the authenticated owner's usage + + Return current usage against the owner's resolved limits, per resource type. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Usage + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/fastfuels_sdk/v2/client_library/base_url.py b/fastfuels_sdk/v2/client_library/base_url.py new file mode 100644 index 0000000..e4c13ce --- /dev/null +++ b/fastfuels_sdk/v2/client_library/base_url.py @@ -0,0 +1,8 @@ +"""Deployment URL of the FastFuels v2 API this client was generated against. + +Written by generate_client.sh (the OpenAPI spec carries no `servers` +entry and openapi-python-client takes base_url at construction time, so +the regen script records the URL alongside the client it generates). +""" + +DEFAULT_BASE_URL = "https://api-v2-prod-782971006568.us-west1.run.app" diff --git a/fastfuels_sdk/v2/client_library/client.py b/fastfuels_sdk/v2/client_library/client.py new file mode 100644 index 0000000..a5f74f6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/client.py @@ -0,0 +1,282 @@ +import ssl +from typing import Any + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client( + self, async_client: httpx.AsyncClient + ) -> "AuthenticatedClient": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/fastfuels_sdk/v2/client_library/errors.py b/fastfuels_sdk/v2/client_library/errors.py new file mode 100644 index 0000000..5f92e76 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/fastfuels_sdk/v2/client_library/models/__init__.py b/fastfuels_sdk/v2/client_library/models/__init__.py new file mode 100644 index 0000000..1be370f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/__init__.py @@ -0,0 +1,553 @@ +"""Contains all the data models used in inputs/outputs""" + +from .access import Access +from .allometry_biomass_source import AllometryBiomassSource +from .allometry_biomass_source_component_states import ( + AllometryBiomassSourceComponentStates, +) +from .allometry_max_crown_radius_source import AllometryMaxCrownRadiusSource +from .application import Application +from .application_quota_overrides_type_0 import ApplicationQuotaOverridesType0 +from .apply_grid_modifications_request import ApplyGridModificationsRequest +from .apply_modifications_request import ApplyModificationsRequest +from .apply_treatments_request import ApplyTreatmentsRequest +from .band import Band +from .band_type import BandType +from .base_model import BaseModel +from .biomass_component import BiomassComponent +from .biomass_component_state import BiomassComponentState +from .biomass_equations import BiomassEquations +from .biomass_unit import BiomassUnit +from .categorical_band_summary import CategoricalBandSummary +from .categorical_column_summary import CategoricalColumnSummary +from .chunks import Chunks +from .chunks_count_by_axis_type_0 import ChunksCountByAxisType0 +from .column import Column +from .column_type import ColumnType +from .compose_attribute_condition import ComposeAttributeCondition +from .compose_comparison_operator import ComposeComparisonOperator +from .compose_compute import ComposeCompute +from .compose_input import ComposeInput +from .compose_literal import ComposeLiteral +from .compose_operator import ComposeOperator +from .compose_select import ComposeSelect +from .continuous_band_summary import ContinuousBandSummary +from .continuous_column_summary import ContinuousColumnSummary +from .count_usage import CountUsage +from .create_application_request import CreateApplicationRequest +from .create_chm_inventory_request import CreateChmInventoryRequest +from .create_compose_request import CreateComposeRequest +from .create_duet_request import CreateDuetRequest +from .create_fbfm_13_lookup_request import CreateFbfm13LookupRequest +from .create_fbfm_40_lookup_request import CreateFbfm40LookupRequest +from .create_fccs_lookup_request import CreateFccsLookupRequest +from .create_gdam_inventory_request import CreateGdamInventoryRequest +from .create_gdam_inventory_request_impute_columns_item import ( + CreateGdamInventoryRequestImputeColumnsItem, +) +from .create_geo_tiff_upload_request import CreateGeoTIFFUploadRequest +from .create_inventory_upload_request import CreateInventoryUploadRequest +from .create_key_request import CreateKeyRequest +from .create_key_response import CreateKeyResponse +from .create_landfire_canopy_request import CreateLandfireCanopyRequest +from .create_landfire_fbfm_13_request import CreateLandfireFbfm13Request +from .create_landfire_fbfm_40_request import CreateLandfireFbfm40Request +from .create_landfire_fccs_request import CreateLandfireFccsRequest +from .create_landfire_topography_request import CreateLandfireTopographyRequest +from .create_layerset_rasterize_request import CreateLayersetRasterizeRequest +from .create_layerset_request_body import CreateLayersetRequestBody +from .create_meta_chm_request import CreateMetaChmRequest +from .create_naip_chm_request import CreateNaipChmRequest +from .create_netcdf_upload_request import CreateNetcdfUploadRequest +from .create_osm_road_feature_request import CreateOsmRoadFeatureRequest +from .create_osm_water_feature_request import CreateOsmWaterFeatureRequest +from .create_pim_inventory_request import CreatePimInventoryRequest +from .create_point_cloud_chm_request import CreatePointCloudChmRequest +from .create_point_cloud_upload_request import CreatePointCloudUploadRequest +from .create_resample_request import CreateResampleRequest +from .create_resample_request_method_overrides import ( + CreateResampleRequestMethodOverrides, +) +from .create_three_dep_point_cloud_request import CreateThreeDepPointCloudRequest +from .create_three_dep_topography_request import CreateThreeDepTopographyRequest +from .create_tree_inventory_request import CreateTreeInventoryRequest +from .create_tree_map_request import CreateTreeMapRequest +from .create_uniform_request import CreateUniformRequest +from .crown_profile_model import CrownProfileModel +from .dense_grid_data import DenseGridData +from .distribution import Distribution +from .domain import Domain +from .domain_lattice import DomainLattice +from .domain_sort_field import DomainSortField +from .domain_sort_order import DomainSortOrder +from .domain_style import DomainStyle +from .duet_band import DuetBand +from .duet_calibration import DuetCalibration +from .duet_constant_calibration_target import DuetConstantCalibrationTarget +from .duet_max_min_calibration_target import DuetMaxMinCalibrationTarget +from .duet_mean_sd_calibration_target import DuetMeanSdCalibrationTarget +from .duet_parameter_calibration import DuetParameterCalibration +from .duplicate_grid_request import DuplicateGridRequest +from .duplicate_inventory_request import DuplicateInventoryRequest +from .export import Export +from .export_grid_request import ExportGridRequest +from .export_inventory_request import ExportInventoryRequest +from .export_sort_field import ExportSortField +from .export_source import ExportSource +from .fbfm_13_lookup_band import Fbfm13LookupBand +from .fbfm_40_lookup_band import Fbfm40LookupBand +from .fccs_lookup_band import FccsLookupBand +from .feature import Feature +from .feature_data_metadata import FeatureDataMetadata +from .feature_georeference import FeatureGeoreference +from .feature_partition_info import FeaturePartitionInfo +from .feature_sort_field import FeatureSortField +from .feature_source import FeatureSource +from .feature_type import FeatureType +from .fia_species_group_share import FIASpeciesGroupShare +from .field_source import FieldSource +from .fine_biomass_config import FineBiomassConfig +from .geo_json_crs import GeoJsonCRS +from .geo_json_crs_properties import GeoJsonCRSProperties +from .geo_json_feature import GeoJsonFeature +from .geo_json_feature_collection import GeoJsonFeatureCollection +from .geo_json_feature_properties_type_0 import GeoJsonFeaturePropertiesType0 +from .geometry_collection import GeometryCollection +from .georeference import Georeference +from .georeference_3d import Georeference3D +from .grid import Grid +from .grid_alignment_domain_target import GridAlignmentDomainTarget +from .grid_alignment_grid_target import GridAlignmentGridTarget +from .grid_alignment_native_target import GridAlignmentNativeTarget +from .grid_data_array_format import GridDataArrayFormat +from .grid_data_chunk_metadata import GridDataChunkMetadata +from .grid_data_order import GridDataOrder +from .grid_data_response import GridDataResponse +from .grid_data_response_order import GridDataResponseOrder +from .grid_export_format import GridExportFormat +from .grid_feature_spatial_condition import GridFeatureSpatialCondition +from .grid_geometry_spatial_condition import GridGeometrySpatialCondition +from .grid_geometry_spatial_condition_crs_type_0 import ( + GridGeometrySpatialConditionCrsType0, +) +from .grid_geometry_spatial_condition_geometry import ( + GridGeometrySpatialConditionGeometry, +) +from .grid_modification import GridModification +from .grid_modification_action import GridModificationAction +from .grid_modification_condition import GridModificationCondition +from .grid_sort_field import GridSortField +from .grid_source import GridSource +from .grid_spatial_target import GridSpatialTarget +from .grid_upload_created_response import GridUploadCreatedResponse +from .grid_upload_spec import GridUploadSpec +from .grid_upload_spec_headers import GridUploadSpecHeaders +from .http_validation_error import HTTPValidationError +from .inline_compute import InlineCompute +from .inventory import Inventory +from .inventory_attribute import InventoryAttribute +from .inventory_basal_area_treatment import InventoryBasalAreaTreatment +from .inventory_biomass_column import InventoryBiomassColumn +from .inventory_column_mapping import InventoryColumnMapping +from .inventory_column_max_crown_radius_source import ( + InventoryColumnMaxCrownRadiusSource, +) +from .inventory_columns_biomass_source import InventoryColumnsBiomassSource +from .inventory_columns_biomass_source_columns import ( + InventoryColumnsBiomassSourceColumns, +) +from .inventory_columns_biomass_source_component_states import ( + InventoryColumnsBiomassSourceComponentStates, +) +from .inventory_data_metadata import InventoryDataMetadata +from .inventory_data_response import InventoryDataResponse +from .inventory_data_response_data_type_1_item import InventoryDataResponseDataType1Item +from .inventory_diameter_treatment import InventoryDiameterTreatment +from .inventory_diameter_treatment_method import InventoryDiameterTreatmentMethod +from .inventory_export_format import InventoryExportFormat +from .inventory_expression_condition import InventoryExpressionCondition +from .inventory_feature_spatial_condition import InventoryFeatureSpatialCondition +from .inventory_geometry_spatial_condition import InventoryGeometrySpatialCondition +from .inventory_geometry_spatial_condition_crs_type_0 import ( + InventoryGeometrySpatialConditionCrsType0, +) +from .inventory_geometry_spatial_condition_geometry import ( + InventoryGeometrySpatialConditionGeometry, +) +from .inventory_georeference import InventoryGeoreference +from .inventory_json_orientation import InventoryJsonOrientation +from .inventory_modification import InventoryModification +from .inventory_modification_action import InventoryModificationAction +from .inventory_modification_condition import InventoryModificationCondition +from .inventory_partition_info import InventoryPartitionInfo +from .inventory_sort_field import InventorySortField +from .inventory_source import InventorySource +from .inventory_treatment_method import InventoryTreatmentMethod +from .inventory_type import InventoryType +from .inventory_upload_created_response import InventoryUploadCreatedResponse +from .inventory_upload_format import InventoryUploadFormat +from .inventory_upload_spec import InventoryUploadSpec +from .inventory_upload_spec_headers import InventoryUploadSpecHeaders +from .job_error import JobError +from .job_progress import JobProgress +from .job_resource_usage import JobResourceUsage +from .job_status import JobStatus +from .key import Key +from .landfire_canopy_fuel_band import LandfireCanopyFuelBand +from .landfire_canopy_version import LandfireCanopyVersion +from .landfire_fbfm_13_version import LandfireFbfm13Version +from .landfire_fbfm_40_version import LandfireFbfm40Version +from .landfire_fccs_version import LandfireFccsVersion +from .landfire_topography_version import LandfireTopographyVersion +from .landscape_export_alignment_domain_target import ( + LandscapeExportAlignmentDomainTarget, +) +from .landscape_export_alignment_grid_target import LandscapeExportAlignmentGridTarget +from .landscape_export_request import LandscapeExportRequest +from .landscape_export_request_fire_behavior_fuel_model import ( + LandscapeExportRequestFireBehaviorFuelModel, +) +from .landscape_field_source import LandscapeFieldSource +from .layerset_crs import LayersetCrs +from .layerset_crs_properties import LayersetCrsProperties +from .layerset_feature import LayersetFeature +from .layerset_properties import LayersetProperties +from .line_string import LineString +from .list_applications_response import ListApplicationsResponse +from .list_domains_response import ListDomainsResponse +from .list_exports_response import ListExportsResponse +from .list_features_response import ListFeaturesResponse +from .list_grids_response import ListGridsResponse +from .list_inventories_response import ListInventoriesResponse +from .list_keys_response import ListKeysResponse +from .list_point_clouds_response import ListPointCloudsResponse +from .max_crown_radius_unit import MaxCrownRadiusUnit +from .meta_chm_version import MetaCHMVersion +from .modifier import Modifier +from .moisture_model import MoistureModel +from .multi_line_string import MultiLineString +from .multi_point import MultiPoint +from .multi_polygon import MultiPolygon +from .non_burnable_fuel_model import NonBurnableFuelModel +from .operator import Operator +from .overlap_method import OverlapMethod +from .point import Point +from .point_cloud import PointCloud +from .point_cloud_georeference import PointCloudGeoreference +from .point_cloud_sort_field import PointCloudSortField +from .point_cloud_source import PointCloudSource +from .point_cloud_summary import PointCloudSummary +from .point_cloud_three_dep_coverage_response import PointCloudThreeDepCoverageResponse +from .point_cloud_type import PointCloudType +from .point_cloud_upload_created_response import PointCloudUploadCreatedResponse +from .point_cloud_upload_spec import PointCloudUploadSpec +from .point_cloud_upload_spec_headers import PointCloudUploadSpecHeaders +from .point_process import PointProcess +from .polygon import Polygon +from .quic_fire_export_alignment_domain_target import ( + QUICFireExportAlignmentDomainTarget, +) +from .quic_fire_export_alignment_grid_target import QUICFireExportAlignmentGridTarget +from .quicfire_export_request import QuicfireExportRequest +from .quicfire_export_request_moist_merge import QuicfireExportRequestMoistMerge +from .quota_exceeded_detail import QuotaExceededDetail +from .quotas import Quotas +from .remove_action import RemoveAction +from .resampling_method import ResamplingMethod +from .resolution_3d import Resolution3D +from .scope import Scope +from .sort_order import SortOrder +from .sparse_grid_data import SparseGridData +from .spatial_operator import SpatialOperator +from .stem_isolation_lmf import StemIsolationLmf +from .stem_isolation_vwf import StemIsolationVwf +from .three_dep_dataset_coverage import ThreeDepDatasetCoverage +from .three_dep_resolution import ThreeDepResolution +from .topography_band import TopographyBand +from .topography_three_dep_coverage_response import TopographyThreeDepCoverageResponse +from .tree_band import TreeBand +from .tree_forestry_metrics import TreeForestryMetrics +from .tree_map_band import TreeMapBand +from .tree_map_version import TreeMapVersion +from .uniform_band import UniformBand +from .uniform_band_input import UniformBandInput +from .uniform_moisture_value import UniformMoistureValue +from .update_application_request import UpdateApplicationRequest +from .update_domain_request_body import UpdateDomainRequestBody +from .update_export_request_body import UpdateExportRequestBody +from .update_feature_request_body import UpdateFeatureRequestBody +from .update_grid_request_body import UpdateGridRequestBody +from .update_inventory_request_body import UpdateInventoryRequestBody +from .update_point_cloud_request_body import UpdatePointCloudRequestBody +from .upload_band_definition import UploadBandDefinition +from .usage import Usage +from .usage_count import UsageCount +from .usage_lifecycle import UsageLifecycle +from .usage_storage import UsageStorage +from .user_me_response import UserMeResponse +from .user_me_response_kind import UserMeResponseKind +from .validation_error import ValidationError + +__all__ = ( + "Access", + "AllometryBiomassSource", + "AllometryBiomassSourceComponentStates", + "AllometryMaxCrownRadiusSource", + "Application", + "ApplicationQuotaOverridesType0", + "ApplyGridModificationsRequest", + "ApplyModificationsRequest", + "ApplyTreatmentsRequest", + "Band", + "BandType", + "BaseModel", + "BiomassComponent", + "BiomassComponentState", + "BiomassEquations", + "BiomassUnit", + "CategoricalBandSummary", + "CategoricalColumnSummary", + "Chunks", + "ChunksCountByAxisType0", + "Column", + "ColumnType", + "ComposeAttributeCondition", + "ComposeComparisonOperator", + "ComposeCompute", + "ComposeInput", + "ComposeLiteral", + "ComposeOperator", + "ComposeSelect", + "ContinuousBandSummary", + "ContinuousColumnSummary", + "CountUsage", + "CreateApplicationRequest", + "CreateChmInventoryRequest", + "CreateComposeRequest", + "CreateDuetRequest", + "CreateFbfm13LookupRequest", + "CreateFbfm40LookupRequest", + "CreateFccsLookupRequest", + "CreateGdamInventoryRequest", + "CreateGdamInventoryRequestImputeColumnsItem", + "CreateGeoTIFFUploadRequest", + "CreateInventoryUploadRequest", + "CreateKeyRequest", + "CreateKeyResponse", + "CreateLandfireCanopyRequest", + "CreateLandfireFbfm13Request", + "CreateLandfireFbfm40Request", + "CreateLandfireFccsRequest", + "CreateLandfireTopographyRequest", + "CreateLayersetRasterizeRequest", + "CreateLayersetRequestBody", + "CreateMetaChmRequest", + "CreateNaipChmRequest", + "CreateNetcdfUploadRequest", + "CreateOsmRoadFeatureRequest", + "CreateOsmWaterFeatureRequest", + "CreatePimInventoryRequest", + "CreatePointCloudChmRequest", + "CreatePointCloudUploadRequest", + "CreateResampleRequest", + "CreateResampleRequestMethodOverrides", + "CreateThreeDepPointCloudRequest", + "CreateThreeDepTopographyRequest", + "CreateTreeInventoryRequest", + "CreateTreeMapRequest", + "CreateUniformRequest", + "CrownProfileModel", + "DenseGridData", + "Distribution", + "Domain", + "DomainLattice", + "DomainSortField", + "DomainSortOrder", + "DomainStyle", + "DuetBand", + "DuetCalibration", + "DuetConstantCalibrationTarget", + "DuetMaxMinCalibrationTarget", + "DuetMeanSdCalibrationTarget", + "DuetParameterCalibration", + "DuplicateGridRequest", + "DuplicateInventoryRequest", + "Export", + "ExportGridRequest", + "ExportInventoryRequest", + "ExportSortField", + "ExportSource", + "FIASpeciesGroupShare", + "Fbfm13LookupBand", + "Fbfm40LookupBand", + "FccsLookupBand", + "Feature", + "FeatureDataMetadata", + "FeatureGeoreference", + "FeaturePartitionInfo", + "FeatureSortField", + "FeatureSource", + "FeatureType", + "FieldSource", + "FineBiomassConfig", + "GeoJsonCRS", + "GeoJsonCRSProperties", + "GeoJsonFeature", + "GeoJsonFeatureCollection", + "GeoJsonFeaturePropertiesType0", + "GeometryCollection", + "Georeference", + "Georeference3D", + "Grid", + "GridAlignmentDomainTarget", + "GridAlignmentGridTarget", + "GridAlignmentNativeTarget", + "GridDataArrayFormat", + "GridDataChunkMetadata", + "GridDataOrder", + "GridDataResponse", + "GridDataResponseOrder", + "GridExportFormat", + "GridFeatureSpatialCondition", + "GridGeometrySpatialCondition", + "GridGeometrySpatialConditionCrsType0", + "GridGeometrySpatialConditionGeometry", + "GridModification", + "GridModificationAction", + "GridModificationCondition", + "GridSortField", + "GridSource", + "GridSpatialTarget", + "GridUploadCreatedResponse", + "GridUploadSpec", + "GridUploadSpecHeaders", + "HTTPValidationError", + "InlineCompute", + "Inventory", + "InventoryAttribute", + "InventoryBasalAreaTreatment", + "InventoryBiomassColumn", + "InventoryColumnMapping", + "InventoryColumnMaxCrownRadiusSource", + "InventoryColumnsBiomassSource", + "InventoryColumnsBiomassSourceColumns", + "InventoryColumnsBiomassSourceComponentStates", + "InventoryDataMetadata", + "InventoryDataResponse", + "InventoryDataResponseDataType1Item", + "InventoryDiameterTreatment", + "InventoryDiameterTreatmentMethod", + "InventoryExportFormat", + "InventoryExpressionCondition", + "InventoryFeatureSpatialCondition", + "InventoryGeometrySpatialCondition", + "InventoryGeometrySpatialConditionCrsType0", + "InventoryGeometrySpatialConditionGeometry", + "InventoryGeoreference", + "InventoryJsonOrientation", + "InventoryModification", + "InventoryModificationAction", + "InventoryModificationCondition", + "InventoryPartitionInfo", + "InventorySortField", + "InventorySource", + "InventoryTreatmentMethod", + "InventoryType", + "InventoryUploadCreatedResponse", + "InventoryUploadFormat", + "InventoryUploadSpec", + "InventoryUploadSpecHeaders", + "JobError", + "JobProgress", + "JobResourceUsage", + "JobStatus", + "Key", + "LandfireCanopyFuelBand", + "LandfireCanopyVersion", + "LandfireFbfm13Version", + "LandfireFbfm40Version", + "LandfireFccsVersion", + "LandfireTopographyVersion", + "LandscapeExportAlignmentDomainTarget", + "LandscapeExportAlignmentGridTarget", + "LandscapeExportRequest", + "LandscapeExportRequestFireBehaviorFuelModel", + "LandscapeFieldSource", + "LayersetCrs", + "LayersetCrsProperties", + "LayersetFeature", + "LayersetProperties", + "LineString", + "ListApplicationsResponse", + "ListDomainsResponse", + "ListExportsResponse", + "ListFeaturesResponse", + "ListGridsResponse", + "ListInventoriesResponse", + "ListKeysResponse", + "ListPointCloudsResponse", + "MaxCrownRadiusUnit", + "MetaCHMVersion", + "Modifier", + "MoistureModel", + "MultiLineString", + "MultiPoint", + "MultiPolygon", + "NonBurnableFuelModel", + "Operator", + "OverlapMethod", + "Point", + "PointCloud", + "PointCloudGeoreference", + "PointCloudSortField", + "PointCloudSource", + "PointCloudSummary", + "PointCloudThreeDepCoverageResponse", + "PointCloudType", + "PointCloudUploadCreatedResponse", + "PointCloudUploadSpec", + "PointCloudUploadSpecHeaders", + "PointProcess", + "Polygon", + "QUICFireExportAlignmentDomainTarget", + "QUICFireExportAlignmentGridTarget", + "QuicfireExportRequest", + "QuicfireExportRequestMoistMerge", + "QuotaExceededDetail", + "Quotas", + "RemoveAction", + "ResamplingMethod", + "Resolution3D", + "Scope", + "SortOrder", + "SparseGridData", + "SpatialOperator", + "StemIsolationLmf", + "StemIsolationVwf", + "ThreeDepDatasetCoverage", + "ThreeDepResolution", + "TopographyBand", + "TopographyThreeDepCoverageResponse", + "TreeBand", + "TreeForestryMetrics", + "TreeMapBand", + "TreeMapVersion", + "UniformBand", + "UniformBandInput", + "UniformMoistureValue", + "UpdateApplicationRequest", + "UpdateDomainRequestBody", + "UpdateExportRequestBody", + "UpdateFeatureRequestBody", + "UpdateGridRequestBody", + "UpdateInventoryRequestBody", + "UpdatePointCloudRequestBody", + "UploadBandDefinition", + "Usage", + "UsageCount", + "UsageLifecycle", + "UsageStorage", + "UserMeResponse", + "UserMeResponseKind", + "ValidationError", +) diff --git a/fastfuels_sdk/v2/client_library/models/access.py b/fastfuels_sdk/v2/client_library/models/access.py new file mode 100644 index 0000000..7f7052c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/access.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class Access(str, Enum): + APPLICATION = "application" + PERSONAL = "personal" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/allometry_biomass_source.py b/fastfuels_sdk/v2/client_library/models/allometry_biomass_source.py new file mode 100644 index 0000000..e49b88d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/allometry_biomass_source.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..models.biomass_component import BiomassComponent +from ..models.biomass_equations import BiomassEquations +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.allometry_biomass_source_component_states import ( + AllometryBiomassSourceComponentStates, + ) + from ..models.fine_biomass_config import FineBiomassConfig + + +T = TypeVar("T", bound="AllometryBiomassSource") + + +@_attrs_define +class AllometryBiomassSource: + """Estimate biomass from allometric equations. + + Attributes: + type_ (Literal['allometry'] | Unset): Default: 'allometry'. + equations (BiomassEquations | Unset): Allometric equation families for estimating biomass components. + components (list[BiomassComponent] | Unset): + component_states (AllometryBiomassSourceComponentStates | Unset): Per-component live/dead biomass partition + fractions. + fine (FineBiomassConfig | None | Unset): + """ + + type_: Literal["allometry"] | Unset = "allometry" + equations: BiomassEquations | Unset = UNSET + components: list[BiomassComponent] | Unset = UNSET + component_states: AllometryBiomassSourceComponentStates | Unset = UNSET + fine: FineBiomassConfig | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.fine_biomass_config import FineBiomassConfig + + type_ = self.type_ + + equations: str | Unset = UNSET + if not isinstance(self.equations, Unset): + equations = self.equations.value + + components: list[str] | Unset = UNSET + if not isinstance(self.components, Unset): + components = [] + for components_item_data in self.components: + components_item = components_item_data.value + components.append(components_item) + + component_states: dict[str, Any] | Unset = UNSET + if not isinstance(self.component_states, Unset): + component_states = self.component_states.to_dict() + + fine: dict[str, Any] | None | Unset + if isinstance(self.fine, Unset): + fine = UNSET + elif isinstance(self.fine, FineBiomassConfig): + fine = self.fine.to_dict() + else: + fine = self.fine + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if type_ is not UNSET: + field_dict["type"] = type_ + if equations is not UNSET: + field_dict["equations"] = equations + if components is not UNSET: + field_dict["components"] = components + if component_states is not UNSET: + field_dict["component_states"] = component_states + if fine is not UNSET: + field_dict["fine"] = fine + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.allometry_biomass_source_component_states import ( + AllometryBiomassSourceComponentStates, + ) + from ..models.fine_biomass_config import FineBiomassConfig + + d = dict(src_dict) + type_ = cast(Literal["allometry"] | Unset, d.pop("type", UNSET)) + if type_ != "allometry" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'allometry', got '{type_}'") + + _equations = d.pop("equations", UNSET) + equations: BiomassEquations | Unset + if isinstance(_equations, Unset): + equations = UNSET + else: + equations = BiomassEquations(_equations) + + _components = d.pop("components", UNSET) + components: list[BiomassComponent] | Unset = UNSET + if _components is not UNSET: + components = [] + for components_item_data in _components: + components_item = BiomassComponent(components_item_data) + + components.append(components_item) + + _component_states = d.pop("component_states", UNSET) + component_states: AllometryBiomassSourceComponentStates | Unset + if isinstance(_component_states, Unset): + component_states = UNSET + else: + component_states = AllometryBiomassSourceComponentStates.from_dict( + _component_states + ) + + def _parse_fine(data: object) -> FineBiomassConfig | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fine_type_0 = FineBiomassConfig.from_dict(data) + + return fine_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FineBiomassConfig | None | Unset, data) + + fine = _parse_fine(d.pop("fine", UNSET)) + + allometry_biomass_source = cls( + type_=type_, + equations=equations, + components=components, + component_states=component_states, + fine=fine, + ) + + return allometry_biomass_source diff --git a/fastfuels_sdk/v2/client_library/models/allometry_biomass_source_component_states.py b/fastfuels_sdk/v2/client_library/models/allometry_biomass_source_component_states.py new file mode 100644 index 0000000..56035b9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/allometry_biomass_source_component_states.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.biomass_component_state import BiomassComponentState + + +T = TypeVar("T", bound="AllometryBiomassSourceComponentStates") + + +@_attrs_define +class AllometryBiomassSourceComponentStates: + """Per-component live/dead biomass partition fractions.""" + + additional_properties: dict[str, BiomassComponentState] = _attrs_field( + init=False, factory=dict + ) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.biomass_component_state import BiomassComponentState + + d = dict(src_dict) + allometry_biomass_source_component_states = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = BiomassComponentState.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + allometry_biomass_source_component_states.additional_properties = ( + additional_properties + ) + return allometry_biomass_source_component_states + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> BiomassComponentState: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: BiomassComponentState) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/allometry_max_crown_radius_source.py b/fastfuels_sdk/v2/client_library/models/allometry_max_crown_radius_source.py new file mode 100644 index 0000000..5f6f606 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/allometry_max_crown_radius_source.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AllometryMaxCrownRadiusSource") + + +@_attrs_define +class AllometryMaxCrownRadiusSource: + """Use the crown profile model's allometric max crown radius (default). + + Attributes: + type_ (Literal['allometry'] | Unset): Default: 'allometry'. + """ + + type_: Literal["allometry"] | Unset = "allometry" + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if type_ is not UNSET: + field_dict["type"] = type_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["allometry"] | Unset, d.pop("type", UNSET)) + if type_ != "allometry" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'allometry', got '{type_}'") + + allometry_max_crown_radius_source = cls( + type_=type_, + ) + + return allometry_max_crown_radius_source diff --git a/fastfuels_sdk/v2/client_library/models/application.py b/fastfuels_sdk/v2/client_library/models/application.py new file mode 100644 index 0000000..f943647 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/application.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.application_quota_overrides_type_0 import ( + ApplicationQuotaOverridesType0, + ) + + +T = TypeVar("T", bound="Application") + + +@_attrs_define +class Application: + """Represents an application that can own API keys. + + Attributes: + id (str): Unique identifier for the application. + owner_id (str): The unique ID of the user who owns the application. + name (str): Name of the application. + description (None | str | Unset): Description of the application. + created_on (datetime.datetime | Unset): When the application was created. + modified_on (datetime.datetime | Unset): When the application was last modified. + tier (None | str | Unset): Quota tier for the application. Set by the FastFuels team. + quota_overrides (ApplicationQuotaOverridesType0 | None | Unset): Per-application quota overrides. Set by the + FastFuels team. + """ + + id: str + owner_id: str + name: str + description: None | str | Unset = UNSET + created_on: datetime.datetime | Unset = UNSET + modified_on: datetime.datetime | Unset = UNSET + tier: None | str | Unset = UNSET + quota_overrides: ApplicationQuotaOverridesType0 | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.application_quota_overrides_type_0 import ( + ApplicationQuotaOverridesType0, + ) + + id = self.id + + owner_id = self.owner_id + + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + created_on: str | Unset = UNSET + if not isinstance(self.created_on, Unset): + created_on = self.created_on.isoformat() + + modified_on: str | Unset = UNSET + if not isinstance(self.modified_on, Unset): + modified_on = self.modified_on.isoformat() + + tier: None | str | Unset + if isinstance(self.tier, Unset): + tier = UNSET + else: + tier = self.tier + + quota_overrides: dict[str, Any] | None | Unset + if isinstance(self.quota_overrides, Unset): + quota_overrides = UNSET + elif isinstance(self.quota_overrides, ApplicationQuotaOverridesType0): + quota_overrides = self.quota_overrides.to_dict() + else: + quota_overrides = self.quota_overrides + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "owner_id": owner_id, + "name": name, + } + ) + if description is not UNSET: + field_dict["description"] = description + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if tier is not UNSET: + field_dict["tier"] = tier + if quota_overrides is not UNSET: + field_dict["quota_overrides"] = quota_overrides + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.application_quota_overrides_type_0 import ( + ApplicationQuotaOverridesType0, + ) + + d = dict(src_dict) + id = d.pop("id") + + owner_id = d.pop("owner_id") + + name = d.pop("name") + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + _created_on = d.pop("created_on", UNSET) + created_on: datetime.datetime | Unset + if isinstance(_created_on, Unset): + created_on = UNSET + else: + created_on = datetime.datetime.fromisoformat(_created_on) + + _modified_on = d.pop("modified_on", UNSET) + modified_on: datetime.datetime | Unset + if isinstance(_modified_on, Unset): + modified_on = UNSET + else: + modified_on = datetime.datetime.fromisoformat(_modified_on) + + def _parse_tier(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + tier = _parse_tier(d.pop("tier", UNSET)) + + def _parse_quota_overrides( + data: object, + ) -> ApplicationQuotaOverridesType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + quota_overrides_type_0 = ApplicationQuotaOverridesType0.from_dict(data) + + return quota_overrides_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(ApplicationQuotaOverridesType0 | None | Unset, data) + + quota_overrides = _parse_quota_overrides(d.pop("quota_overrides", UNSET)) + + application = cls( + id=id, + owner_id=owner_id, + name=name, + description=description, + created_on=created_on, + modified_on=modified_on, + tier=tier, + quota_overrides=quota_overrides, + ) + + application.additional_properties = d + return application + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/application_quota_overrides_type_0.py b/fastfuels_sdk/v2/client_library/models/application_quota_overrides_type_0.py new file mode 100644 index 0000000..b52a1c3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/application_quota_overrides_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ApplicationQuotaOverridesType0") + + +@_attrs_define +class ApplicationQuotaOverridesType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + application_quota_overrides_type_0 = cls() + + application_quota_overrides_type_0.additional_properties = d + return application_quota_overrides_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/apply_grid_modifications_request.py b/fastfuels_sdk/v2/client_library/models/apply_grid_modifications_request.py new file mode 100644 index 0000000..8aafbfe --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/apply_grid_modifications_request.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="ApplyGridModificationsRequest") + + +@_attrs_define +class ApplyGridModificationsRequest: + """Request body for applying modifications to a grid in place. + + Metadata (name, description, tags) is not accepted here — the grid keeps + its identity; use PATCH to edit metadata. + + Attributes: + modifications (list[GridModification]): Modifications to append to this grid and apply to its data. + """ + + modifications: list[GridModification] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "modifications": modifications, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_modification import GridModification + + d = dict(src_dict) + modifications = [] + _modifications = d.pop("modifications") + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + apply_grid_modifications_request = cls( + modifications=modifications, + ) + + apply_grid_modifications_request.additional_properties = d + return apply_grid_modifications_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/apply_modifications_request.py b/fastfuels_sdk/v2/client_library/models/apply_modifications_request.py new file mode 100644 index 0000000..815f15b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/apply_modifications_request.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_modification import InventoryModification + + +T = TypeVar("T", bound="ApplyModificationsRequest") + + +@_attrs_define +class ApplyModificationsRequest: + """Request body for applying modifications to an inventory in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Attributes: + modifications (list[InventoryModification]): Modifications to append to this inventory and apply to its data. + """ + + modifications: list[InventoryModification] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "modifications": modifications, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_modification import InventoryModification + + d = dict(src_dict) + modifications = [] + _modifications = d.pop("modifications") + for modifications_item_data in _modifications: + modifications_item = InventoryModification.from_dict( + modifications_item_data + ) + + modifications.append(modifications_item) + + apply_modifications_request = cls( + modifications=modifications, + ) + + apply_modifications_request.additional_properties = d + return apply_modifications_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/apply_treatments_request.py b/fastfuels_sdk/v2/client_library/models/apply_treatments_request.py new file mode 100644 index 0000000..5b8b331 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/apply_treatments_request.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + + +T = TypeVar("T", bound="ApplyTreatmentsRequest") + + +@_attrs_define +class ApplyTreatmentsRequest: + """Request body for applying treatments to an inventory in place. + + Metadata (name, description, tags) is not accepted here — the inventory + keeps its identity; use PATCH to edit metadata. + + Attributes: + treatments (list[InventoryBasalAreaTreatment | InventoryDiameterTreatment]): Treatments to append to this + inventory and apply to its data. + """ + + treatments: list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + + treatments = [] + for treatments_item_data in self.treatments: + treatments_item: dict[str, Any] + if isinstance(treatments_item_data, InventoryDiameterTreatment): + treatments_item = treatments_item_data.to_dict() + else: + treatments_item = treatments_item_data.to_dict() + + treatments.append(treatments_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "treatments": treatments, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + + d = dict(src_dict) + treatments = [] + _treatments = d.pop("treatments") + for treatments_item_data in _treatments: + + def _parse_treatments_item( + data: object, + ) -> InventoryBasalAreaTreatment | InventoryDiameterTreatment: + try: + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_0 = InventoryDiameterTreatment.from_dict(data) + + return treatments_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_1 = InventoryBasalAreaTreatment.from_dict(data) + + return treatments_item_type_1 + + treatments_item = _parse_treatments_item(treatments_item_data) + + treatments.append(treatments_item) + + apply_treatments_request = cls( + treatments=treatments, + ) + + apply_treatments_request.additional_properties = d + return apply_treatments_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/band.py b/fastfuels_sdk/v2/client_library/models/band.py new file mode 100644 index 0000000..38d7349 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/band.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.band_type import BandType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.categorical_band_summary import CategoricalBandSummary + from ..models.continuous_band_summary import ContinuousBandSummary + + +T = TypeVar("T", bound="Band") + + +@_attrs_define +class Band: + """A single band in a grid. + + Attributes: + key (str): Dot-notation key (e.g., 'fuel_load.1hr') + type_ (BandType): Type of band data. + index (int): + name (None | str | Unset): Human-readable display name for the band (e.g. 'TreeMap ID', '1-hour Fuel Load'). + description (None | str | Unset): Longer-form description of what the band represents and how to interpret its + values. + unit (None | str | Unset): Physical unit of the band's pixel values, in UDUNITS-2-conformant ASCII form with + `**` for exponents (e.g. `kg/m**3`, `1/m`, `%`). `None` for categorical/identifier bands. See docs/units.md. + nodata (float | int | None | Unset): Value marking missing pixels in this band; pixels equal to it carry no data + and should be excluded from analysis. `null` when the band has no missing pixels, or when they are represented + as floating-point NaN. + summary (CategoricalBandSummary | ContinuousBandSummary | None | Unset): + """ + + key: str + type_: BandType + index: int + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + unit: None | str | Unset = UNSET + nodata: float | int | None | Unset = UNSET + summary: CategoricalBandSummary | ContinuousBandSummary | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.categorical_band_summary import CategoricalBandSummary + from ..models.continuous_band_summary import ContinuousBandSummary + + key = self.key + + type_ = self.type_.value + + index = self.index + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + nodata: float | int | None | Unset + if isinstance(self.nodata, Unset): + nodata = UNSET + else: + nodata = self.nodata + + summary: dict[str, Any] | None | Unset + if isinstance(self.summary, Unset): + summary = UNSET + elif isinstance(self.summary, ContinuousBandSummary) or isinstance( + self.summary, CategoricalBandSummary + ): + summary = self.summary.to_dict() + else: + summary = self.summary + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "key": key, + "type": type_, + "index": index, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if unit is not UNSET: + field_dict["unit"] = unit + if nodata is not UNSET: + field_dict["nodata"] = nodata + if summary is not UNSET: + field_dict["summary"] = summary + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.categorical_band_summary import CategoricalBandSummary + from ..models.continuous_band_summary import ContinuousBandSummary + + d = dict(src_dict) + key = d.pop("key") + + type_ = BandType(d.pop("type")) + + index = d.pop("index") + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + def _parse_nodata(data: object) -> float | int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | int | None | Unset, data) + + nodata = _parse_nodata(d.pop("nodata", UNSET)) + + def _parse_summary( + data: object, + ) -> CategoricalBandSummary | ContinuousBandSummary | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + summary_type_0_type_0 = ContinuousBandSummary.from_dict(data) + + return summary_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + summary_type_0_type_1 = CategoricalBandSummary.from_dict(data) + + return summary_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + CategoricalBandSummary | ContinuousBandSummary | None | Unset, data + ) + + summary = _parse_summary(d.pop("summary", UNSET)) + + band = cls( + key=key, + type_=type_, + index=index, + name=name, + description=description, + unit=unit, + nodata=nodata, + summary=summary, + ) + + band.additional_properties = d + return band + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/band_type.py b/fastfuels_sdk/v2/client_library/models/band_type.py new file mode 100644 index 0000000..1282591 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/band_type.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class BandType(str, Enum): + CATEGORICAL = "categorical" + CONTINUOUS = "continuous" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/base_model.py b/fastfuels_sdk/v2/client_library/models/base_model.py new file mode 100644 index 0000000..37e831d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/base_model.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="BaseModel") + + +@_attrs_define +class BaseModel: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + base_model = cls() + + base_model.additional_properties = d + return base_model + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/biomass_component.py b/fastfuels_sdk/v2/client_library/models/biomass_component.py new file mode 100644 index 0000000..ce509ef --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/biomass_component.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class BiomassComponent(str, Enum): + BRANCHWOOD = "branchwood" + FINE = "fine" + FOLIAGE = "foliage" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/biomass_component_state.py b/fastfuels_sdk/v2/client_library/models/biomass_component_state.py new file mode 100644 index 0000000..6b77593 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/biomass_component_state.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BiomassComponentState") + + +@_attrs_define +class BiomassComponentState: + """Live/dead partition for one biomass component. + + Attributes: + live (float | Unset): Default: 1.0. + dead (float | Unset): Default: 0.0. + """ + + live: float | Unset = 1.0 + dead: float | Unset = 0.0 + + def to_dict(self) -> dict[str, Any]: + live = self.live + + dead = self.dead + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if live is not UNSET: + field_dict["live"] = live + if dead is not UNSET: + field_dict["dead"] = dead + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + live = d.pop("live", UNSET) + + dead = d.pop("dead", UNSET) + + biomass_component_state = cls( + live=live, + dead=dead, + ) + + return biomass_component_state diff --git a/fastfuels_sdk/v2/client_library/models/biomass_equations.py b/fastfuels_sdk/v2/client_library/models/biomass_equations.py new file mode 100644 index 0000000..b42fe29 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/biomass_equations.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class BiomassEquations(str, Enum): + JENKINS = "jenkins" + NSVB = "nsvb" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/biomass_unit.py b/fastfuels_sdk/v2/client_library/models/biomass_unit.py new file mode 100644 index 0000000..f271950 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/biomass_unit.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class BiomassUnit(str, Enum): + KG = "kg" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/categorical_band_summary.py b/fastfuels_sdk/v2/client_library/models/categorical_band_summary.py new file mode 100644 index 0000000..75c397a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/categorical_band_summary.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="CategoricalBandSummary") + + +@_attrs_define +class CategoricalBandSummary: + """ + Attributes: + type_ (Literal['categorical']): + count (int): + nodata_count (int): + unique_count (int): + """ + + type_: Literal["categorical"] + count: int + nodata_count: int + unique_count: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + count = self.count + + nodata_count = self.nodata_count + + unique_count = self.unique_count + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "count": count, + "nodata_count": nodata_count, + "unique_count": unique_count, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["categorical"], d.pop("type")) + if type_ != "categorical": + raise ValueError(f"type must match const 'categorical', got '{type_}'") + + count = d.pop("count") + + nodata_count = d.pop("nodata_count") + + unique_count = d.pop("unique_count") + + categorical_band_summary = cls( + type_=type_, + count=count, + nodata_count=nodata_count, + unique_count=unique_count, + ) + + categorical_band_summary.additional_properties = d + return categorical_band_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/categorical_column_summary.py b/fastfuels_sdk/v2/client_library/models/categorical_column_summary.py new file mode 100644 index 0000000..be09b27 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/categorical_column_summary.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="CategoricalColumnSummary") + + +@_attrs_define +class CategoricalColumnSummary: + """ + Attributes: + type_ (Literal['categorical']): + count (int): + null_count (int): + unique_count (int): + """ + + type_: Literal["categorical"] + count: int + null_count: int + unique_count: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + count = self.count + + null_count = self.null_count + + unique_count = self.unique_count + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "count": count, + "null_count": null_count, + "unique_count": unique_count, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["categorical"], d.pop("type")) + if type_ != "categorical": + raise ValueError(f"type must match const 'categorical', got '{type_}'") + + count = d.pop("count") + + null_count = d.pop("null_count") + + unique_count = d.pop("unique_count") + + categorical_column_summary = cls( + type_=type_, + count=count, + null_count=null_count, + unique_count=unique_count, + ) + + categorical_column_summary.additional_properties = d + return categorical_column_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/chunks.py b/fastfuels_sdk/v2/client_library/models/chunks.py new file mode 100644 index 0000000..6303419 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/chunks.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.chunks_count_by_axis_type_0 import ChunksCountByAxisType0 + + +T = TypeVar("T", bound="Chunks") + + +@_attrs_define +class Chunks: + """Chunk layout for a grid. + + Attributes: + shape (list[int]): Size of a single chunk. 2D grids: (y, x). 3D grids: (z, y, x). Edge chunks may be smaller. + count (int | None | Unset): Total number of chunks in the grid. + count_by_axis (ChunksCountByAxisType0 | None | Unset): Number of chunks along each axis. Keys are 'y','x' for 2D + grids and 'z','y','x' for 3D grids. + """ + + shape: list[int] + count: int | None | Unset = UNSET + count_by_axis: ChunksCountByAxisType0 | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.chunks_count_by_axis_type_0 import ChunksCountByAxisType0 + + shape: list[int] + if isinstance(self.shape, list): + shape = [] + for shape_type_0_item_data in self.shape: + shape_type_0_item: int + shape_type_0_item = shape_type_0_item_data + shape.append(shape_type_0_item) + + count: int | None | Unset + if isinstance(self.count, Unset): + count = UNSET + else: + count = self.count + + count_by_axis: dict[str, Any] | None | Unset + if isinstance(self.count_by_axis, Unset): + count_by_axis = UNSET + elif isinstance(self.count_by_axis, ChunksCountByAxisType0): + count_by_axis = self.count_by_axis.to_dict() + else: + count_by_axis = self.count_by_axis + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "shape": shape, + } + ) + if count is not UNSET: + field_dict["count"] = count + if count_by_axis is not UNSET: + field_dict["count_by_axis"] = count_by_axis + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.chunks_count_by_axis_type_0 import ChunksCountByAxisType0 + + d = dict(src_dict) + + def _parse_shape(data: object) -> list[int]: + if not isinstance(data, list): + raise TypeError() + shape_type_0 = [] + _shape_type_0 = data + for shape_type_0_item_data in _shape_type_0: + + def _parse_shape_type_0_item(data: object) -> int: + return cast(int, data) + + shape_type_0_item = _parse_shape_type_0_item(shape_type_0_item_data) + + shape_type_0.append(shape_type_0_item) + + return shape_type_0 + + shape = _parse_shape(d.pop("shape")) + + def _parse_count(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + count = _parse_count(d.pop("count", UNSET)) + + def _parse_count_by_axis(data: object) -> ChunksCountByAxisType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + count_by_axis_type_0 = ChunksCountByAxisType0.from_dict(data) + + return count_by_axis_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(ChunksCountByAxisType0 | None | Unset, data) + + count_by_axis = _parse_count_by_axis(d.pop("count_by_axis", UNSET)) + + chunks = cls( + shape=shape, + count=count, + count_by_axis=count_by_axis, + ) + + chunks.additional_properties = d + return chunks + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/chunks_count_by_axis_type_0.py b/fastfuels_sdk/v2/client_library/models/chunks_count_by_axis_type_0.py new file mode 100644 index 0000000..2f2a6b5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/chunks_count_by_axis_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ChunksCountByAxisType0") + + +@_attrs_define +class ChunksCountByAxisType0: + """ """ + + additional_properties: dict[str, int] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + chunks_count_by_axis_type_0 = cls() + + chunks_count_by_axis_type_0.additional_properties = d + return chunks_count_by_axis_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> int: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: int) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/column.py b/fastfuels_sdk/v2/client_library/models/column.py new file mode 100644 index 0000000..ba34ff5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/column.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.column_type import ColumnType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.categorical_column_summary import CategoricalColumnSummary + from ..models.continuous_column_summary import ContinuousColumnSummary + + +T = TypeVar("T", bound="Column") + + +@_attrs_define +class Column: + """A single column in an inventory. + + Attributes: + key (str): Column name (e.g., 'dbh', 'fia_species_code') + type_ (ColumnType): Type of column data. + unit (None | str | Unset): + summary (CategoricalColumnSummary | ContinuousColumnSummary | None | Unset): + """ + + key: str + type_: ColumnType + unit: None | str | Unset = UNSET + summary: CategoricalColumnSummary | ContinuousColumnSummary | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.categorical_column_summary import CategoricalColumnSummary + from ..models.continuous_column_summary import ContinuousColumnSummary + + key = self.key + + type_ = self.type_.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + summary: dict[str, Any] | None | Unset + if isinstance(self.summary, Unset): + summary = UNSET + elif isinstance(self.summary, ContinuousColumnSummary) or isinstance( + self.summary, CategoricalColumnSummary + ): + summary = self.summary.to_dict() + else: + summary = self.summary + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "key": key, + "type": type_, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + if summary is not UNSET: + field_dict["summary"] = summary + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.categorical_column_summary import CategoricalColumnSummary + from ..models.continuous_column_summary import ContinuousColumnSummary + + d = dict(src_dict) + key = d.pop("key") + + type_ = ColumnType(d.pop("type")) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + def _parse_summary( + data: object, + ) -> CategoricalColumnSummary | ContinuousColumnSummary | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + summary_type_0_type_0 = ContinuousColumnSummary.from_dict(data) + + return summary_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + summary_type_0_type_1 = CategoricalColumnSummary.from_dict(data) + + return summary_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + CategoricalColumnSummary | ContinuousColumnSummary | None | Unset, data + ) + + summary = _parse_summary(d.pop("summary", UNSET)) + + column = cls( + key=key, + type_=type_, + unit=unit, + summary=summary, + ) + + column.additional_properties = d + return column + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/column_type.py b/fastfuels_sdk/v2/client_library/models/column_type.py new file mode 100644 index 0000000..fc1ea26 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/column_type.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ColumnType(str, Enum): + CATEGORICAL = "categorical" + CONTINUOUS = "continuous" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/compose_attribute_condition.py b/fastfuels_sdk/v2/client_library/models/compose_attribute_condition.py new file mode 100644 index 0000000..455dffe --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_attribute_condition.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.compose_comparison_operator import ComposeComparisonOperator + +T = TypeVar("T", bound="ComposeAttributeCondition") + + +@_attrs_define +class ComposeAttributeCondition: + """Attribute condition using an alias-qualified input band reference. + + Attributes: + band (str): Alias-qualified band ref, e.g. `a.fbfm`. + operator (ComposeComparisonOperator): Comparison operators for compose attribute conditions. + value (float | int | list[float | int | str] | str): + """ + + band: str + operator: ComposeComparisonOperator + value: float | int | list[float | int | str] | str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + band = self.band + + operator = self.operator.value + + value: float | int | list[float | int | str] | str + if isinstance(self.value, list): + value = [] + for value_type_3_item_data in self.value: + value_type_3_item: float | int | str + value_type_3_item = value_type_3_item_data + value.append(value_type_3_item) + + else: + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "band": band, + "operator": operator, + "value": value, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + band = d.pop("band") + + operator = ComposeComparisonOperator(d.pop("operator")) + + def _parse_value(data: object) -> float | int | list[float | int | str] | str: + try: + if not isinstance(data, list): + raise TypeError() + value_type_3 = [] + _value_type_3 = data + for value_type_3_item_data in _value_type_3: + + def _parse_value_type_3_item(data: object) -> float | int | str: + return cast(float | int | str, data) + + value_type_3_item = _parse_value_type_3_item(value_type_3_item_data) + + value_type_3.append(value_type_3_item) + + return value_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(float | int | list[float | int | str] | str, data) + + value = _parse_value(d.pop("value")) + + compose_attribute_condition = cls( + band=band, + operator=operator, + value=value, + ) + + compose_attribute_condition.additional_properties = d + return compose_attribute_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/compose_comparison_operator.py b/fastfuels_sdk/v2/client_library/models/compose_comparison_operator.py new file mode 100644 index 0000000..0d2e30c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_comparison_operator.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class ComposeComparisonOperator(str, Enum): + EQ = "eq" + GE = "ge" + GT = "gt" + IN = "in" + LE = "le" + LT = "lt" + NE = "ne" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/compose_compute.py b/fastfuels_sdk/v2/client_library/models/compose_compute.py new file mode 100644 index 0000000..1637adb --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_compute.py @@ -0,0 +1,354 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.compose_operator import ComposeOperator +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import GridGeometrySpatialCondition + from ..models.inline_compute import InlineCompute + + +T = TypeVar("T", bound="ComposeCompute") + + +@_attrs_define +class ComposeCompute: + """Compute an output band from one or more operands. + + The output band is always continuous and its unit is derived from the + operands (the product/quotient for `multiply`/`divide`, the operand unit + otherwise). Supply `unit` only to express the result in a different but + dimensionally compatible unit; the worker converts to it. + + Attributes: + operator (ComposeOperator): Operators available for compose computations. + operands (list[ComposeLiteral | float | int | str]): + output (str): Output band key, e.g. `fuel_load.1hr`. + name (None | str | Unset): Optional display name for the output band. + description (None | str | Unset): Optional description for the output band. + unit (None | str | Unset): Optional canonical output unit. Defaults to the unit derived from the operands; if + given it must be dimensionally compatible. + conditions (list[ComposeAttributeCondition | GridFeatureSpatialCondition | GridGeometrySpatialCondition] | None + | Unset): + else_ (ComposeLiteral | float | InlineCompute | int | None | str | Unset): + """ + + operator: ComposeOperator + operands: list[ComposeLiteral | float | int | str] + output: str + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + unit: None | str | Unset = UNSET + conditions: ( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset + ) = UNSET + else_: ComposeLiteral | float | InlineCompute | int | None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.inline_compute import InlineCompute + + operator = self.operator.value + + operands = [] + for operands_item_data in self.operands: + operands_item: dict[str, Any] | float | int | str + if isinstance(operands_item_data, ComposeLiteral): + operands_item = operands_item_data.to_dict() + else: + operands_item = operands_item_data + operands.append(operands_item) + + output = self.output + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + conditions: list[dict[str, Any]] | None | Unset + if isinstance(self.conditions, Unset): + conditions = UNSET + elif isinstance(self.conditions, list): + conditions = [] + for conditions_type_0_item_data in self.conditions: + conditions_type_0_item: dict[str, Any] + if isinstance( + conditions_type_0_item_data, ComposeAttributeCondition + ) or isinstance( + conditions_type_0_item_data, GridGeometrySpatialCondition + ): + conditions_type_0_item = conditions_type_0_item_data.to_dict() + else: + conditions_type_0_item = conditions_type_0_item_data.to_dict() + + conditions.append(conditions_type_0_item) + + else: + conditions = self.conditions + + else_: dict[str, Any] | float | int | None | str | Unset + if isinstance(self.else_, Unset): + else_ = UNSET + elif isinstance(self.else_, ComposeLiteral) or isinstance( + self.else_, InlineCompute + ): + else_ = self.else_.to_dict() + else: + else_ = self.else_ + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "operator": operator, + "operands": operands, + "output": output, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if unit is not UNSET: + field_dict["unit"] = unit + if conditions is not UNSET: + field_dict["conditions"] = conditions + if else_ is not UNSET: + field_dict["else"] = else_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.inline_compute import InlineCompute + + d = dict(src_dict) + operator = ComposeOperator(d.pop("operator")) + + operands = [] + _operands = d.pop("operands") + for operands_item_data in _operands: + + def _parse_operands_item( + data: object, + ) -> ComposeLiteral | float | int | str: + try: + if not isinstance(data, dict): + raise TypeError() + operands_item_type_3 = ComposeLiteral.from_dict(data) + + return operands_item_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(ComposeLiteral | float | int | str, data) + + operands_item = _parse_operands_item(operands_item_data) + + operands.append(operands_item) + + output = d.pop("output") + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + def _parse_conditions( + data: object, + ) -> ( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + conditions_type_0 = [] + _conditions_type_0 = data + for conditions_type_0_item_data in _conditions_type_0: + + def _parse_conditions_type_0_item( + data: object, + ) -> ( + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_0 = ( + ComposeAttributeCondition.from_dict(data) + ) + + return conditions_type_0_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_1 = ( + GridGeometrySpatialCondition.from_dict(data) + ) + + return conditions_type_0_item_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_2 = ( + GridFeatureSpatialCondition.from_dict(data) + ) + + return conditions_type_0_item_type_2 + + conditions_type_0_item = _parse_conditions_type_0_item( + conditions_type_0_item_data + ) + + conditions_type_0.append(conditions_type_0_item) + + return conditions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset, + data, + ) + + conditions = _parse_conditions(d.pop("conditions", UNSET)) + + def _parse_else_( + data: object, + ) -> ComposeLiteral | float | InlineCompute | int | None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + else_type_3 = ComposeLiteral.from_dict(data) + + return else_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + else_type_4 = InlineCompute.from_dict(data) + + return else_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + ComposeLiteral | float | InlineCompute | int | None | str | Unset, data + ) + + else_ = _parse_else_(d.pop("else", UNSET)) + + compose_compute = cls( + operator=operator, + operands=operands, + output=output, + name=name, + description=description, + unit=unit, + conditions=conditions, + else_=else_, + ) + + compose_compute.additional_properties = d + return compose_compute + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/compose_input.py b/fastfuels_sdk/v2/client_library/models/compose_input.py new file mode 100644 index 0000000..710f56e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_input.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ComposeInput") + + +@_attrs_define +class ComposeInput: + """A source grid participating in a compose request. + + Attributes: + grid_id (str): + alias (str): Short alias used to reference bands, e.g. `a.fuel_load.1hr`. + """ + + grid_id: str + alias: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + grid_id = self.grid_id + + alias = self.alias + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "grid_id": grid_id, + "alias": alias, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_id = d.pop("grid_id") + + alias = d.pop("alias") + + compose_input = cls( + grid_id=grid_id, + alias=alias, + ) + + compose_input.additional_properties = d + return compose_input + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/compose_literal.py b/fastfuels_sdk/v2/client_library/models/compose_literal.py new file mode 100644 index 0000000..2f5de45 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_literal.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ComposeLiteral") + + +@_attrs_define +class ComposeLiteral: + """Typed literal value for compose operands and fallback values. + + Attributes: + value (float | int | str): + type_ (Literal['literal'] | Unset): Default: 'literal'. + unit (None | str | Unset): Canonical unit for numeric values. Must be null for string literals. + """ + + value: float | int | str + type_: Literal["literal"] | Unset = "literal" + unit: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + value: float | int | str + value = self.value + + type_ = self.type_ + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "value": value, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_value(data: object) -> float | int | str: + return cast(float | int | str, data) + + value = _parse_value(d.pop("value")) + + type_ = cast(Literal["literal"] | Unset, d.pop("type", UNSET)) + if type_ != "literal" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'literal', got '{type_}'") + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + compose_literal = cls( + value=value, + type_=type_, + unit=unit, + ) + + compose_literal.additional_properties = d + return compose_literal + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/compose_operator.py b/fastfuels_sdk/v2/client_library/models/compose_operator.py new file mode 100644 index 0000000..eab996d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_operator.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class ComposeOperator(str, Enum): + ADD = "add" + AVERAGE = "average" + DIVIDE = "divide" + MAX = "max" + MIN = "min" + MULTIPLY = "multiply" + SUBTRACT = "subtract" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/compose_select.py b/fastfuels_sdk/v2/client_library/models/compose_select.py new file mode 100644 index 0000000..ef2e9c3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/compose_select.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import GridGeometrySpatialCondition + from ..models.inline_compute import InlineCompute + + +T = TypeVar("T", bound="ComposeSelect") + + +@_attrs_define +class ComposeSelect: + """Select one input band into an output band, optionally conditionally. + + The output band's type and unit are inherited from the selected source + band; only the human-readable `name`/`description` are optional overrides. + + Attributes: + output (str): Output band key, e.g. `fuel_load.1hr`. + from_ (str): Alias-qualified source band ref. + name (None | str | Unset): Optional display name for the output band. + description (None | str | Unset): Optional description for the output band. + conditions (list[ComposeAttributeCondition | GridFeatureSpatialCondition | GridGeometrySpatialCondition] | None + | Unset): + else_ (ComposeLiteral | float | InlineCompute | int | None | str | Unset): + """ + + output: str + from_: str + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + conditions: ( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset + ) = UNSET + else_: ComposeLiteral | float | InlineCompute | int | None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.inline_compute import InlineCompute + + output = self.output + + from_ = self.from_ + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + conditions: list[dict[str, Any]] | None | Unset + if isinstance(self.conditions, Unset): + conditions = UNSET + elif isinstance(self.conditions, list): + conditions = [] + for conditions_type_0_item_data in self.conditions: + conditions_type_0_item: dict[str, Any] + if isinstance( + conditions_type_0_item_data, ComposeAttributeCondition + ) or isinstance( + conditions_type_0_item_data, GridGeometrySpatialCondition + ): + conditions_type_0_item = conditions_type_0_item_data.to_dict() + else: + conditions_type_0_item = conditions_type_0_item_data.to_dict() + + conditions.append(conditions_type_0_item) + + else: + conditions = self.conditions + + else_: dict[str, Any] | float | int | None | str | Unset + if isinstance(self.else_, Unset): + else_ = UNSET + elif isinstance(self.else_, ComposeLiteral) or isinstance( + self.else_, InlineCompute + ): + else_ = self.else_.to_dict() + else: + else_ = self.else_ + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "output": output, + "from": from_, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if conditions is not UNSET: + field_dict["conditions"] = conditions + if else_ is not UNSET: + field_dict["else"] = else_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.compose_attribute_condition import ComposeAttributeCondition + from ..models.compose_literal import ComposeLiteral + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.inline_compute import InlineCompute + + d = dict(src_dict) + output = d.pop("output") + + from_ = d.pop("from") + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_conditions( + data: object, + ) -> ( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + conditions_type_0 = [] + _conditions_type_0 = data + for conditions_type_0_item_data in _conditions_type_0: + + def _parse_conditions_type_0_item( + data: object, + ) -> ( + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_0 = ( + ComposeAttributeCondition.from_dict(data) + ) + + return conditions_type_0_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_1 = ( + GridGeometrySpatialCondition.from_dict(data) + ) + + return conditions_type_0_item_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_type_0_item_type_2 = ( + GridFeatureSpatialCondition.from_dict(data) + ) + + return conditions_type_0_item_type_2 + + conditions_type_0_item = _parse_conditions_type_0_item( + conditions_type_0_item_data + ) + + conditions_type_0.append(conditions_type_0_item) + + return conditions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + list[ + ComposeAttributeCondition + | GridFeatureSpatialCondition + | GridGeometrySpatialCondition + ] + | None + | Unset, + data, + ) + + conditions = _parse_conditions(d.pop("conditions", UNSET)) + + def _parse_else_( + data: object, + ) -> ComposeLiteral | float | InlineCompute | int | None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + else_type_3 = ComposeLiteral.from_dict(data) + + return else_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + else_type_4 = InlineCompute.from_dict(data) + + return else_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + ComposeLiteral | float | InlineCompute | int | None | str | Unset, data + ) + + else_ = _parse_else_(d.pop("else", UNSET)) + + compose_select = cls( + output=output, + from_=from_, + name=name, + description=description, + conditions=conditions, + else_=else_, + ) + + compose_select.additional_properties = d + return compose_select + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/continuous_band_summary.py b/fastfuels_sdk/v2/client_library/models/continuous_band_summary.py new file mode 100644 index 0000000..1ac10e7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/continuous_band_summary.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ContinuousBandSummary") + + +@_attrs_define +class ContinuousBandSummary: + """ + Attributes: + type_ (Literal['continuous']): + count (int): + nodata_count (int): + min_ (float | None): + max_ (float | None): + mean (float | None): + std (float | None): + """ + + type_: Literal["continuous"] + count: int + nodata_count: int + min_: float | None + max_: float | None + mean: float | None + std: float | None + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + count = self.count + + nodata_count = self.nodata_count + + min_: float | None + min_ = self.min_ + + max_: float | None + max_ = self.max_ + + mean: float | None + mean = self.mean + + std: float | None + std = self.std + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "count": count, + "nodata_count": nodata_count, + "min": min_, + "max": max_, + "mean": mean, + "std": std, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["continuous"], d.pop("type")) + if type_ != "continuous": + raise ValueError(f"type must match const 'continuous', got '{type_}'") + + count = d.pop("count") + + nodata_count = d.pop("nodata_count") + + def _parse_min_(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + min_ = _parse_min_(d.pop("min")) + + def _parse_max_(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + max_ = _parse_max_(d.pop("max")) + + def _parse_mean(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + mean = _parse_mean(d.pop("mean")) + + def _parse_std(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + std = _parse_std(d.pop("std")) + + continuous_band_summary = cls( + type_=type_, + count=count, + nodata_count=nodata_count, + min_=min_, + max_=max_, + mean=mean, + std=std, + ) + + continuous_band_summary.additional_properties = d + return continuous_band_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/continuous_column_summary.py b/fastfuels_sdk/v2/client_library/models/continuous_column_summary.py new file mode 100644 index 0000000..d0f227e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/continuous_column_summary.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ContinuousColumnSummary") + + +@_attrs_define +class ContinuousColumnSummary: + """ + Attributes: + type_ (Literal['continuous']): + count (int): + null_count (int): + min_ (float | None): + max_ (float | None): + mean (float | None): + std (float | None): + """ + + type_: Literal["continuous"] + count: int + null_count: int + min_: float | None + max_: float | None + mean: float | None + std: float | None + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + count = self.count + + null_count = self.null_count + + min_: float | None + min_ = self.min_ + + max_: float | None + max_ = self.max_ + + mean: float | None + mean = self.mean + + std: float | None + std = self.std + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "count": count, + "null_count": null_count, + "min": min_, + "max": max_, + "mean": mean, + "std": std, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["continuous"], d.pop("type")) + if type_ != "continuous": + raise ValueError(f"type must match const 'continuous', got '{type_}'") + + count = d.pop("count") + + null_count = d.pop("null_count") + + def _parse_min_(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + min_ = _parse_min_(d.pop("min")) + + def _parse_max_(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + max_ = _parse_max_(d.pop("max")) + + def _parse_mean(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + mean = _parse_mean(d.pop("mean")) + + def _parse_std(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + std = _parse_std(d.pop("std")) + + continuous_column_summary = cls( + type_=type_, + count=count, + null_count=null_count, + min_=min_, + max_=max_, + mean=mean, + std=std, + ) + + continuous_column_summary.additional_properties = d + return continuous_column_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/count_usage.py b/fastfuels_sdk/v2/client_library/models/count_usage.py new file mode 100644 index 0000000..fad9e55 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/count_usage.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.usage_count import UsageCount + + +T = TypeVar("T", bound="CountUsage") + + +@_attrs_define +class CountUsage: + """Usage for a count-only resource type (domains, applications, API keys). + + Attributes: + total (UsageCount): A count-based usage/limit pair (resources or concurrent jobs). + """ + + total: UsageCount + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + total = self.total.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "total": total, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.usage_count import UsageCount + + d = dict(src_dict) + total = UsageCount.from_dict(d.pop("total")) + + count_usage = cls( + total=total, + ) + + count_usage.additional_properties = d + return count_usage + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_application_request.py b/fastfuels_sdk/v2/client_library/models/create_application_request.py new file mode 100644 index 0000000..9cc32b7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_application_request.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateApplicationRequest") + + +@_attrs_define +class CreateApplicationRequest: + """Request body for creating an application. + + Attributes: + name (str): Name of the application. + description (None | str | Unset): Description of the application. + """ + + name: str + description: None | str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "name": name, + } + ) + if description is not UNSET: + field_dict["description"] = description + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name") + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + create_application_request = cls( + name=name, + description=description, + ) + + return create_application_request diff --git a/fastfuels_sdk/v2/client_library/models/create_chm_inventory_request.py b/fastfuels_sdk/v2/client_library/models/create_chm_inventory_request.py new file mode 100644 index 0000000..5797190 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_chm_inventory_request.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_type import InventoryType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_modification import InventoryModification + from ..models.stem_isolation_lmf import StemIsolationLmf + from ..models.stem_isolation_vwf import StemIsolationVwf + + +T = TypeVar("T", bound="CreateChmInventoryRequest") + + +@_attrs_define +class CreateChmInventoryRequest: + """Request body for creating an inventory via CHM extraction. + + Attributes: + source_chm_grid_id (str): ID of a completed CHM grid to use as the source. + type_ (InventoryType | Unset): Type of entities in the inventory. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + algorithm (StemIsolationLmf | StemIsolationVwf | Unset): Stem isolation algorithm and its parameters. + modifications (list[InventoryModification] | Unset): Modifications to apply after stem extraction. + treatments (list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset): Silvicultural treatments + thin against tree diameter, so they require a diameter (`dbh`) column. CHM stem isolation produces only height + and position (`x`, `y`, `height`), so treatments are not supported here and this must be empty. + """ + + source_chm_grid_id: str + type_: InventoryType | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + algorithm: StemIsolationLmf | StemIsolationVwf | Unset = UNSET + modifications: list[InventoryModification] | Unset = UNSET + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.stem_isolation_lmf import StemIsolationLmf + + source_chm_grid_id = self.source_chm_grid_id + + type_: str | Unset = UNSET + if not isinstance(self.type_, Unset): + type_ = self.type_.value + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + algorithm: dict[str, Any] | Unset + if isinstance(self.algorithm, Unset): + algorithm = UNSET + elif isinstance(self.algorithm, StemIsolationLmf): + algorithm = self.algorithm.to_dict() + else: + algorithm = self.algorithm.to_dict() + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + treatments: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.treatments, Unset): + treatments = [] + for treatments_item_data in self.treatments: + treatments_item: dict[str, Any] + if isinstance(treatments_item_data, InventoryDiameterTreatment): + treatments_item = treatments_item_data.to_dict() + else: + treatments_item = treatments_item_data.to_dict() + + treatments.append(treatments_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_chm_grid_id": source_chm_grid_id, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if algorithm is not UNSET: + field_dict["algorithm"] = algorithm + if modifications is not UNSET: + field_dict["modifications"] = modifications + if treatments is not UNSET: + field_dict["treatments"] = treatments + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_modification import InventoryModification + from ..models.stem_isolation_lmf import StemIsolationLmf + from ..models.stem_isolation_vwf import StemIsolationVwf + + d = dict(src_dict) + source_chm_grid_id = d.pop("source_chm_grid_id") + + _type_ = d.pop("type", UNSET) + type_: InventoryType | Unset + if isinstance(_type_, Unset): + type_ = UNSET + else: + type_ = InventoryType(_type_) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + def _parse_algorithm( + data: object, + ) -> StemIsolationLmf | StemIsolationVwf | Unset: + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + algorithm_type_0 = StemIsolationLmf.from_dict(data) + + return algorithm_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + algorithm_type_1 = StemIsolationVwf.from_dict(data) + + return algorithm_type_1 + + algorithm = _parse_algorithm(d.pop("algorithm", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[InventoryModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = InventoryModification.from_dict( + modifications_item_data + ) + + modifications.append(modifications_item) + + _treatments = d.pop("treatments", UNSET) + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + if _treatments is not UNSET: + treatments = [] + for treatments_item_data in _treatments: + + def _parse_treatments_item( + data: object, + ) -> InventoryBasalAreaTreatment | InventoryDiameterTreatment: + try: + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_0 = InventoryDiameterTreatment.from_dict( + data + ) + + return treatments_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_1 = InventoryBasalAreaTreatment.from_dict(data) + + return treatments_item_type_1 + + treatments_item = _parse_treatments_item(treatments_item_data) + + treatments.append(treatments_item) + + create_chm_inventory_request = cls( + source_chm_grid_id=source_chm_grid_id, + type_=type_, + name=name, + description=description, + tags=tags, + algorithm=algorithm, + modifications=modifications, + treatments=treatments, + ) + + create_chm_inventory_request.additional_properties = d + return create_chm_inventory_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_compose_request.py b/fastfuels_sdk/v2/client_library/models/create_compose_request.py new file mode 100644 index 0000000..9a1a476 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_compose_request.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.compose_compute import ComposeCompute + from ..models.compose_input import ComposeInput + from ..models.compose_select import ComposeSelect + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateComposeRequest") + + +@_attrs_define +class CreateComposeRequest: + """Request to create a grid by composing one or more existing grids. + + Attributes: + inputs (list[ComposeInput]): + select (list[ComposeSelect] | Unset): + compute (list[ComposeCompute] | Unset): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): + """ + + inputs: list[ComposeInput] + select: list[ComposeSelect] | Unset = UNSET + compute: list[ComposeCompute] | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + inputs = [] + for inputs_item_data in self.inputs: + inputs_item = inputs_item_data.to_dict() + inputs.append(inputs_item) + + select: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.select, Unset): + select = [] + for select_item_data in self.select: + select_item = select_item_data.to_dict() + select.append(select_item) + + compute: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.compute, Unset): + compute = [] + for compute_item_data in self.compute: + compute_item = compute_item_data.to_dict() + compute.append(compute_item) + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "inputs": inputs, + } + ) + if select is not UNSET: + field_dict["select"] = select + if compute is not UNSET: + field_dict["compute"] = compute + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.compose_compute import ComposeCompute + from ..models.compose_input import ComposeInput + from ..models.compose_select import ComposeSelect + from ..models.grid_modification import GridModification + + d = dict(src_dict) + inputs = [] + _inputs = d.pop("inputs") + for inputs_item_data in _inputs: + inputs_item = ComposeInput.from_dict(inputs_item_data) + + inputs.append(inputs_item) + + _select = d.pop("select", UNSET) + select: list[ComposeSelect] | Unset = UNSET + if _select is not UNSET: + select = [] + for select_item_data in _select: + select_item = ComposeSelect.from_dict(select_item_data) + + select.append(select_item) + + _compute = d.pop("compute", UNSET) + compute: list[ComposeCompute] | Unset = UNSET + if _compute is not UNSET: + compute = [] + for compute_item_data in _compute: + compute_item = ComposeCompute.from_dict(compute_item_data) + + compute.append(compute_item) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_compose_request = cls( + inputs=inputs, + select=select, + compute=compute, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_compose_request.additional_properties = d + return create_compose_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_duet_request.py b/fastfuels_sdk/v2/client_library/models/create_duet_request.py new file mode 100644 index 0000000..7ffef1f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_duet_request.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.duet_band import DuetBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.duet_calibration import DuetCalibration + + +T = TypeVar("T", bound="CreateDuetRequest") + + +@_attrs_define +class CreateDuetRequest: + """Request body for creating a DUET surface fuel grid from a tree grid. + + Does not extend CreateGridRequestBase: like the 3D grids it derives from, + DUET grids do not support modifications. + + Attributes: + years_since_burn (int): Years of litter accumulation to simulate. DUET begins the year of the last burn, when + standing grass and litter have been consumed, so this is the stand's time since fire. It is the highest-leverage + parameter in the model and also drives runtime. + source_grid_id (str): ID of a completed 3D tree grid carrying the `bulk_density.foliage.live`, `spcd`, and + `fuel_moisture.live` bands. + wind_direction (int | Unset): Prevailing wind direction in whole degrees clockwise from north. Default: 270. + wind_variability (int | Unset): Angular spread of wind direction, in whole degrees. Default: 30. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + bands (list[DuetBand] | Unset): Which output bands to produce. Defaults to `fuel_load.grass` and + `fuel_load.litter`. + calibration (DuetCalibration | None | Unset): Optional calibration targets. DUET supplies the spatial pattern of + surface fuels; its raw magnitudes are not physical. Without calibration the raw values are stored as-is. + """ + + years_since_burn: int + source_grid_id: str + wind_direction: int | Unset = 270 + wind_variability: int | Unset = 30 + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + bands: list[DuetBand] | Unset = UNSET + calibration: DuetCalibration | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.duet_calibration import DuetCalibration + + years_since_burn = self.years_since_burn + + source_grid_id = self.source_grid_id + + wind_direction = self.wind_direction + + wind_variability = self.wind_variability + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + calibration: dict[str, Any] | None | Unset + if isinstance(self.calibration, Unset): + calibration = UNSET + elif isinstance(self.calibration, DuetCalibration): + calibration = self.calibration.to_dict() + else: + calibration = self.calibration + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "years_since_burn": years_since_burn, + "source_grid_id": source_grid_id, + } + ) + if wind_direction is not UNSET: + field_dict["wind_direction"] = wind_direction + if wind_variability is not UNSET: + field_dict["wind_variability"] = wind_variability + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if bands is not UNSET: + field_dict["bands"] = bands + if calibration is not UNSET: + field_dict["calibration"] = calibration + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.duet_calibration import DuetCalibration + + d = dict(src_dict) + years_since_burn = d.pop("years_since_burn") + + source_grid_id = d.pop("source_grid_id") + + wind_direction = d.pop("wind_direction", UNSET) + + wind_variability = d.pop("wind_variability", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _bands = d.pop("bands", UNSET) + bands: list[DuetBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = DuetBand(bands_item_data) + + bands.append(bands_item) + + def _parse_calibration(data: object) -> DuetCalibration | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + calibration_type_0 = DuetCalibration.from_dict(data) + + return calibration_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DuetCalibration | None | Unset, data) + + calibration = _parse_calibration(d.pop("calibration", UNSET)) + + create_duet_request = cls( + years_since_burn=years_since_burn, + source_grid_id=source_grid_id, + wind_direction=wind_direction, + wind_variability=wind_variability, + name=name, + description=description, + tags=tags, + bands=bands, + calibration=calibration, + ) + + return create_duet_request diff --git a/fastfuels_sdk/v2/client_library/models/create_fbfm_13_lookup_request.py b/fastfuels_sdk/v2/client_library/models/create_fbfm_13_lookup_request.py new file mode 100644 index 0000000..7e1d917 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_fbfm_13_lookup_request.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.fbfm_13_lookup_band import Fbfm13LookupBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateFbfm13LookupRequest") + + +@_attrs_define +class CreateFbfm13LookupRequest: + """Request to create a grid by looking up FBFM13 fuel parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Attributes: + source_grid_id (str): Grid containing FBFM13 codes + bands (list[Fbfm13LookupBand]): + source_band (str | Unset): Band in source grid containing FBFM13 codes Default: 'fbfm13'. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): + """ + + source_grid_id: str + bands: list[Fbfm13LookupBand] + source_band: str | Unset = "fbfm13" + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source_grid_id = self.source_grid_id + + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + source_band = self.source_band + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_grid_id": source_grid_id, + "bands": bands, + } + ) + if source_band is not UNSET: + field_dict["source_band"] = source_band + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_modification import GridModification + + d = dict(src_dict) + source_grid_id = d.pop("source_grid_id") + + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = Fbfm13LookupBand(bands_item_data) + + bands.append(bands_item) + + source_band = d.pop("source_band", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_fbfm_13_lookup_request = cls( + source_grid_id=source_grid_id, + bands=bands, + source_band=source_band, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_fbfm_13_lookup_request.additional_properties = d + return create_fbfm_13_lookup_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_fbfm_40_lookup_request.py b/fastfuels_sdk/v2/client_library/models/create_fbfm_40_lookup_request.py new file mode 100644 index 0000000..90bea72 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_fbfm_40_lookup_request.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.fbfm_40_lookup_band import Fbfm40LookupBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateFbfm40LookupRequest") + + +@_attrs_define +class CreateFbfm40LookupRequest: + """Request to create a grid by looking up FBFM40 fuel parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Attributes: + source_grid_id (str): Grid containing FBFM40 codes + bands (list[Fbfm40LookupBand]): + source_band (str | Unset): Band in source grid containing FBFM codes Default: 'fbfm'. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): + """ + + source_grid_id: str + bands: list[Fbfm40LookupBand] + source_band: str | Unset = "fbfm" + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source_grid_id = self.source_grid_id + + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + source_band = self.source_band + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_grid_id": source_grid_id, + "bands": bands, + } + ) + if source_band is not UNSET: + field_dict["source_band"] = source_band + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_modification import GridModification + + d = dict(src_dict) + source_grid_id = d.pop("source_grid_id") + + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = Fbfm40LookupBand(bands_item_data) + + bands.append(bands_item) + + source_band = d.pop("source_band", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_fbfm_40_lookup_request = cls( + source_grid_id=source_grid_id, + bands=bands, + source_band=source_band, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_fbfm_40_lookup_request.additional_properties = d + return create_fbfm_40_lookup_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_fccs_lookup_request.py b/fastfuels_sdk/v2/client_library/models/create_fccs_lookup_request.py new file mode 100644 index 0000000..5b2fa37 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_fccs_lookup_request.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.fccs_lookup_band import FccsLookupBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateFccsLookupRequest") + + +@_attrs_define +class CreateFccsLookupRequest: + """Request to create a grid by looking up FCCS fuel parameters. + + Unlike entry-point grid creation requests, domain_id is not required + because derived grids carry the same domain reference as their source. + + Attributes: + source_grid_id (str): Grid containing FCCS codes + bands (list[FccsLookupBand]): + source_band (str | Unset): Band in source grid containing FCCS codes Default: 'fccs'. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): + """ + + source_grid_id: str + bands: list[FccsLookupBand] + source_band: str | Unset = "fccs" + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source_grid_id = self.source_grid_id + + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + source_band = self.source_band + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_grid_id": source_grid_id, + "bands": bands, + } + ) + if source_band is not UNSET: + field_dict["source_band"] = source_band + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_modification import GridModification + + d = dict(src_dict) + source_grid_id = d.pop("source_grid_id") + + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = FccsLookupBand(bands_item_data) + + bands.append(bands_item) + + source_band = d.pop("source_band", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_fccs_lookup_request = cls( + source_grid_id=source_grid_id, + bands=bands, + source_band=source_band, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_fccs_lookup_request.additional_properties = d + return create_fccs_lookup_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request.py b/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request.py new file mode 100644 index 0000000..df5939e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.create_gdam_inventory_request_impute_columns_item import ( + CreateGdamInventoryRequestImputeColumnsItem, +) +from ..models.inventory_type import InventoryType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateGdamInventoryRequest") + + +@_attrs_define +class CreateGdamInventoryRequest: + """Request body for creating an inventory via GDAM allometry imputation. + + Attributes: + source_tree_inventory_id (str): ID of a completed tree inventory whose missing morphology columns (dbh, crown + ratio, species) GDAM will fill in. Existing values are preserved; only missing cells are imputed. + type_ (InventoryType | Unset): Type of entities in the inventory. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + impute_columns (list[CreateGdamInventoryRequestImputeColumnsItem] | Unset): Which morphology columns GDAM should + impute. Defaults to all of `dbh`, `crown_ratio`, `fia_species_code`. Narrow it (e.g. `['fia_species_code']`) to + impute fewer columns and write less to disk; columns left out are not imputed (they stay as the source had + them). Must contain at least one column, with no duplicates. + """ + + source_tree_inventory_id: str + type_: InventoryType | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + impute_columns: list[CreateGdamInventoryRequestImputeColumnsItem] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source_tree_inventory_id = self.source_tree_inventory_id + + type_: str | Unset = UNSET + if not isinstance(self.type_, Unset): + type_ = self.type_.value + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + impute_columns: list[str] | Unset = UNSET + if not isinstance(self.impute_columns, Unset): + impute_columns = [] + for impute_columns_item_data in self.impute_columns: + impute_columns_item = impute_columns_item_data.value + impute_columns.append(impute_columns_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_tree_inventory_id": source_tree_inventory_id, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if impute_columns is not UNSET: + field_dict["impute_columns"] = impute_columns + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + source_tree_inventory_id = d.pop("source_tree_inventory_id") + + _type_ = d.pop("type", UNSET) + type_: InventoryType | Unset + if isinstance(_type_, Unset): + type_ = UNSET + else: + type_ = InventoryType(_type_) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _impute_columns = d.pop("impute_columns", UNSET) + impute_columns: list[CreateGdamInventoryRequestImputeColumnsItem] | Unset = ( + UNSET + ) + if _impute_columns is not UNSET: + impute_columns = [] + for impute_columns_item_data in _impute_columns: + impute_columns_item = CreateGdamInventoryRequestImputeColumnsItem( + impute_columns_item_data + ) + + impute_columns.append(impute_columns_item) + + create_gdam_inventory_request = cls( + source_tree_inventory_id=source_tree_inventory_id, + type_=type_, + name=name, + description=description, + tags=tags, + impute_columns=impute_columns, + ) + + create_gdam_inventory_request.additional_properties = d + return create_gdam_inventory_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request_impute_columns_item.py b/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request_impute_columns_item.py new file mode 100644 index 0000000..7af142c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_gdam_inventory_request_impute_columns_item.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class CreateGdamInventoryRequestImputeColumnsItem(str, Enum): + CROWN_RATIO = "crown_ratio" + DBH = "dbh" + FIA_SPECIES_CODE = "fia_species_code" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/create_geo_tiff_upload_request.py b/fastfuels_sdk/v2/client_library/models/create_geo_tiff_upload_request.py new file mode 100644 index 0000000..f052e5c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_geo_tiff_upload_request.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.upload_band_definition import UploadBandDefinition + + +T = TypeVar("T", bound="CreateGeoTIFFUploadRequest") + + +@_attrs_define +class CreateGeoTIFFUploadRequest: + """ + Attributes: + bands (list[UploadBandDefinition]): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + num_buffer_cells (int | Unset): Number of extra native-resolution cells to keep around the domain extent in the + stored grid. The uploaded GeoTIFF must cover the domain bbox expanded by num_buffer_cells * native_pixel_size on + each side; pixels beyond that expanded extent are clipped away. Default: 0. + """ + + bands: list[UploadBandDefinition] + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + num_buffer_cells: int | Unset = 0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.to_dict() + bands.append(bands_item) + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + num_buffer_cells = self.num_buffer_cells + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "bands": bands, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if num_buffer_cells is not UNSET: + field_dict["num_buffer_cells"] = num_buffer_cells + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.upload_band_definition import UploadBandDefinition + + d = dict(src_dict) + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = UploadBandDefinition.from_dict(bands_item_data) + + bands.append(bands_item) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + num_buffer_cells = d.pop("num_buffer_cells", UNSET) + + create_geo_tiff_upload_request = cls( + bands=bands, + name=name, + description=description, + tags=tags, + num_buffer_cells=num_buffer_cells, + ) + + create_geo_tiff_upload_request.additional_properties = d + return create_geo_tiff_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_inventory_upload_request.py b/fastfuels_sdk/v2/client_library/models/create_inventory_upload_request.py new file mode 100644 index 0000000..9eb63fd --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_inventory_upload_request.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_upload_format import InventoryUploadFormat +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_column_mapping import InventoryColumnMapping + + +T = TypeVar("T", bound="CreateInventoryUploadRequest") + + +@_attrs_define +class CreateInventoryUploadRequest: + """ + Attributes: + format_ (InventoryUploadFormat): + columns (InventoryColumnMapping | Unset): Maps v2 column names to the corresponding column names in the uploaded + file. + + Omit any entry whose column already uses the v2 name. For GeoJSON and + GeoPackage formats, x and y are extracted from geometry — their mapping + entries are ignored. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + format_: InventoryUploadFormat + columns: InventoryColumnMapping | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + format_ = self.format_.value + + columns: dict[str, Any] | Unset = UNSET + if not isinstance(self.columns, Unset): + columns = self.columns.to_dict() + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "format": format_, + } + ) + if columns is not UNSET: + field_dict["columns"] = columns + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_column_mapping import InventoryColumnMapping + + d = dict(src_dict) + format_ = InventoryUploadFormat(d.pop("format")) + + _columns = d.pop("columns", UNSET) + columns: InventoryColumnMapping | Unset + if isinstance(_columns, Unset): + columns = UNSET + else: + columns = InventoryColumnMapping.from_dict(_columns) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + create_inventory_upload_request = cls( + format_=format_, + columns=columns, + name=name, + description=description, + tags=tags, + ) + + create_inventory_upload_request.additional_properties = d + return create_inventory_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_key_request.py b/fastfuels_sdk/v2/client_library/models/create_key_request.py new file mode 100644 index 0000000..4334e75 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_key_request.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.access import Access +from ..models.scope import Scope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateKeyRequest") + + +@_attrs_define +class CreateKeyRequest: + """Request body for creating an API key. + + Attributes: + name (str): A name to semantically identify the key. + description (None | str | Unset): An optional description of the key's purpose. + valid_days (int | Unset): Number of days for which this key will be valid. Default: 30. + scopes (list[Scope] | Unset): A list of scopes available to the key. + access (Access | Unset): Access types for an API key. + application_id (None | str | Unset): Application ID accessed by the API key. + """ + + name: str + description: None | str | Unset = UNSET + valid_days: int | Unset = 30 + scopes: list[Scope] | Unset = UNSET + access: Access | Unset = UNSET + application_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + valid_days = self.valid_days + + scopes: list[str] | Unset = UNSET + if not isinstance(self.scopes, Unset): + scopes = [] + for scopes_item_data in self.scopes: + scopes_item = scopes_item_data.value + scopes.append(scopes_item) + + access: str | Unset = UNSET + if not isinstance(self.access, Unset): + access = self.access.value + + application_id: None | str | Unset + if isinstance(self.application_id, Unset): + application_id = UNSET + else: + application_id = self.application_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + } + ) + if description is not UNSET: + field_dict["description"] = description + if valid_days is not UNSET: + field_dict["valid_days"] = valid_days + if scopes is not UNSET: + field_dict["scopes"] = scopes + if access is not UNSET: + field_dict["access"] = access + if application_id is not UNSET: + field_dict["application_id"] = application_id + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name") + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + valid_days = d.pop("valid_days", UNSET) + + _scopes = d.pop("scopes", UNSET) + scopes: list[Scope] | Unset = UNSET + if _scopes is not UNSET: + scopes = [] + for scopes_item_data in _scopes: + scopes_item = Scope(scopes_item_data) + + scopes.append(scopes_item) + + _access = d.pop("access", UNSET) + access: Access | Unset + if isinstance(_access, Unset): + access = UNSET + else: + access = Access(_access) + + def _parse_application_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + application_id = _parse_application_id(d.pop("application_id", UNSET)) + + create_key_request = cls( + name=name, + description=description, + valid_days=valid_days, + scopes=scopes, + access=access, + application_id=application_id, + ) + + create_key_request.additional_properties = d + return create_key_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_key_response.py b/fastfuels_sdk/v2/client_library/models/create_key_response.py new file mode 100644 index 0000000..195c1dc --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_key_response.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.access import Access +from ..models.scope import Scope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateKeyResponse") + + +@_attrs_define +class CreateKeyResponse: + """Response for key creation. Includes the secret, which is shown only once. + + Attributes: + id (str): Unique identifier for the key (SHA-256 hash of the secret). + owner_id (str): The unique ID of the user or application who owns the key. + creator_id (str): The unique ID of the human user who created the key. + name (str): A name to semantically identify the key. + secret (str): The API key secret. Store this securely — it cannot be retrieved again. + description (None | str | Unset): An optional description of the key's purpose. + valid_days (int | Unset): Number of days for which this key will be valid. Default: 30. + scopes (list[Scope] | Unset): A list of scopes available to the key. + access (Access | Unset): Access types for an API key. + application_id (None | str | Unset): Application ID accessed by the API key. + created_on (datetime.datetime | Unset): The date and time the key was created. + expires_on (datetime.datetime | Unset): The date at which this key is no longer valid. + """ + + id: str + owner_id: str + creator_id: str + name: str + secret: str + description: None | str | Unset = UNSET + valid_days: int | Unset = 30 + scopes: list[Scope] | Unset = UNSET + access: Access | Unset = UNSET + application_id: None | str | Unset = UNSET + created_on: datetime.datetime | Unset = UNSET + expires_on: datetime.datetime | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + owner_id = self.owner_id + + creator_id = self.creator_id + + name = self.name + + secret = self.secret + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + valid_days = self.valid_days + + scopes: list[str] | Unset = UNSET + if not isinstance(self.scopes, Unset): + scopes = [] + for scopes_item_data in self.scopes: + scopes_item = scopes_item_data.value + scopes.append(scopes_item) + + access: str | Unset = UNSET + if not isinstance(self.access, Unset): + access = self.access.value + + application_id: None | str | Unset + if isinstance(self.application_id, Unset): + application_id = UNSET + else: + application_id = self.application_id + + created_on: str | Unset = UNSET + if not isinstance(self.created_on, Unset): + created_on = self.created_on.isoformat() + + expires_on: str | Unset = UNSET + if not isinstance(self.expires_on, Unset): + expires_on = self.expires_on.isoformat() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "owner_id": owner_id, + "creator_id": creator_id, + "name": name, + "secret": secret, + } + ) + if description is not UNSET: + field_dict["description"] = description + if valid_days is not UNSET: + field_dict["valid_days"] = valid_days + if scopes is not UNSET: + field_dict["scopes"] = scopes + if access is not UNSET: + field_dict["access"] = access + if application_id is not UNSET: + field_dict["application_id"] = application_id + if created_on is not UNSET: + field_dict["created_on"] = created_on + if expires_on is not UNSET: + field_dict["expires_on"] = expires_on + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + id = d.pop("id") + + owner_id = d.pop("owner_id") + + creator_id = d.pop("creator_id") + + name = d.pop("name") + + secret = d.pop("secret") + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + valid_days = d.pop("valid_days", UNSET) + + _scopes = d.pop("scopes", UNSET) + scopes: list[Scope] | Unset = UNSET + if _scopes is not UNSET: + scopes = [] + for scopes_item_data in _scopes: + scopes_item = Scope(scopes_item_data) + + scopes.append(scopes_item) + + _access = d.pop("access", UNSET) + access: Access | Unset + if isinstance(_access, Unset): + access = UNSET + else: + access = Access(_access) + + def _parse_application_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + application_id = _parse_application_id(d.pop("application_id", UNSET)) + + _created_on = d.pop("created_on", UNSET) + created_on: datetime.datetime | Unset + if isinstance(_created_on, Unset): + created_on = UNSET + else: + created_on = datetime.datetime.fromisoformat(_created_on) + + _expires_on = d.pop("expires_on", UNSET) + expires_on: datetime.datetime | Unset + if isinstance(_expires_on, Unset): + expires_on = UNSET + else: + expires_on = datetime.datetime.fromisoformat(_expires_on) + + create_key_response = cls( + id=id, + owner_id=owner_id, + creator_id=creator_id, + name=name, + secret=secret, + description=description, + valid_days=valid_days, + scopes=scopes, + access=access, + application_id=application_id, + created_on=created_on, + expires_on=expires_on, + ) + + create_key_response.additional_properties = d + return create_key_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_landfire_canopy_request.py b/fastfuels_sdk/v2/client_library/models/create_landfire_canopy_request.py new file mode 100644 index 0000000..d77e7a9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_landfire_canopy_request.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landfire_canopy_fuel_band import LandfireCanopyFuelBand +from ..models.landfire_canopy_version import LandfireCanopyVersion +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLandfireCanopyRequest") + + +@_attrs_define +class CreateLandfireCanopyRequest: + """Request to create a grid from LANDFIRE canopy data. + + Returns a grid with one or more continuous canopy bands at 30m + resolution (CONUS): + - chm: Canopy height (m) + - cbd: Canopy bulk density (kg/m**3) + - cbh: Canopy base height (m) + - cc: Canopy cover (%) + + Bands are validated against the canopy band vocabulary and may not be + duplicated. + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (LandfireCanopyVersion | Unset): Available LANDFIRE canopy data versions. + bands (list[LandfireCanopyFuelBand] | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: LandfireCanopyVersion | Unset = UNSET + bands: list[LandfireCanopyFuelBand] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if bands is not UNSET: + field_dict["bands"] = bands + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: LandfireCanopyVersion | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = LandfireCanopyVersion(_version) + + _bands = d.pop("bands", UNSET) + bands: list[LandfireCanopyFuelBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = LandfireCanopyFuelBand(bands_item_data) + + bands.append(bands_item) + + create_landfire_canopy_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + bands=bands, + ) + + create_landfire_canopy_request.additional_properties = d + return create_landfire_canopy_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_13_request.py b/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_13_request.py new file mode 100644 index 0000000..29d8d82 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_13_request.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landfire_fbfm_13_version import LandfireFbfm13Version +from ..models.non_burnable_fuel_model import NonBurnableFuelModel +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLandfireFbfm13Request") + + +@_attrs_define +class CreateLandfireFbfm13Request: + """Request to create a grid from LANDFIRE FBFM13. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm13. + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (LandfireFbfm13Version | Unset): Available LANDFIRE FBFM13 data versions. + remove_non_burnable (list[NonBurnableFuelModel] | None | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: LandfireFbfm13Version | Unset = UNSET + remove_non_burnable: list[NonBurnableFuelModel] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + remove_non_burnable: list[str] | None | Unset + if isinstance(self.remove_non_burnable, Unset): + remove_non_burnable = UNSET + elif isinstance(self.remove_non_burnable, list): + remove_non_burnable = [] + for remove_non_burnable_type_0_item_data in self.remove_non_burnable: + remove_non_burnable_type_0_item = ( + remove_non_burnable_type_0_item_data.value + ) + remove_non_burnable.append(remove_non_burnable_type_0_item) + + else: + remove_non_burnable = self.remove_non_burnable + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if remove_non_burnable is not UNSET: + field_dict["remove_non_burnable"] = remove_non_burnable + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: LandfireFbfm13Version | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = LandfireFbfm13Version(_version) + + def _parse_remove_non_burnable( + data: object, + ) -> list[NonBurnableFuelModel] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + remove_non_burnable_type_0 = [] + _remove_non_burnable_type_0 = data + for remove_non_burnable_type_0_item_data in _remove_non_burnable_type_0: + remove_non_burnable_type_0_item = NonBurnableFuelModel( + remove_non_burnable_type_0_item_data + ) + + remove_non_burnable_type_0.append(remove_non_burnable_type_0_item) + + return remove_non_burnable_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[NonBurnableFuelModel] | None | Unset, data) + + remove_non_burnable = _parse_remove_non_burnable( + d.pop("remove_non_burnable", UNSET) + ) + + create_landfire_fbfm_13_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + remove_non_burnable=remove_non_burnable, + ) + + create_landfire_fbfm_13_request.additional_properties = d + return create_landfire_fbfm_13_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_40_request.py b/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_40_request.py new file mode 100644 index 0000000..dfd542d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_landfire_fbfm_40_request.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landfire_fbfm_40_version import LandfireFbfm40Version +from ..models.non_burnable_fuel_model import NonBurnableFuelModel +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLandfireFbfm40Request") + + +@_attrs_define +class CreateLandfireFbfm40Request: + """Request to create a grid from LANDFIRE FBFM40. + + Returns a single-band grid with categorical fuel model codes. + To convert codes to fuel parameters, use /grids/lookup/fbfm40. + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (LandfireFbfm40Version | Unset): Available LANDFIRE FBFM40 data versions. + remove_non_burnable (list[NonBurnableFuelModel] | None | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: LandfireFbfm40Version | Unset = UNSET + remove_non_burnable: list[NonBurnableFuelModel] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + remove_non_burnable: list[str] | None | Unset + if isinstance(self.remove_non_burnable, Unset): + remove_non_burnable = UNSET + elif isinstance(self.remove_non_burnable, list): + remove_non_burnable = [] + for remove_non_burnable_type_0_item_data in self.remove_non_burnable: + remove_non_burnable_type_0_item = ( + remove_non_burnable_type_0_item_data.value + ) + remove_non_burnable.append(remove_non_burnable_type_0_item) + + else: + remove_non_burnable = self.remove_non_burnable + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if remove_non_burnable is not UNSET: + field_dict["remove_non_burnable"] = remove_non_burnable + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: LandfireFbfm40Version | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = LandfireFbfm40Version(_version) + + def _parse_remove_non_burnable( + data: object, + ) -> list[NonBurnableFuelModel] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + remove_non_burnable_type_0 = [] + _remove_non_burnable_type_0 = data + for remove_non_burnable_type_0_item_data in _remove_non_burnable_type_0: + remove_non_burnable_type_0_item = NonBurnableFuelModel( + remove_non_burnable_type_0_item_data + ) + + remove_non_burnable_type_0.append(remove_non_burnable_type_0_item) + + return remove_non_burnable_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[NonBurnableFuelModel] | None | Unset, data) + + remove_non_burnable = _parse_remove_non_burnable( + d.pop("remove_non_burnable", UNSET) + ) + + create_landfire_fbfm_40_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + remove_non_burnable=remove_non_burnable, + ) + + create_landfire_fbfm_40_request.additional_properties = d + return create_landfire_fbfm_40_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_landfire_fccs_request.py b/fastfuels_sdk/v2/client_library/models/create_landfire_fccs_request.py new file mode 100644 index 0000000..796ad33 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_landfire_fccs_request.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landfire_fccs_version import LandfireFccsVersion +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLandfireFccsRequest") + + +@_attrs_define +class CreateLandfireFccsRequest: + """Request to create a grid from LANDFIRE FCCS. + + Returns a single-band grid with categorical fuelbed IDs. + To convert IDs to fuel parameters, use /grids/lookup/fccs. + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (LandfireFccsVersion | Unset): Available LANDFIRE FCCS data versions. + remove_bare_ground (bool | Unset): Default: False. + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: LandfireFccsVersion | Unset = UNSET + remove_bare_ground: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + remove_bare_ground = self.remove_bare_ground + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if remove_bare_ground is not UNSET: + field_dict["remove_bare_ground"] = remove_bare_ground + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: LandfireFccsVersion | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = LandfireFccsVersion(_version) + + remove_bare_ground = d.pop("remove_bare_ground", UNSET) + + create_landfire_fccs_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + remove_bare_ground=remove_bare_ground, + ) + + create_landfire_fccs_request.additional_properties = d + return create_landfire_fccs_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_landfire_topography_request.py b/fastfuels_sdk/v2/client_library/models/create_landfire_topography_request.py new file mode 100644 index 0000000..791d98b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_landfire_topography_request.py @@ -0,0 +1,238 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landfire_topography_version import LandfireTopographyVersion +from ..models.topography_band import TopographyBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLandfireTopographyRequest") + + +@_attrs_define +class CreateLandfireTopographyRequest: + """Request to create a grid from LANDFIRE topographic data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (LandfireTopographyVersion | Unset): Available LANDFIRE topography data versions. + bands (list[TopographyBand] | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: LandfireTopographyVersion | Unset = UNSET + bands: list[TopographyBand] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if bands is not UNSET: + field_dict["bands"] = bands + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: LandfireTopographyVersion | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = LandfireTopographyVersion(_version) + + _bands = d.pop("bands", UNSET) + bands: list[TopographyBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = TopographyBand(bands_item_data) + + bands.append(bands_item) + + create_landfire_topography_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + bands=bands, + ) + + create_landfire_topography_request.additional_properties = d + return create_landfire_topography_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_layerset_rasterize_request.py b/fastfuels_sdk/v2/client_library/models/create_layerset_rasterize_request.py new file mode 100644 index 0000000..118e2f2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_layerset_rasterize_request.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.overlap_method import OverlapMethod +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateLayersetRasterizeRequest") + + +@_attrs_define +class CreateLayersetRasterizeRequest: + """Request to create a grid by rasterizing a previously-uploaded layerset. + + The referenced layerset must be an existing Feature owned by the caller, + uploaded via ``POST /domains/{id}/features/layerset``. The worker fetches + the GeoJSON from GCS at job time; a fresh upload produces a new + ``feature_id``, so the reference is effectively immutable. + + Attributes: + layerset_id (str): Feature ID of an existing layerset uploaded for this domain. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Buffer in result-grid cells around the domain extent. Cells inside the + buffered extent that fall outside polygon coverage are populated with the rasterizer's fill value. Default 0 + adds no buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + overlap_method (OverlapMethod | Unset): Per-cell reduction when multiple polygons of the same ``fuel_type`` + overlap a cell. Applies to ``height`` and the optional bands only — + ``loading`` is always summed by ``fastfuels_core.rasterize_layerset`` + regardless of this setting. + """ + + layerset_id: str + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + overlap_method: OverlapMethod | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + layerset_id = self.layerset_id + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + overlap_method: str | Unset = UNSET + if not isinstance(self.overlap_method, Unset): + overlap_method = self.overlap_method.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "layerset_id": layerset_id, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if overlap_method is not UNSET: + field_dict["overlap_method"] = overlap_method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + layerset_id = d.pop("layerset_id") + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _overlap_method = d.pop("overlap_method", UNSET) + overlap_method: OverlapMethod | Unset + if isinstance(_overlap_method, Unset): + overlap_method = UNSET + else: + overlap_method = OverlapMethod(_overlap_method) + + create_layerset_rasterize_request = cls( + layerset_id=layerset_id, + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + overlap_method=overlap_method, + ) + + create_layerset_rasterize_request.additional_properties = d + return create_layerset_rasterize_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_layerset_request_body.py b/fastfuels_sdk/v2/client_library/models/create_layerset_request_body.py new file mode 100644 index 0000000..ee41b7d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_layerset_request_body.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.layerset_crs import LayersetCrs + from ..models.layerset_feature import LayersetFeature + + +T = TypeVar("T", bound="CreateLayersetRequestBody") + + +@_attrs_define +class CreateLayersetRequestBody: + """Request body for uploading a flat GeoJSON layerset. + + The body **is** the GeoJSON FeatureCollection (matching ``POST /domains``, + whose body is a ``FeatureCollection`` directly), extended with the + resource-metadata fields. No ``type`` discriminator: the URL + ``/features/layerset/geojson`` already discriminates layersets from + road/water uploads. + + ``name`` overrides the optional GeoJSON ``name`` member inherited from + ``LayersetFeatureCollection`` — the FeatureCollection's name doubles as the + resource name, exactly as ``CreateDomainRequestBody`` treats it. + + Attributes: + type_ (Literal['FeatureCollection']): + features (list[LayersetFeature]): + bbox (list[float] | None | Unset): + name (str | Unset): Default: ''. + crs (LayersetCrs | None | Unset): + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + type_: Literal["FeatureCollection"] + features: list[LayersetFeature] + bbox: list[float] | None | Unset = UNSET + name: str | Unset = "" + crs: LayersetCrs | None | Unset = UNSET + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.layerset_crs import LayersetCrs + + type_ = self.type_ + + features = [] + for features_item_data in self.features: + features_item = features_item_data.to_dict() + features.append(features_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + name = self.name + + crs: dict[str, Any] | None | Unset + if isinstance(self.crs, Unset): + crs = UNSET + elif isinstance(self.crs, LayersetCrs): + crs = self.crs.to_dict() + else: + crs = self.crs + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "features": features, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + if name is not UNSET: + field_dict["name"] = name + if crs is not UNSET: + field_dict["crs"] = crs + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.layerset_crs import LayersetCrs + from ..models.layerset_feature import LayersetFeature + + d = dict(src_dict) + type_ = cast(Literal["FeatureCollection"], d.pop("type")) + if type_ != "FeatureCollection": + raise ValueError( + f"type must match const 'FeatureCollection', got '{type_}'" + ) + + features = [] + _features = d.pop("features") + for features_item_data in _features: + features_item = LayersetFeature.from_dict(features_item_data) + + features.append(features_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + name = d.pop("name", UNSET) + + def _parse_crs(data: object) -> LayersetCrs | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + crs_type_0 = LayersetCrs.from_dict(data) + + return crs_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(LayersetCrs | None | Unset, data) + + crs = _parse_crs(d.pop("crs", UNSET)) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + create_layerset_request_body = cls( + type_=type_, + features=features, + bbox=bbox, + name=name, + crs=crs, + description=description, + tags=tags, + ) + + create_layerset_request_body.additional_properties = d + return create_layerset_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_meta_chm_request.py b/fastfuels_sdk/v2/client_library/models/create_meta_chm_request.py new file mode 100644 index 0000000..a933e78 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_meta_chm_request.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.meta_chm_version import MetaCHMVersion +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateMetaChmRequest") + + +@_attrs_define +class CreateMetaChmRequest: + """Request to create a grid from Meta CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (MetaCHMVersion | Unset): Available Meta CHM data versions. + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: MetaCHMVersion | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: MetaCHMVersion | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = MetaCHMVersion(_version) + + create_meta_chm_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + ) + + create_meta_chm_request.additional_properties = d + return create_meta_chm_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_naip_chm_request.py b/fastfuels_sdk/v2/client_library/models/create_naip_chm_request.py new file mode 100644 index 0000000..58c1be3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_naip_chm_request.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateNaipChmRequest") + + +@_attrs_define +class CreateNaipChmRequest: + """Request to create a grid from NAIP CHM. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + create_naip_chm_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + ) + + create_naip_chm_request.additional_properties = d + return create_naip_chm_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_netcdf_upload_request.py b/fastfuels_sdk/v2/client_library/models/create_netcdf_upload_request.py new file mode 100644 index 0000000..2d53e5b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_netcdf_upload_request.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateNetcdfUploadRequest") + + +@_attrs_define +class CreateNetcdfUploadRequest: + """ + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + num_buffer_cells (int | Unset): Number of extra native-resolution cells to keep around the domain extent in the + stored grid. The uploaded netCDF must cover the domain bbox expanded by num_buffer_cells * native_pixel_size on + each side; pixels beyond that expanded extent are clipped away. Default: 0. + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + num_buffer_cells: int | Unset = 0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + num_buffer_cells = self.num_buffer_cells + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if num_buffer_cells is not UNSET: + field_dict["num_buffer_cells"] = num_buffer_cells + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + num_buffer_cells = d.pop("num_buffer_cells", UNSET) + + create_netcdf_upload_request = cls( + name=name, + description=description, + tags=tags, + num_buffer_cells=num_buffer_cells, + ) + + create_netcdf_upload_request.additional_properties = d + return create_netcdf_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_osm_road_feature_request.py b/fastfuels_sdk/v2/client_library/models/create_osm_road_feature_request.py new file mode 100644 index 0000000..c5cd342 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_osm_road_feature_request.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateOsmRoadFeatureRequest") + + +@_attrs_define +class CreateOsmRoadFeatureRequest: + """Request body for creating a road feature via OpenStreetMap. + + Attributes: + type_ (Literal['road'] | Unset): Default: 'road'. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + extent_buffer_m (float | Unset): Distance in meters to expand the domain extent outward before clipping fetched + features. Lets roads that exit the domain at the boundary extend slightly past the edge, providing context for + visualization and downstream operations (fuel breaks, perimeter analysis). Applied in the domain's projected CRS + (reprojected to UTM if the domain CRS is geographic). Default 0 clips exactly to the domain boundary. Maximum: + 100 meters. Default: 0.0. + """ + + type_: Literal["road"] | Unset = "road" + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + extent_buffer_m: float | Unset = 0.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + extent_buffer_m = self.extent_buffer_m + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if type_ is not UNSET: + field_dict["type"] = type_ + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if extent_buffer_m is not UNSET: + field_dict["extent_buffer_m"] = extent_buffer_m + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["road"] | Unset, d.pop("type", UNSET)) + if type_ != "road" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'road', got '{type_}'") + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + extent_buffer_m = d.pop("extent_buffer_m", UNSET) + + create_osm_road_feature_request = cls( + type_=type_, + name=name, + description=description, + tags=tags, + extent_buffer_m=extent_buffer_m, + ) + + create_osm_road_feature_request.additional_properties = d + return create_osm_road_feature_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_osm_water_feature_request.py b/fastfuels_sdk/v2/client_library/models/create_osm_water_feature_request.py new file mode 100644 index 0000000..a1bf229 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_osm_water_feature_request.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateOsmWaterFeatureRequest") + + +@_attrs_define +class CreateOsmWaterFeatureRequest: + """Request body for creating a water feature via OpenStreetMap. + + Attributes: + type_ (Literal['water'] | Unset): Default: 'water'. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + extent_buffer_m (float | Unset): Distance in meters to expand the domain extent outward before clipping fetched + features. Lets streams and rivers that exit the domain at the boundary extend slightly past the edge, providing + context for visualization and downstream operations (fuel breaks, perimeter analysis). Applied in the domain's + projected CRS (reprojected to UTM if the domain CRS is geographic). Default 0 clips exactly to the domain + boundary. Maximum: 100 meters. Default: 0.0. + """ + + type_: Literal["water"] | Unset = "water" + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + extent_buffer_m: float | Unset = 0.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + extent_buffer_m = self.extent_buffer_m + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if type_ is not UNSET: + field_dict["type"] = type_ + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if extent_buffer_m is not UNSET: + field_dict["extent_buffer_m"] = extent_buffer_m + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["water"] | Unset, d.pop("type", UNSET)) + if type_ != "water" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'water', got '{type_}'") + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + extent_buffer_m = d.pop("extent_buffer_m", UNSET) + + create_osm_water_feature_request = cls( + type_=type_, + name=name, + description=description, + tags=tags, + extent_buffer_m=extent_buffer_m, + ) + + create_osm_water_feature_request.additional_properties = d + return create_osm_water_feature_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_pim_inventory_request.py b/fastfuels_sdk/v2/client_library/models/create_pim_inventory_request.py new file mode 100644 index 0000000..4c1dfbf --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_pim_inventory_request.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_type import InventoryType +from ..models.point_process import PointProcess +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_modification import InventoryModification + + +T = TypeVar("T", bound="CreatePimInventoryRequest") + + +@_attrs_define +class CreatePimInventoryRequest: + """Request body for creating an inventory via PIM expansion. + + Attributes: + source_pim_grid_id (str): ID of a completed PIM grid to use as the source. + type_ (InventoryType | Unset): Type of entities in the inventory. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + seed (int | Unset): Random seed for reproducibility. Generated randomly if omitted. + point_process (PointProcess | Unset): Spatial point process for tree coordinate assignment. + modifications (list[InventoryModification] | Unset): Modifications to apply after point process expansion. + treatments (list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset): Silvicultural treatments to + apply after modifications. + """ + + source_pim_grid_id: str + type_: InventoryType | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + seed: int | Unset = UNSET + point_process: PointProcess | Unset = UNSET + modifications: list[InventoryModification] | Unset = UNSET + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + + source_pim_grid_id = self.source_pim_grid_id + + type_: str | Unset = UNSET + if not isinstance(self.type_, Unset): + type_ = self.type_.value + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + seed = self.seed + + point_process: str | Unset = UNSET + if not isinstance(self.point_process, Unset): + point_process = self.point_process.value + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + treatments: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.treatments, Unset): + treatments = [] + for treatments_item_data in self.treatments: + treatments_item: dict[str, Any] + if isinstance(treatments_item_data, InventoryDiameterTreatment): + treatments_item = treatments_item_data.to_dict() + else: + treatments_item = treatments_item_data.to_dict() + + treatments.append(treatments_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_pim_grid_id": source_pim_grid_id, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if seed is not UNSET: + field_dict["seed"] = seed + if point_process is not UNSET: + field_dict["point_process"] = point_process + if modifications is not UNSET: + field_dict["modifications"] = modifications + if treatments is not UNSET: + field_dict["treatments"] = treatments + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_modification import InventoryModification + + d = dict(src_dict) + source_pim_grid_id = d.pop("source_pim_grid_id") + + _type_ = d.pop("type", UNSET) + type_: InventoryType | Unset + if isinstance(_type_, Unset): + type_ = UNSET + else: + type_ = InventoryType(_type_) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + seed = d.pop("seed", UNSET) + + _point_process = d.pop("point_process", UNSET) + point_process: PointProcess | Unset + if isinstance(_point_process, Unset): + point_process = UNSET + else: + point_process = PointProcess(_point_process) + + _modifications = d.pop("modifications", UNSET) + modifications: list[InventoryModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = InventoryModification.from_dict( + modifications_item_data + ) + + modifications.append(modifications_item) + + _treatments = d.pop("treatments", UNSET) + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + if _treatments is not UNSET: + treatments = [] + for treatments_item_data in _treatments: + + def _parse_treatments_item( + data: object, + ) -> InventoryBasalAreaTreatment | InventoryDiameterTreatment: + try: + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_0 = InventoryDiameterTreatment.from_dict( + data + ) + + return treatments_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_1 = InventoryBasalAreaTreatment.from_dict(data) + + return treatments_item_type_1 + + treatments_item = _parse_treatments_item(treatments_item_data) + + treatments.append(treatments_item) + + create_pim_inventory_request = cls( + source_pim_grid_id=source_pim_grid_id, + type_=type_, + name=name, + description=description, + tags=tags, + seed=seed, + point_process=point_process, + modifications=modifications, + treatments=treatments, + ) + + create_pim_inventory_request.additional_properties = d + return create_pim_inventory_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_point_cloud_chm_request.py b/fastfuels_sdk/v2/client_library/models/create_point_cloud_chm_request.py new file mode 100644 index 0000000..a53606c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_point_cloud_chm_request.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreatePointCloudChmRequest") + + +@_attrs_define +class CreatePointCloudChmRequest: + """Request to create a canopy height model grid from a point cloud. + + Returns a grid with a single continuous band: + - chm: Canopy height in meters + + The point cloud must be airborne (`type: als`) and `completed`. Cell size + comes from `alignment.resolution`, defaulting to 1 m — unlike the + raster-backed canopy sources there is no source pixel size to inherit. + + Attributes: + source_point_cloud_id (str): ID of the point cloud to rasterize. Must be an ALS cloud in this domain. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + """ + + source_point_cloud_id: str + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + source_point_cloud_id = self.source_point_cloud_id + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_point_cloud_id": source_point_cloud_id, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + source_point_cloud_id = d.pop("source_point_cloud_id") + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + create_point_cloud_chm_request = cls( + source_point_cloud_id=source_point_cloud_id, + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + ) + + create_point_cloud_chm_request.additional_properties = d + return create_point_cloud_chm_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_point_cloud_upload_request.py b/fastfuels_sdk/v2/client_library/models/create_point_cloud_upload_request.py new file mode 100644 index 0000000..14aab41 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_point_cloud_upload_request.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.point_cloud_type import PointCloudType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreatePointCloudUploadRequest") + + +@_attrs_define +class CreatePointCloudUploadRequest: + """Request body for creating a point cloud from a direct file upload. + + Attributes: + type_ (PointCloudType): How a point cloud was acquired. + + The acquisition platform determines the cloud's geometry and which downstream + products it can feed, so it is recorded as a first-class, filterable field. + + - ``als`` — **Airborne Laser Scanning.** Captured from an aircraft or drone + looking down. Covers large areas from above and is the basis for canopy + height models and individual-tree detection. Available from an upload or + from USGS 3DEP. + - ``tls`` — **Terrestrial Laser Scanning.** Captured from a tripod-mounted + scanner on the ground looking out and up. Resolves fine sub-canopy and + stem structure over a small plot. Available from an upload only (3DEP is + airborne and cannot produce terrestrial scans). + name (str | Unset): Human-readable name for the point cloud. Default: ''. + description (str | Unset): Longer free-text description of the point cloud. Default: ''. + tags (list[str] | Unset): Tags for organizing and filtering point clouds. + """ + + type_: PointCloudType + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_.value + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = PointCloudType(d.pop("type")) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + create_point_cloud_upload_request = cls( + type_=type_, + name=name, + description=description, + tags=tags, + ) + + create_point_cloud_upload_request.additional_properties = d + return create_point_cloud_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_resample_request.py b/fastfuels_sdk/v2/client_library/models/create_resample_request.py new file mode 100644 index 0000000..dc76df2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_resample_request.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.create_resample_request_method_overrides import ( + CreateResampleRequestMethodOverrides, + ) + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateResampleRequest") + + +@_attrs_define +class CreateResampleRequest: + """Request to create a grid by resampling an existing grid. + + Unlike entry-point grid creation requests, ``domain_id`` is not required + because derived grids carry the same domain reference as their source. + + The ``alignment`` field controls the output lattice. ``alignment.resolution`` + is required for ``target="domain"`` and ``target="native"``; for + ``target="grid"`` it is optional (defaults to the target grid's exact + transform/shape; if supplied, keeps the target's CRS and origin and + recomputes shape at the new cell size). + + Attributes: + source_grid_id (str): Grid to resample + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Output + alignment target. Default `target="domain"` anchors the resampled grid to the domain origin. + method_overrides (CreateResampleRequestMethodOverrides | Unset): Per-band resampling method overrides keyed by + band key. Wins over ``alignment.method`` for the listed bands. Useful for using nearest-neighbor on categorical + bands while using bilinear on continuous bands. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): + """ + + source_grid_id: str + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + method_overrides: CreateResampleRequestMethodOverrides | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + source_grid_id = self.source_grid_id + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + method_overrides: dict[str, Any] | Unset = UNSET + if not isinstance(self.method_overrides, Unset): + method_overrides = self.method_overrides.to_dict() + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source_grid_id": source_grid_id, + } + ) + if alignment is not UNSET: + field_dict["alignment"] = alignment + if method_overrides is not UNSET: + field_dict["method_overrides"] = method_overrides + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.create_resample_request_method_overrides import ( + CreateResampleRequestMethodOverrides, + ) + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + source_grid_id = d.pop("source_grid_id") + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _method_overrides = d.pop("method_overrides", UNSET) + method_overrides: CreateResampleRequestMethodOverrides | Unset + if isinstance(_method_overrides, Unset): + method_overrides = UNSET + else: + method_overrides = CreateResampleRequestMethodOverrides.from_dict( + _method_overrides + ) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_resample_request = cls( + source_grid_id=source_grid_id, + alignment=alignment, + method_overrides=method_overrides, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_resample_request.additional_properties = d + return create_resample_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_resample_request_method_overrides.py b/fastfuels_sdk/v2/client_library/models/create_resample_request_method_overrides.py new file mode 100644 index 0000000..15cca20 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_resample_request_method_overrides.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.resampling_method import ResamplingMethod + +T = TypeVar("T", bound="CreateResampleRequestMethodOverrides") + + +@_attrs_define +class CreateResampleRequestMethodOverrides: + """Per-band resampling method overrides keyed by band key. Wins over ``alignment.method`` for the listed bands. Useful + for using nearest-neighbor on categorical bands while using bilinear on continuous bands. + + """ + + additional_properties: dict[str, ResamplingMethod] = _attrs_field( + init=False, factory=dict + ) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.value + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + create_resample_request_method_overrides = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = ResamplingMethod(prop_dict) + + additional_properties[prop_name] = additional_property + + create_resample_request_method_overrides.additional_properties = ( + additional_properties + ) + return create_resample_request_method_overrides + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> ResamplingMethod: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: ResamplingMethod) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_three_dep_point_cloud_request.py b/fastfuels_sdk/v2/client_library/models/create_three_dep_point_cloud_request.py new file mode 100644 index 0000000..fd7ebb6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_three_dep_point_cloud_request.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateThreeDepPointCloudRequest") + + +@_attrs_define +class CreateThreeDepPointCloudRequest: + """Request body for fetching a point cloud from USGS 3DEP. + + Attributes: + name (str | Unset): Human-readable name for the point cloud. Default: ''. + description (str | Unset): Longer free-text description of the point cloud. Default: ''. + tags (list[str] | Unset): Tags for organizing and filtering point clouds. + datasets (list[str] | None | Unset): Names of the 3DEP acquisitions to read, in priority order. Omit this to let + the backend choose, which it does by preferring a single acquisition that covers the whole domain and otherwise + combining the fewest acquisitions that fill it. Set it to pin the result to specific acquisitions — for example + to force a higher-density or more recent survey where several overlap. Where two listed acquisitions overlap, + the one listed first is used. Names come from the coverage endpoint; every name must exist and overlap the + domain. + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + datasets: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + datasets: list[str] | None | Unset + if isinstance(self.datasets, Unset): + datasets = UNSET + elif isinstance(self.datasets, list): + datasets = self.datasets + + else: + datasets = self.datasets + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if datasets is not UNSET: + field_dict["datasets"] = datasets + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + def _parse_datasets(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + datasets_type_0 = cast(list[str], data) + + return datasets_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + datasets = _parse_datasets(d.pop("datasets", UNSET)) + + create_three_dep_point_cloud_request = cls( + name=name, + description=description, + tags=tags, + datasets=datasets, + ) + + create_three_dep_point_cloud_request.additional_properties = d + return create_three_dep_point_cloud_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_three_dep_topography_request.py b/fastfuels_sdk/v2/client_library/models/create_three_dep_topography_request.py new file mode 100644 index 0000000..94abfb8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_three_dep_topography_request.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.three_dep_resolution import ThreeDepResolution +from ..models.topography_band import TopographyBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateThreeDepTopographyRequest") + + +@_attrs_define +class CreateThreeDepTopographyRequest: + """Request to create a grid from 3DEP topographic data. + + Returns a grid with one or more continuous bands: elevation (m), + slope (degrees), and/or aspect (degrees). + + `source_resolution` selects the 3DEP product family (1m, 10m, or 30m). + To change the *output* cell size, set ``alignment.resolution`` instead. + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + source_resolution (ThreeDepResolution | Unset): Available resolutions for 3DEP data (meters). + bands (list[TopographyBand] | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + source_resolution: ThreeDepResolution | Unset = UNSET + bands: list[TopographyBand] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + source_resolution: int | Unset = UNSET + if not isinstance(self.source_resolution, Unset): + source_resolution = self.source_resolution.value + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if source_resolution is not UNSET: + field_dict["source_resolution"] = source_resolution + if bands is not UNSET: + field_dict["bands"] = bands + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _source_resolution = d.pop("source_resolution", UNSET) + source_resolution: ThreeDepResolution | Unset + if isinstance(_source_resolution, Unset): + source_resolution = UNSET + else: + source_resolution = ThreeDepResolution(_source_resolution) + + _bands = d.pop("bands", UNSET) + bands: list[TopographyBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = TopographyBand(bands_item_data) + + bands.append(bands_item) + + create_three_dep_topography_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + source_resolution=source_resolution, + bands=bands, + ) + + create_three_dep_topography_request.additional_properties = d + return create_three_dep_topography_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_tree_inventory_request.py b/fastfuels_sdk/v2/client_library/models/create_tree_inventory_request.py new file mode 100644 index 0000000..6e35552 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_tree_inventory_request.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.crown_profile_model import CrownProfileModel +from ..models.tree_band import TreeBand +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.allometry_biomass_source import AllometryBiomassSource + from ..models.allometry_max_crown_radius_source import AllometryMaxCrownRadiusSource + from ..models.inventory_column_max_crown_radius_source import ( + InventoryColumnMaxCrownRadiusSource, + ) + from ..models.inventory_columns_biomass_source import InventoryColumnsBiomassSource + from ..models.moisture_model import MoistureModel + from ..models.resolution_3d import Resolution3D + + +T = TypeVar("T", bound="CreateTreeInventoryRequest") + + +@_attrs_define +class CreateTreeInventoryRequest: + """Request body for creating a tree fuel grid from a tree inventory. + + Does not extend CreateGridRequestBase because 3D grids do not support + modifications — modifications must be applied to the inventory before + voxelization, not to the resulting voxel grid. + + Attributes: + source_inventory_id (str): ID of a completed tree inventory to voxelize. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + resolution (Resolution3D | Unset): Voxel resolution for a 3D grid. + + `horizontal` applies to both x and y (fastfuels-core requires isotropic + horizontal resolution). `vertical` is independent. + bands (list[TreeBand] | Unset): Which output bands to produce. Defaults to `bulk_density.foliage.live`. + crown_profile_model (CrownProfileModel | Unset): Crown geometry models — which voxels a tree's crown occupies. + biomass_source (AllometryBiomassSource | InventoryColumnsBiomassSource | Unset): Biomass source and requested + biomass components. + max_crown_radius_source (AllometryMaxCrownRadiusSource | InventoryColumnMaxCrownRadiusSource | Unset): Source of + each tree's maximum crown radius. Defaults to the crown profile model's allometric value. Use `{"type": + "inventory_column", "column": ...}` to read a per-tree maximum crown radius (m) from an inventory column (e.g. + derived from LiDAR); the crown profile model still controls the crown shape — only the peak radius is rescaled. + moisture_model (MoistureModel | None | Unset): Live/dead fuel moisture model. Applied only when matching + fuel_moisture bands are requested. Live defaults to uniform 100.0; dead defaults to uniform 10.0. + seed (int | Unset): Random seed for reproducibility. Controls stochastic tree voxel sampling and biomass + distribution. Generated randomly if omitted; persisted on the grid document either way so re-running a grid + always yields bit-identical output. + """ + + source_inventory_id: str + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + resolution: Resolution3D | Unset = UNSET + bands: list[TreeBand] | Unset = UNSET + crown_profile_model: CrownProfileModel | Unset = UNSET + biomass_source: AllometryBiomassSource | InventoryColumnsBiomassSource | Unset = ( + UNSET + ) + max_crown_radius_source: ( + AllometryMaxCrownRadiusSource | InventoryColumnMaxCrownRadiusSource | Unset + ) = UNSET + moisture_model: MoistureModel | None | Unset = UNSET + seed: int | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.allometry_biomass_source import AllometryBiomassSource + from ..models.allometry_max_crown_radius_source import ( + AllometryMaxCrownRadiusSource, + ) + from ..models.moisture_model import MoistureModel + + source_inventory_id = self.source_inventory_id + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + resolution: dict[str, Any] | Unset = UNSET + if not isinstance(self.resolution, Unset): + resolution = self.resolution.to_dict() + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + crown_profile_model: str | Unset = UNSET + if not isinstance(self.crown_profile_model, Unset): + crown_profile_model = self.crown_profile_model.value + + biomass_source: dict[str, Any] | Unset + if isinstance(self.biomass_source, Unset): + biomass_source = UNSET + elif isinstance(self.biomass_source, AllometryBiomassSource): + biomass_source = self.biomass_source.to_dict() + else: + biomass_source = self.biomass_source.to_dict() + + max_crown_radius_source: dict[str, Any] | Unset + if isinstance(self.max_crown_radius_source, Unset): + max_crown_radius_source = UNSET + elif isinstance(self.max_crown_radius_source, AllometryMaxCrownRadiusSource): + max_crown_radius_source = self.max_crown_radius_source.to_dict() + else: + max_crown_radius_source = self.max_crown_radius_source.to_dict() + + moisture_model: dict[str, Any] | None | Unset + if isinstance(self.moisture_model, Unset): + moisture_model = UNSET + elif isinstance(self.moisture_model, MoistureModel): + moisture_model = self.moisture_model.to_dict() + else: + moisture_model = self.moisture_model + + seed = self.seed + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "source_inventory_id": source_inventory_id, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if resolution is not UNSET: + field_dict["resolution"] = resolution + if bands is not UNSET: + field_dict["bands"] = bands + if crown_profile_model is not UNSET: + field_dict["crown_profile_model"] = crown_profile_model + if biomass_source is not UNSET: + field_dict["biomass_source"] = biomass_source + if max_crown_radius_source is not UNSET: + field_dict["max_crown_radius_source"] = max_crown_radius_source + if moisture_model is not UNSET: + field_dict["moisture_model"] = moisture_model + if seed is not UNSET: + field_dict["seed"] = seed + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.allometry_biomass_source import AllometryBiomassSource + from ..models.allometry_max_crown_radius_source import ( + AllometryMaxCrownRadiusSource, + ) + from ..models.inventory_column_max_crown_radius_source import ( + InventoryColumnMaxCrownRadiusSource, + ) + from ..models.inventory_columns_biomass_source import ( + InventoryColumnsBiomassSource, + ) + from ..models.moisture_model import MoistureModel + from ..models.resolution_3d import Resolution3D + + d = dict(src_dict) + source_inventory_id = d.pop("source_inventory_id") + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _resolution = d.pop("resolution", UNSET) + resolution: Resolution3D | Unset + if isinstance(_resolution, Unset): + resolution = UNSET + else: + resolution = Resolution3D.from_dict(_resolution) + + _bands = d.pop("bands", UNSET) + bands: list[TreeBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = TreeBand(bands_item_data) + + bands.append(bands_item) + + _crown_profile_model = d.pop("crown_profile_model", UNSET) + crown_profile_model: CrownProfileModel | Unset + if isinstance(_crown_profile_model, Unset): + crown_profile_model = UNSET + else: + crown_profile_model = CrownProfileModel(_crown_profile_model) + + def _parse_biomass_source( + data: object, + ) -> AllometryBiomassSource | InventoryColumnsBiomassSource | Unset: + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + biomass_source_type_0 = AllometryBiomassSource.from_dict(data) + + return biomass_source_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + biomass_source_type_1 = InventoryColumnsBiomassSource.from_dict(data) + + return biomass_source_type_1 + + biomass_source = _parse_biomass_source(d.pop("biomass_source", UNSET)) + + def _parse_max_crown_radius_source( + data: object, + ) -> ( + AllometryMaxCrownRadiusSource | InventoryColumnMaxCrownRadiusSource | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + max_crown_radius_source_type_0 = ( + AllometryMaxCrownRadiusSource.from_dict(data) + ) + + return max_crown_radius_source_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + max_crown_radius_source_type_1 = ( + InventoryColumnMaxCrownRadiusSource.from_dict(data) + ) + + return max_crown_radius_source_type_1 + + max_crown_radius_source = _parse_max_crown_radius_source( + d.pop("max_crown_radius_source", UNSET) + ) + + def _parse_moisture_model(data: object) -> MoistureModel | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + moisture_model_type_0 = MoistureModel.from_dict(data) + + return moisture_model_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(MoistureModel | None | Unset, data) + + moisture_model = _parse_moisture_model(d.pop("moisture_model", UNSET)) + + seed = d.pop("seed", UNSET) + + create_tree_inventory_request = cls( + source_inventory_id=source_inventory_id, + name=name, + description=description, + tags=tags, + resolution=resolution, + bands=bands, + crown_profile_model=crown_profile_model, + biomass_source=biomass_source, + max_crown_radius_source=max_crown_radius_source, + moisture_model=moisture_model, + seed=seed, + ) + + return create_tree_inventory_request diff --git a/fastfuels_sdk/v2/client_library/models/create_tree_map_request.py b/fastfuels_sdk/v2/client_library/models/create_tree_map_request.py new file mode 100644 index 0000000..fa9a44d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_tree_map_request.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.tree_map_band import TreeMapBand +from ..models.tree_map_version import TreeMapVersion +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + +T = TypeVar("T", bound="CreateTreeMapRequest") + + +@_attrs_define +class CreateTreeMapRequest: + """Request to create a grid from TreeMap. + + Returns a grid with one or two categorical bands: + - tm_id: TreeMap raster pixel values (always available) + - plt_cn: FIA plot condition number (optional, derived from tree table) + + Attributes: + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + extent_buffer_cells (int | Unset): Number of result-grid cells included as a buffer around the domain extent in + the stored grid. The buffer is measured after the source raster is projected into the domain CRS, so a cell + means one cell in the returned grid rather than one source raster cell. Provides context for later operations + (resample, reproject, focal filters, derivative calculations) that are sensitive to edges. Default 0 adds no + buffer. Maximum: 10 cells. Default: 0. + alignment (GridAlignmentDomainTarget | GridAlignmentGridTarget | GridAlignmentNativeTarget | Unset): Per-fetch + alignment target. Default `target="domain"` anchors output cells to the domain origin so cross-source + composition works by construction. `target="native"` preserves the source pixel anchor. `target="grid"` aligns + to an existing grid by id. + version (TreeMapVersion | Unset): Available TreeMap data versions. + bands (list[TreeMapBand] | Unset): + """ + + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + extent_buffer_cells: int | Unset = 0 + alignment: ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ) = UNSET + version: TreeMapVersion | Unset = UNSET + bands: list[TreeMapBand] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + extent_buffer_cells = self.extent_buffer_cells + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, GridAlignmentDomainTarget) or isinstance( + self.alignment, GridAlignmentNativeTarget + ): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + version: str | Unset = UNSET + if not isinstance(self.version, Unset): + version = self.version.value + + bands: list[str] | Unset = UNSET + if not isinstance(self.bands, Unset): + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.value + bands.append(bands_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + if extent_buffer_cells is not UNSET: + field_dict["extent_buffer_cells"] = extent_buffer_cells + if alignment is not UNSET: + field_dict["alignment"] = alignment + if version is not UNSET: + field_dict["version"] = version + if bands is not UNSET: + field_dict["bands"] = bands + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_alignment_domain_target import GridAlignmentDomainTarget + from ..models.grid_alignment_grid_target import GridAlignmentGridTarget + from ..models.grid_alignment_native_target import GridAlignmentNativeTarget + from ..models.grid_modification import GridModification + + d = dict(src_dict) + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + extent_buffer_cells = d.pop("extent_buffer_cells", UNSET) + + def _parse_alignment( + data: object, + ) -> ( + GridAlignmentDomainTarget + | GridAlignmentGridTarget + | GridAlignmentNativeTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = GridAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = GridAlignmentNativeTarget.from_dict(data) + + return alignment_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_2 = GridAlignmentGridTarget.from_dict(data) + + return alignment_type_2 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + _version = d.pop("version", UNSET) + version: TreeMapVersion | Unset + if isinstance(_version, Unset): + version = UNSET + else: + version = TreeMapVersion(_version) + + _bands = d.pop("bands", UNSET) + bands: list[TreeMapBand] | Unset = UNSET + if _bands is not UNSET: + bands = [] + for bands_item_data in _bands: + bands_item = TreeMapBand(bands_item_data) + + bands.append(bands_item) + + create_tree_map_request = cls( + name=name, + description=description, + tags=tags, + modifications=modifications, + extent_buffer_cells=extent_buffer_cells, + alignment=alignment, + version=version, + bands=bands, + ) + + create_tree_map_request.additional_properties = d + return create_tree_map_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/create_uniform_request.py b/fastfuels_sdk/v2/client_library/models/create_uniform_request.py new file mode 100644 index 0000000..935a31c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/create_uniform_request.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_modification import GridModification + from ..models.uniform_band_input import UniformBandInput + + +T = TypeVar("T", bound="CreateUniformRequest") + + +@_attrs_define +class CreateUniformRequest: + """Request to create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the specified + resolution. No default resolution — it must be explicitly provided since + uniform grids have no "native resolution." + + Attributes: + resolution (float): Grid resolution in meters + bands (list[UniformBandInput]): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + modifications (list[GridModification] | Unset): Rules applied to the grid after it is built from its source. + Each rule has a list of `conditions` (ANDed together) and a list of `actions` (applied where the conditions + match). Conditions can be attribute-based (compare a band value) or spatial (test cell location against a + geometry). Spatial conditions come in two variants discriminated by `source`: `geometry` (inline GeoJSON) or + `feature` (reference a persisted Feature resource — road, water, layerset — in the same domain by `feature_id`). + Both spatial variants accept `buffer_m` (meters, applied in the domain's projected CRS) to widen the geometry, + and `target` (`centroid` or `cell`) to choose which part of the cell is tested. Actions modify band values via + `replace`, `multiply`, `divide`, `add`, or `subtract`. See the `GridModification` schema for the full field + reference and worked examples. + """ + + resolution: float + bands: list[UniformBandInput] + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + resolution = self.resolution + + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.to_dict() + bands.append(bands_item) + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "resolution": resolution, + "bands": bands, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if modifications is not UNSET: + field_dict["modifications"] = modifications + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_modification import GridModification + from ..models.uniform_band_input import UniformBandInput + + d = dict(src_dict) + resolution = d.pop("resolution") + + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = UniformBandInput.from_dict(bands_item_data) + + bands.append(bands_item) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + create_uniform_request = cls( + resolution=resolution, + bands=bands, + name=name, + description=description, + tags=tags, + modifications=modifications, + ) + + create_uniform_request.additional_properties = d + return create_uniform_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/crown_profile_model.py b/fastfuels_sdk/v2/client_library/models/crown_profile_model.py new file mode 100644 index 0000000..172454c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/crown_profile_model.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class CrownProfileModel(str, Enum): + BETA = "beta" + PURVES = "purves" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/dense_grid_data.py b/fastfuels_sdk/v2/client_library/models/dense_grid_data.py new file mode 100644 index 0000000..ddda21f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/dense_grid_data.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DenseGridData") + + +@_attrs_define +class DenseGridData: + """ + Attributes: + format_ (Literal['dense']): + values (list[float | int]): + """ + + format_: Literal["dense"] + values: list[float | int] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + format_ = self.format_ + + values = [] + for values_item_data in self.values: + values_item: float | int + values_item = values_item_data + values.append(values_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "format": format_, + "values": values, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + format_ = cast(Literal["dense"], d.pop("format")) + if format_ != "dense": + raise ValueError(f"format must match const 'dense', got '{format_}'") + + values = [] + _values = d.pop("values") + for values_item_data in _values: + + def _parse_values_item(data: object) -> float | int: + return cast(float | int, data) + + values_item = _parse_values_item(values_item_data) + + values.append(values_item) + + dense_grid_data = cls( + format_=format_, + values=values, + ) + + dense_grid_data.additional_properties = d + return dense_grid_data + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/distribution.py b/fastfuels_sdk/v2/client_library/models/distribution.py new file mode 100644 index 0000000..8d0ad42 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/distribution.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class Distribution(str, Enum): + HOMOGENEOUS = "homogeneous" + RANDOM_CLUSTERS = "random_clusters" + UNIFORM_RANDOM = "uniform_random" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/domain.py b/fastfuels_sdk/v2/client_library/models/domain.py new file mode 100644 index 0000000..3145ec2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/domain.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.domain_style import DomainStyle + from ..models.geo_json_crs import GeoJsonCRS + from ..models.geo_json_feature import GeoJsonFeature + + +T = TypeVar("T", bound="Domain") + + +@_attrs_define +class Domain: + """Represents a domain resource. + + Attributes: + type_ (Literal['FeatureCollection']): + features (list[GeoJsonFeature]): + bbox (list[float] | None | Unset): + name (str | Unset): The name of the domain. Default: ''. + description (str | Unset): A description of the domain. Default: ''. + crs (GeoJsonCRS | Unset): + tags (list[str] | None | Unset): A list of tags associated with the domain. + pad_to_resolution (float | None | Unset): Optional resolution in meters to snap the domain bounding box to. When + set, the bounding box (the 'domain' feature) is snapped outward to the nearest multiple of this value. Grids + whose resolutions divide evenly into this value will produce identical, aligned footprints on this domain. + style (DomainStyle | None | Unset): Optional visual style for rendering the domain on a map. + id (str | Unset): A unique identifier for the domain. + created_on (datetime.datetime | None | Unset): The date and time the domain was created. + modified_on (datetime.datetime | None | Unset): The date and time the domain was last modified. + """ + + type_: Literal["FeatureCollection"] + features: list[GeoJsonFeature] + bbox: list[float] | None | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + crs: GeoJsonCRS | Unset = UNSET + tags: list[str] | None | Unset = UNSET + pad_to_resolution: float | None | Unset = UNSET + style: DomainStyle | None | Unset = UNSET + id: str | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.domain_style import DomainStyle + + type_ = self.type_ + + features = [] + for features_item_data in self.features: + features_item = features_item_data.to_dict() + features.append(features_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + name = self.name + + description = self.description + + crs: dict[str, Any] | Unset = UNSET + if not isinstance(self.crs, Unset): + crs = self.crs.to_dict() + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + pad_to_resolution: float | None | Unset + if isinstance(self.pad_to_resolution, Unset): + pad_to_resolution = UNSET + else: + pad_to_resolution = self.pad_to_resolution + + style: dict[str, Any] | None | Unset + if isinstance(self.style, Unset): + style = UNSET + elif isinstance(self.style, DomainStyle): + style = self.style.to_dict() + else: + style = self.style + + id = self.id + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "features": features, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if crs is not UNSET: + field_dict["crs"] = crs + if tags is not UNSET: + field_dict["tags"] = tags + if pad_to_resolution is not UNSET: + field_dict["pad_to_resolution"] = pad_to_resolution + if style is not UNSET: + field_dict["style"] = style + if id is not UNSET: + field_dict["id"] = id + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.domain_style import DomainStyle + from ..models.geo_json_crs import GeoJsonCRS + from ..models.geo_json_feature import GeoJsonFeature + + d = dict(src_dict) + type_ = cast(Literal["FeatureCollection"], d.pop("type")) + if type_ != "FeatureCollection": + raise ValueError( + f"type must match const 'FeatureCollection', got '{type_}'" + ) + + features = [] + _features = d.pop("features") + for features_item_data in _features: + features_item = GeoJsonFeature.from_dict(features_item_data) + + features.append(features_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + _crs = d.pop("crs", UNSET) + crs: GeoJsonCRS | Unset + if isinstance(_crs, Unset): + crs = UNSET + else: + crs = GeoJsonCRS.from_dict(_crs) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + def _parse_pad_to_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + pad_to_resolution = _parse_pad_to_resolution(d.pop("pad_to_resolution", UNSET)) + + def _parse_style(data: object) -> DomainStyle | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + style_type_0 = DomainStyle.from_dict(data) + + return style_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DomainStyle | None | Unset, data) + + style = _parse_style(d.pop("style", UNSET)) + + id = d.pop("id", UNSET) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + domain = cls( + type_=type_, + features=features, + bbox=bbox, + name=name, + description=description, + crs=crs, + tags=tags, + pad_to_resolution=pad_to_resolution, + style=style, + id=id, + created_on=created_on, + modified_on=modified_on, + ) + + domain.additional_properties = d + return domain + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/domain_lattice.py b/fastfuels_sdk/v2/client_library/models/domain_lattice.py new file mode 100644 index 0000000..58ebc19 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/domain_lattice.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DomainLattice") + + +@_attrs_define +class DomainLattice: + """Pixel lattice for a domain at a given resolution. + + Uses rasterio conventions: + - ``transform`` is ``[a, b, c, d, e, f]`` where ``a`` is pixel width, + ``e`` is ``-pixel_height``, and ``(c, f)`` is the upper-left corner. + - ``shape`` is ``(height, width)`` in pixels. + + Attributes: + crs (str): e.g., 'EPSG:32611' + resolution (float): Pixel size in meters. + num_buffer_cells (int): Buffer cells applied on each side. + transform (list[float]): Affine transform [a, b, c, d, e, f] + shape (list[int]): (height, width) in pixels + """ + + crs: str + resolution: float + num_buffer_cells: int + transform: list[float] + shape: list[int] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + resolution = self.resolution + + num_buffer_cells = self.num_buffer_cells + + transform = [] + for transform_item_data in self.transform: + transform_item: float + transform_item = transform_item_data + transform.append(transform_item) + + shape = [] + for shape_item_data in self.shape: + shape_item: int + shape_item = shape_item_data + shape.append(shape_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "resolution": resolution, + "num_buffer_cells": num_buffer_cells, + "transform": transform, + "shape": shape, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + resolution = d.pop("resolution") + + num_buffer_cells = d.pop("num_buffer_cells") + + transform = [] + _transform = d.pop("transform") + for transform_item_data in _transform: + + def _parse_transform_item(data: object) -> float: + return cast(float, data) + + transform_item = _parse_transform_item(transform_item_data) + + transform.append(transform_item) + + shape = [] + _shape = d.pop("shape") + for shape_item_data in _shape: + + def _parse_shape_item(data: object) -> int: + return cast(int, data) + + shape_item = _parse_shape_item(shape_item_data) + + shape.append(shape_item) + + domain_lattice = cls( + crs=crs, + resolution=resolution, + num_buffer_cells=num_buffer_cells, + transform=transform, + shape=shape, + ) + + domain_lattice.additional_properties = d + return domain_lattice + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/domain_sort_field.py b/fastfuels_sdk/v2/client_library/models/domain_sort_field.py new file mode 100644 index 0000000..a52929b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/domain_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class DomainSortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/domain_sort_order.py b/fastfuels_sdk/v2/client_library/models/domain_sort_order.py new file mode 100644 index 0000000..06c3819 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/domain_sort_order.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class DomainSortOrder(str, Enum): + ASCENDING = "ascending" + DESCENDING = "descending" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/domain_style.py b/fastfuels_sdk/v2/client_library/models/domain_style.py new file mode 100644 index 0000000..003204b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/domain_style.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DomainStyle") + + +@_attrs_define +class DomainStyle: + """Optional visual style for rendering a domain on a map. + + All fields are optional. On PATCH only the provided fields are merged + into the stored style; unspecified fields preserve their current values. + + Attributes: + stroke_color (None | str | Unset): Stroke color in any renderer-supported format. + stroke_opacity (float | None | Unset): Stroke opacity in [0, 1]. + stroke_width (float | None | Unset): Stroke width in pixels (non-negative). + fill_color (None | str | Unset): Fill color in any renderer-supported format. + fill_opacity (float | None | Unset): Fill opacity in [0, 1]. + """ + + stroke_color: None | str | Unset = UNSET + stroke_opacity: float | None | Unset = UNSET + stroke_width: float | None | Unset = UNSET + fill_color: None | str | Unset = UNSET + fill_opacity: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + stroke_color: None | str | Unset + if isinstance(self.stroke_color, Unset): + stroke_color = UNSET + else: + stroke_color = self.stroke_color + + stroke_opacity: float | None | Unset + if isinstance(self.stroke_opacity, Unset): + stroke_opacity = UNSET + else: + stroke_opacity = self.stroke_opacity + + stroke_width: float | None | Unset + if isinstance(self.stroke_width, Unset): + stroke_width = UNSET + else: + stroke_width = self.stroke_width + + fill_color: None | str | Unset + if isinstance(self.fill_color, Unset): + fill_color = UNSET + else: + fill_color = self.fill_color + + fill_opacity: float | None | Unset + if isinstance(self.fill_opacity, Unset): + fill_opacity = UNSET + else: + fill_opacity = self.fill_opacity + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if stroke_color is not UNSET: + field_dict["stroke_color"] = stroke_color + if stroke_opacity is not UNSET: + field_dict["stroke_opacity"] = stroke_opacity + if stroke_width is not UNSET: + field_dict["stroke_width"] = stroke_width + if fill_color is not UNSET: + field_dict["fill_color"] = fill_color + if fill_opacity is not UNSET: + field_dict["fill_opacity"] = fill_opacity + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_stroke_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + stroke_color = _parse_stroke_color(d.pop("stroke_color", UNSET)) + + def _parse_stroke_opacity(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + stroke_opacity = _parse_stroke_opacity(d.pop("stroke_opacity", UNSET)) + + def _parse_stroke_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + stroke_width = _parse_stroke_width(d.pop("stroke_width", UNSET)) + + def _parse_fill_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + fill_color = _parse_fill_color(d.pop("fill_color", UNSET)) + + def _parse_fill_opacity(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + fill_opacity = _parse_fill_opacity(d.pop("fill_opacity", UNSET)) + + domain_style = cls( + stroke_color=stroke_color, + stroke_opacity=stroke_opacity, + stroke_width=stroke_width, + fill_color=fill_color, + fill_opacity=fill_opacity, + ) + + domain_style.additional_properties = d + return domain_style + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/duet_band.py b/fastfuels_sdk/v2/client_library/models/duet_band.py new file mode 100644 index 0000000..8f3b9af --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_band.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class DuetBand(str, Enum): + FUEL_DEPTH_GRASS = "fuel_depth.grass" + FUEL_DEPTH_LITTER = "fuel_depth.litter" + FUEL_DEPTH_LITTER_CONIFEROUS = "fuel_depth.litter.coniferous" + FUEL_DEPTH_LITTER_DECIDUOUS = "fuel_depth.litter.deciduous" + FUEL_DEPTH_TOTAL = "fuel_depth.total" + FUEL_LOAD_GRASS = "fuel_load.grass" + FUEL_LOAD_LITTER = "fuel_load.litter" + FUEL_LOAD_LITTER_CONIFEROUS = "fuel_load.litter.coniferous" + FUEL_LOAD_LITTER_DECIDUOUS = "fuel_load.litter.deciduous" + FUEL_LOAD_TOTAL = "fuel_load.total" + FUEL_MOISTURE_GRASS = "fuel_moisture.grass" + FUEL_MOISTURE_LITTER = "fuel_moisture.litter" + FUEL_MOISTURE_LITTER_CONIFEROUS = "fuel_moisture.litter.coniferous" + FUEL_MOISTURE_LITTER_DECIDUOUS = "fuel_moisture.litter.deciduous" + FUEL_MOISTURE_TOTAL = "fuel_moisture.total" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/duet_calibration.py b/fastfuels_sdk/v2/client_library/models/duet_calibration.py new file mode 100644 index 0000000..99f348d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_calibration.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.duet_parameter_calibration import DuetParameterCalibration + + +T = TypeVar("T", bound="DuetCalibration") + + +@_attrs_define +class DuetCalibration: + """Calibration targets, keyed by fuel parameter. + + Each parameter is calibrated independently; omitted parameters keep DUET's + raw values. + + Attributes: + fuel_load (DuetParameterCalibration | None | Unset): + fuel_depth (DuetParameterCalibration | None | Unset): + fuel_moisture (DuetParameterCalibration | None | Unset): + """ + + fuel_load: DuetParameterCalibration | None | Unset = UNSET + fuel_depth: DuetParameterCalibration | None | Unset = UNSET + fuel_moisture: DuetParameterCalibration | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.duet_parameter_calibration import DuetParameterCalibration + + fuel_load: dict[str, Any] | None | Unset + if isinstance(self.fuel_load, Unset): + fuel_load = UNSET + elif isinstance(self.fuel_load, DuetParameterCalibration): + fuel_load = self.fuel_load.to_dict() + else: + fuel_load = self.fuel_load + + fuel_depth: dict[str, Any] | None | Unset + if isinstance(self.fuel_depth, Unset): + fuel_depth = UNSET + elif isinstance(self.fuel_depth, DuetParameterCalibration): + fuel_depth = self.fuel_depth.to_dict() + else: + fuel_depth = self.fuel_depth + + fuel_moisture: dict[str, Any] | None | Unset + if isinstance(self.fuel_moisture, Unset): + fuel_moisture = UNSET + elif isinstance(self.fuel_moisture, DuetParameterCalibration): + fuel_moisture = self.fuel_moisture.to_dict() + else: + fuel_moisture = self.fuel_moisture + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if fuel_load is not UNSET: + field_dict["fuel_load"] = fuel_load + if fuel_depth is not UNSET: + field_dict["fuel_depth"] = fuel_depth + if fuel_moisture is not UNSET: + field_dict["fuel_moisture"] = fuel_moisture + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.duet_parameter_calibration import DuetParameterCalibration + + d = dict(src_dict) + + def _parse_fuel_load(data: object) -> DuetParameterCalibration | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fuel_load_type_0 = DuetParameterCalibration.from_dict(data) + + return fuel_load_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DuetParameterCalibration | None | Unset, data) + + fuel_load = _parse_fuel_load(d.pop("fuel_load", UNSET)) + + def _parse_fuel_depth(data: object) -> DuetParameterCalibration | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fuel_depth_type_0 = DuetParameterCalibration.from_dict(data) + + return fuel_depth_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DuetParameterCalibration | None | Unset, data) + + fuel_depth = _parse_fuel_depth(d.pop("fuel_depth", UNSET)) + + def _parse_fuel_moisture( + data: object, + ) -> DuetParameterCalibration | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fuel_moisture_type_0 = DuetParameterCalibration.from_dict(data) + + return fuel_moisture_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DuetParameterCalibration | None | Unset, data) + + fuel_moisture = _parse_fuel_moisture(d.pop("fuel_moisture", UNSET)) + + duet_calibration = cls( + fuel_load=fuel_load, + fuel_depth=fuel_depth, + fuel_moisture=fuel_moisture, + ) + + return duet_calibration diff --git a/fastfuels_sdk/v2/client_library/models/duet_constant_calibration_target.py b/fastfuels_sdk/v2/client_library/models/duet_constant_calibration_target.py new file mode 100644 index 0000000..7200b82 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_constant_calibration_target.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DuetConstantCalibrationTarget") + + +@_attrs_define +class DuetConstantCalibrationTarget: + """Assign a single value to every fuel-bearing cell. + + Reasonable only when that value is the only one available. + + Attributes: + value (float): Target value. + method (Literal['constant'] | Unset): Default: 'constant'. + """ + + value: float + method: Literal["constant"] | Unset = "constant" + + def to_dict(self) -> dict[str, Any]: + value = self.value + + method = self.method + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "value": value, + } + ) + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + value = d.pop("value") + + method = cast(Literal["constant"] | Unset, d.pop("method", UNSET)) + if method != "constant" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'constant', got '{method}'") + + duet_constant_calibration_target = cls( + value=value, + method=method, + ) + + return duet_constant_calibration_target diff --git a/fastfuels_sdk/v2/client_library/models/duet_max_min_calibration_target.py b/fastfuels_sdk/v2/client_library/models/duet_max_min_calibration_target.py new file mode 100644 index 0000000..b2d0af5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_max_min_calibration_target.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DuetMaxMinCalibrationTarget") + + +@_attrs_define +class DuetMaxMinCalibrationTarget: + """Rescale a fuel type to a target maximum and minimum. + + Best when fuel data are limited, or when their distribution does not + resemble DUET's. + + Attributes: + max_ (float): Target maximum. + method (Literal['maxmin'] | Unset): Default: 'maxmin'. + min_ (float | Unset): Target minimum. Default: 0.0. + """ + + max_: float + method: Literal["maxmin"] | Unset = "maxmin" + min_: float | Unset = 0.0 + + def to_dict(self) -> dict[str, Any]: + max_ = self.max_ + + method = self.method + + min_ = self.min_ + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "max": max_, + } + ) + if method is not UNSET: + field_dict["method"] = method + if min_ is not UNSET: + field_dict["min"] = min_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + max_ = d.pop("max") + + method = cast(Literal["maxmin"] | Unset, d.pop("method", UNSET)) + if method != "maxmin" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'maxmin', got '{method}'") + + min_ = d.pop("min", UNSET) + + duet_max_min_calibration_target = cls( + max_=max_, + method=method, + min_=min_, + ) + + return duet_max_min_calibration_target diff --git a/fastfuels_sdk/v2/client_library/models/duet_mean_sd_calibration_target.py b/fastfuels_sdk/v2/client_library/models/duet_mean_sd_calibration_target.py new file mode 100644 index 0000000..688cea2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_mean_sd_calibration_target.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DuetMeanSdCalibrationTarget") + + +@_attrs_define +class DuetMeanSdCalibrationTarget: + """Rescale a fuel type to a target mean and standard deviation. + + Appropriate only when the targets come from a dataset large enough to + approximate a normal distribution. + + Attributes: + mean (float): Target mean. + sd (float): Target standard deviation. + method (Literal['meansd'] | Unset): Default: 'meansd'. + """ + + mean: float + sd: float + method: Literal["meansd"] | Unset = "meansd" + + def to_dict(self) -> dict[str, Any]: + mean = self.mean + + sd = self.sd + + method = self.method + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "mean": mean, + "sd": sd, + } + ) + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + mean = d.pop("mean") + + sd = d.pop("sd") + + method = cast(Literal["meansd"] | Unset, d.pop("method", UNSET)) + if method != "meansd" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'meansd', got '{method}'") + + duet_mean_sd_calibration_target = cls( + mean=mean, + sd=sd, + method=method, + ) + + return duet_mean_sd_calibration_target diff --git a/fastfuels_sdk/v2/client_library/models/duet_parameter_calibration.py b/fastfuels_sdk/v2/client_library/models/duet_parameter_calibration.py new file mode 100644 index 0000000..5b98fac --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duet_parameter_calibration.py @@ -0,0 +1,415 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.duet_constant_calibration_target import DuetConstantCalibrationTarget + from ..models.duet_max_min_calibration_target import DuetMaxMinCalibrationTarget + from ..models.duet_mean_sd_calibration_target import DuetMeanSdCalibrationTarget + + +T = TypeVar("T", bound="DuetParameterCalibration") + + +@_attrs_define +class DuetParameterCalibration: + """Per-fuel-type calibration targets for one fuel parameter. + + `all` is exclusive: it calibrates every fuel type together and cannot be + combined with a per-type target. + + Attributes: + grass (DuetConstantCalibrationTarget | DuetMaxMinCalibrationTarget | DuetMeanSdCalibrationTarget | None | + Unset): + coniferous (DuetConstantCalibrationTarget | DuetMaxMinCalibrationTarget | DuetMeanSdCalibrationTarget | None | + Unset): + deciduous (DuetConstantCalibrationTarget | DuetMaxMinCalibrationTarget | DuetMeanSdCalibrationTarget | None | + Unset): + litter (DuetConstantCalibrationTarget | DuetMaxMinCalibrationTarget | DuetMeanSdCalibrationTarget | None | + Unset): + all_ (DuetConstantCalibrationTarget | DuetMaxMinCalibrationTarget | DuetMeanSdCalibrationTarget | None | Unset): + """ + + grass: ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ) = UNSET + coniferous: ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ) = UNSET + deciduous: ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ) = UNSET + litter: ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ) = UNSET + all_: ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ) = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.duet_constant_calibration_target import ( + DuetConstantCalibrationTarget, + ) + from ..models.duet_max_min_calibration_target import DuetMaxMinCalibrationTarget + from ..models.duet_mean_sd_calibration_target import DuetMeanSdCalibrationTarget + + grass: dict[str, Any] | None | Unset + if isinstance(self.grass, Unset): + grass = UNSET + elif ( + isinstance(self.grass, DuetMaxMinCalibrationTarget) + or isinstance(self.grass, DuetMeanSdCalibrationTarget) + or isinstance(self.grass, DuetConstantCalibrationTarget) + ): + grass = self.grass.to_dict() + else: + grass = self.grass + + coniferous: dict[str, Any] | None | Unset + if isinstance(self.coniferous, Unset): + coniferous = UNSET + elif ( + isinstance(self.coniferous, DuetMaxMinCalibrationTarget) + or isinstance(self.coniferous, DuetMeanSdCalibrationTarget) + or isinstance(self.coniferous, DuetConstantCalibrationTarget) + ): + coniferous = self.coniferous.to_dict() + else: + coniferous = self.coniferous + + deciduous: dict[str, Any] | None | Unset + if isinstance(self.deciduous, Unset): + deciduous = UNSET + elif ( + isinstance(self.deciduous, DuetMaxMinCalibrationTarget) + or isinstance(self.deciduous, DuetMeanSdCalibrationTarget) + or isinstance(self.deciduous, DuetConstantCalibrationTarget) + ): + deciduous = self.deciduous.to_dict() + else: + deciduous = self.deciduous + + litter: dict[str, Any] | None | Unset + if isinstance(self.litter, Unset): + litter = UNSET + elif ( + isinstance(self.litter, DuetMaxMinCalibrationTarget) + or isinstance(self.litter, DuetMeanSdCalibrationTarget) + or isinstance(self.litter, DuetConstantCalibrationTarget) + ): + litter = self.litter.to_dict() + else: + litter = self.litter + + all_: dict[str, Any] | None | Unset + if isinstance(self.all_, Unset): + all_ = UNSET + elif ( + isinstance(self.all_, DuetMaxMinCalibrationTarget) + or isinstance(self.all_, DuetMeanSdCalibrationTarget) + or isinstance(self.all_, DuetConstantCalibrationTarget) + ): + all_ = self.all_.to_dict() + else: + all_ = self.all_ + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if grass is not UNSET: + field_dict["grass"] = grass + if coniferous is not UNSET: + field_dict["coniferous"] = coniferous + if deciduous is not UNSET: + field_dict["deciduous"] = deciduous + if litter is not UNSET: + field_dict["litter"] = litter + if all_ is not UNSET: + field_dict["all"] = all_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.duet_constant_calibration_target import ( + DuetConstantCalibrationTarget, + ) + from ..models.duet_max_min_calibration_target import DuetMaxMinCalibrationTarget + from ..models.duet_mean_sd_calibration_target import DuetMeanSdCalibrationTarget + + d = dict(src_dict) + + def _parse_grass( + data: object, + ) -> ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + grass_type_0_type_0 = DuetMaxMinCalibrationTarget.from_dict(data) + + return grass_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + grass_type_0_type_1 = DuetMeanSdCalibrationTarget.from_dict(data) + + return grass_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + grass_type_0_type_2 = DuetConstantCalibrationTarget.from_dict(data) + + return grass_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset, + data, + ) + + grass = _parse_grass(d.pop("grass", UNSET)) + + def _parse_coniferous( + data: object, + ) -> ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + coniferous_type_0_type_0 = DuetMaxMinCalibrationTarget.from_dict(data) + + return coniferous_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + coniferous_type_0_type_1 = DuetMeanSdCalibrationTarget.from_dict(data) + + return coniferous_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + coniferous_type_0_type_2 = DuetConstantCalibrationTarget.from_dict(data) + + return coniferous_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset, + data, + ) + + coniferous = _parse_coniferous(d.pop("coniferous", UNSET)) + + def _parse_deciduous( + data: object, + ) -> ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + deciduous_type_0_type_0 = DuetMaxMinCalibrationTarget.from_dict(data) + + return deciduous_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + deciduous_type_0_type_1 = DuetMeanSdCalibrationTarget.from_dict(data) + + return deciduous_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + deciduous_type_0_type_2 = DuetConstantCalibrationTarget.from_dict(data) + + return deciduous_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset, + data, + ) + + deciduous = _parse_deciduous(d.pop("deciduous", UNSET)) + + def _parse_litter( + data: object, + ) -> ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + litter_type_0_type_0 = DuetMaxMinCalibrationTarget.from_dict(data) + + return litter_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + litter_type_0_type_1 = DuetMeanSdCalibrationTarget.from_dict(data) + + return litter_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + litter_type_0_type_2 = DuetConstantCalibrationTarget.from_dict(data) + + return litter_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset, + data, + ) + + litter = _parse_litter(d.pop("litter", UNSET)) + + def _parse_all_( + data: object, + ) -> ( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + all_type_0_type_0 = DuetMaxMinCalibrationTarget.from_dict(data) + + return all_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + all_type_0_type_1 = DuetMeanSdCalibrationTarget.from_dict(data) + + return all_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + all_type_0_type_2 = DuetConstantCalibrationTarget.from_dict(data) + + return all_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + DuetConstantCalibrationTarget + | DuetMaxMinCalibrationTarget + | DuetMeanSdCalibrationTarget + | None + | Unset, + data, + ) + + all_ = _parse_all_(d.pop("all", UNSET)) + + duet_parameter_calibration = cls( + grass=grass, + coniferous=coniferous, + deciduous=deciduous, + litter=litter, + all_=all_, + ) + + return duet_parameter_calibration diff --git a/fastfuels_sdk/v2/client_library/models/duplicate_grid_request.py b/fastfuels_sdk/v2/client_library/models/duplicate_grid_request.py new file mode 100644 index 0000000..f5193f7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duplicate_grid_request.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DuplicateGridRequest") + + +@_attrs_define +class DuplicateGridRequest: + """Optional metadata overrides for a duplicated grid. + + Every field is optional. Any field omitted is carried over verbatim from + the source grid. + + Attributes: + name (None | str | Unset): Name for the copy. Omit to reuse the source grid's name. + description (None | str | Unset): Description for the copy. Omit to reuse the source grid's description. + tags (list[str] | None | Unset): Tags for the copy. Omit to reuse the source grid's tags. + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + duplicate_grid_request = cls( + name=name, + description=description, + tags=tags, + ) + + duplicate_grid_request.additional_properties = d + return duplicate_grid_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/duplicate_inventory_request.py b/fastfuels_sdk/v2/client_library/models/duplicate_inventory_request.py new file mode 100644 index 0000000..9b4a1d0 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/duplicate_inventory_request.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DuplicateInventoryRequest") + + +@_attrs_define +class DuplicateInventoryRequest: + """Optional metadata overrides for a duplicated inventory. + + Every field is optional. Any field omitted is carried over verbatim from + the source inventory. + + Attributes: + name (None | str | Unset): Name for the copy. Omit to reuse the source inventory's name. + description (None | str | Unset): Description for the copy. Omit to reuse the source inventory's description. + tags (list[str] | None | Unset): Tags for the copy. Omit to reuse the source inventory's tags. + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + duplicate_inventory_request = cls( + name=name, + description=description, + tags=tags, + ) + + duplicate_inventory_request.additional_properties = d + return duplicate_inventory_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/export.py b/fastfuels_sdk/v2/client_library/models/export.py new file mode 100644 index 0000000..1d2c1aa --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/export.py @@ -0,0 +1,309 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.job_status import JobStatus +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.export_source import ExportSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + +T = TypeVar("T", bound="Export") + + +@_attrs_define +class Export: + """The Export resource. + + Exports are standalone artifacts that record provenance (domain_id, grid_id) + but have independent lifecycle — deleting a domain does not delete its exports. + + When status is "completed", signed_url contains a signed URL for + downloading the exported file. + + Attributes: + id (str): + domain_id (str): Domain the source grids belong to (provenance, not lifecycle dependency). + status (JobStatus): Status of an async job. + source (ExportSource): Format-specific export configuration. Contains 'name' (the export format) plus additional + fields depending on the source type. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + progress (JobProgress | None | Unset): Progress info when status is 'running'. Null otherwise. + created_on (datetime.datetime | None | Unset): + modified_on (datetime.datetime | None | Unset): + signed_url (None | str | Unset): Signed URL for downloading the exported file. Populated on completion. + expires_on (datetime.datetime | None | Unset): When the signed URL expires. + error (JobError | None | Unset): Error details if status is 'failed'. + tags (list[str] | Unset): + """ + + id: str + domain_id: str + status: JobStatus + source: ExportSource + name: str | Unset = "" + description: str | Unset = "" + progress: JobProgress | None | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + signed_url: None | str | Unset = UNSET + expires_on: datetime.datetime | None | Unset = UNSET + error: JobError | None | Unset = UNSET + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + id = self.id + + domain_id = self.domain_id + + status = self.status.value + + source = self.source.to_dict() + + name = self.name + + description = self.description + + progress: dict[str, Any] | None | Unset + if isinstance(self.progress, Unset): + progress = UNSET + elif isinstance(self.progress, JobProgress): + progress = self.progress.to_dict() + else: + progress = self.progress + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + signed_url: None | str | Unset + if isinstance(self.signed_url, Unset): + signed_url = UNSET + else: + signed_url = self.signed_url + + expires_on: None | str | Unset + if isinstance(self.expires_on, Unset): + expires_on = UNSET + elif isinstance(self.expires_on, datetime.datetime): + expires_on = self.expires_on.isoformat() + else: + expires_on = self.expires_on + + error: dict[str, Any] | None | Unset + if isinstance(self.error, Unset): + error = UNSET + elif isinstance(self.error, JobError): + error = self.error.to_dict() + else: + error = self.error + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "domain_id": domain_id, + "status": status, + "source": source, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if progress is not UNSET: + field_dict["progress"] = progress + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if signed_url is not UNSET: + field_dict["signed_url"] = signed_url + if expires_on is not UNSET: + field_dict["expires_on"] = expires_on + if error is not UNSET: + field_dict["error"] = error + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.export_source import ExportSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + d = dict(src_dict) + id = d.pop("id") + + domain_id = d.pop("domain_id") + + status = JobStatus(d.pop("status")) + + source = ExportSource.from_dict(d.pop("source")) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + def _parse_progress(data: object) -> JobProgress | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + progress_type_0 = JobProgress.from_dict(data) + + return progress_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobProgress | None | Unset, data) + + progress = _parse_progress(d.pop("progress", UNSET)) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + def _parse_signed_url(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + signed_url = _parse_signed_url(d.pop("signed_url", UNSET)) + + def _parse_expires_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + expires_on_type_0 = datetime.datetime.fromisoformat(data) + + return expires_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + expires_on = _parse_expires_on(d.pop("expires_on", UNSET)) + + def _parse_error(data: object) -> JobError | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + error_type_0 = JobError.from_dict(data) + + return error_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobError | None | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + tags = cast(list[str], d.pop("tags", UNSET)) + + export = cls( + id=id, + domain_id=domain_id, + status=status, + source=source, + name=name, + description=description, + progress=progress, + created_on=created_on, + modified_on=modified_on, + signed_url=signed_url, + expires_on=expires_on, + error=error, + tags=tags, + ) + + export.additional_properties = d + return export + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/export_grid_request.py b/fastfuels_sdk/v2/client_library/models/export_grid_request.py new file mode 100644 index 0000000..379d84b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/export_grid_request.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ExportGridRequest") + + +@_attrs_define +class ExportGridRequest: + """Request body for creating a grid export. + + Used at: POST /domains/{domain_id}/grids/{grid_id}/exports/{format} + + Attributes: + bands (list[str] | None | Unset): Band keys to include (e.g. 'fuel_load.1hr', 'fbfm'). Omit to export all bands + from the grid. + expiration_days (int | Unset): Number of days until the signed download URL expires (max 7). Default: 7. + Default: 7. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + bands: list[str] | None | Unset = UNSET + expiration_days: int | Unset = 7 + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + bands: list[str] | None | Unset + if isinstance(self.bands, Unset): + bands = UNSET + elif isinstance(self.bands, list): + bands = self.bands + + else: + bands = self.bands + + expiration_days = self.expiration_days + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if bands is not UNSET: + field_dict["bands"] = bands + if expiration_days is not UNSET: + field_dict["expiration_days"] = expiration_days + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_bands(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bands_type_0 = cast(list[str], data) + + return bands_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + bands = _parse_bands(d.pop("bands", UNSET)) + + expiration_days = d.pop("expiration_days", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + export_grid_request = cls( + bands=bands, + expiration_days=expiration_days, + name=name, + description=description, + tags=tags, + ) + + export_grid_request.additional_properties = d + return export_grid_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/export_inventory_request.py b/fastfuels_sdk/v2/client_library/models/export_inventory_request.py new file mode 100644 index 0000000..f9b96a4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/export_inventory_request.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ExportInventoryRequest") + + +@_attrs_define +class ExportInventoryRequest: + """Request body for creating an inventory export. + + Used at: POST /domains/{domain_id}/inventories/{inventory_id}/exports/{format} + + Attributes: + columns (list[str] | None | Unset): Column keys to include in the export (e.g. 'x', 'dbh', 'height'). Omit to + export all columns. Maximum 100 columns. + expiration_days (int | Unset): Number of days until the signed download URL expires (max 7). Default: 7. + Default: 7. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + columns: list[str] | None | Unset = UNSET + expiration_days: int | Unset = 7 + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + columns: list[str] | None | Unset + if isinstance(self.columns, Unset): + columns = UNSET + elif isinstance(self.columns, list): + columns = self.columns + + else: + columns = self.columns + + expiration_days = self.expiration_days + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if columns is not UNSET: + field_dict["columns"] = columns + if expiration_days is not UNSET: + field_dict["expiration_days"] = expiration_days + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_columns(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + columns_type_0 = cast(list[str], data) + + return columns_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + columns = _parse_columns(d.pop("columns", UNSET)) + + expiration_days = d.pop("expiration_days", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + export_inventory_request = cls( + columns=columns, + expiration_days=expiration_days, + name=name, + description=description, + tags=tags, + ) + + export_inventory_request.additional_properties = d + return export_inventory_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/export_sort_field.py b/fastfuels_sdk/v2/client_library/models/export_sort_field.py new file mode 100644 index 0000000..597233e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/export_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ExportSortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/export_source.py b/fastfuels_sdk/v2/client_library/models/export_source.py new file mode 100644 index 0000000..a0e9ca1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/export_source.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ExportSource") + + +@_attrs_define +class ExportSource: + """Format-specific export configuration. Contains 'name' (the export format) plus additional fields depending on the + source type. + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + export_source = cls() + + export_source.additional_properties = d + return export_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/fbfm_13_lookup_band.py b/fastfuels_sdk/v2/client_library/models/fbfm_13_lookup_band.py new file mode 100644 index 0000000..513bc23 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/fbfm_13_lookup_band.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class Fbfm13LookupBand(str, Enum): + FUEL_DEPTH = "fuel_depth" + FUEL_LOAD_100HR = "fuel_load.100hr" + FUEL_LOAD_10HR = "fuel_load.10hr" + FUEL_LOAD_1HR = "fuel_load.1hr" + FUEL_LOAD_LIVE_FOLIAGE = "fuel_load.live_foliage" + SAVR_100HR = "savr.100hr" + SAVR_10HR = "savr.10hr" + SAVR_1HR = "savr.1hr" + SAVR_LIVE_FOLIAGE = "savr.live_foliage" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/fbfm_40_lookup_band.py b/fastfuels_sdk/v2/client_library/models/fbfm_40_lookup_band.py new file mode 100644 index 0000000..8aee452 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/fbfm_40_lookup_band.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class Fbfm40LookupBand(str, Enum): + FUEL_DEPTH = "fuel_depth" + FUEL_LOAD_100HR = "fuel_load.100hr" + FUEL_LOAD_10HR = "fuel_load.10hr" + FUEL_LOAD_1HR = "fuel_load.1hr" + FUEL_LOAD_LIVE_HERB = "fuel_load.live_herb" + FUEL_LOAD_LIVE_WOODY = "fuel_load.live_woody" + SAVR_100HR = "savr.100hr" + SAVR_10HR = "savr.10hr" + SAVR_1HR = "savr.1hr" + SAVR_LIVE_HERB = "savr.live_herb" + SAVR_LIVE_WOODY = "savr.live_woody" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/fccs_lookup_band.py b/fastfuels_sdk/v2/client_library/models/fccs_lookup_band.py new file mode 100644 index 0000000..5aef201 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/fccs_lookup_band.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class FccsLookupBand(str, Enum): + DUFF_DEPTH = "duff_depth" + FUEL_LOAD_1000HR_ROTTEN = "fuel_load.1000hr_rotten" + FUEL_LOAD_1000HR_SOUND = "fuel_load.1000hr_sound" + FUEL_LOAD_100HR = "fuel_load.100hr" + FUEL_LOAD_10HR = "fuel_load.10hr" + FUEL_LOAD_1HR = "fuel_load.1hr" + FUEL_LOAD_DUFF = "fuel_load.duff" + FUEL_LOAD_LITTER = "fuel_load.litter" + FUEL_LOAD_LIVE_BRANCH = "fuel_load.live_branch" + FUEL_LOAD_LIVE_FOLIAGE = "fuel_load.live_foliage" + FUEL_LOAD_LIVE_HERB = "fuel_load.live_herb" + FUEL_LOAD_LIVE_SHRUB = "fuel_load.live_shrub" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/feature.py b/fastfuels_sdk/v2/client_library/models/feature.py new file mode 100644 index 0000000..5645e31 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.feature_type import FeatureType +from ..models.job_status import JobStatus +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.feature_georeference import FeatureGeoreference + from ..models.feature_source import FeatureSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + +T = TypeVar("T", bound="Feature") + + +@_attrs_define +class Feature: + """The Feature resource. + + When status is "pending" or "running", georeference will be null. + The backend worker populates it after successfully generating the GeoJSON + and uploading it to GCS, at which point status transitions to "completed". + + Attributes: + id (str): + domain_id (str): + type_ (FeatureType): Type of geographic feature. + status (JobStatus): Status of an async job. + source (FeatureSource): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + progress (JobProgress | None | Unset): Progress info when status is 'running'. Null otherwise. + created_on (datetime.datetime | None | Unset): + modified_on (datetime.datetime | None | Unset): + georeference (FeatureGeoreference | None | Unset): Spatial reference. Null until backend completes processing. + error (JobError | None | Unset): Error details if status is 'failed'. + tags (list[str] | Unset): + """ + + id: str + domain_id: str + type_: FeatureType + status: JobStatus + source: FeatureSource + name: str | Unset = "" + description: str | Unset = "" + progress: JobProgress | None | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + georeference: FeatureGeoreference | None | Unset = UNSET + error: JobError | None | Unset = UNSET + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.feature_georeference import FeatureGeoreference + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + id = self.id + + domain_id = self.domain_id + + type_ = self.type_.value + + status = self.status.value + + source = self.source.to_dict() + + name = self.name + + description = self.description + + progress: dict[str, Any] | None | Unset + if isinstance(self.progress, Unset): + progress = UNSET + elif isinstance(self.progress, JobProgress): + progress = self.progress.to_dict() + else: + progress = self.progress + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + georeference: dict[str, Any] | None | Unset + if isinstance(self.georeference, Unset): + georeference = UNSET + elif isinstance(self.georeference, FeatureGeoreference): + georeference = self.georeference.to_dict() + else: + georeference = self.georeference + + error: dict[str, Any] | None | Unset + if isinstance(self.error, Unset): + error = UNSET + elif isinstance(self.error, JobError): + error = self.error.to_dict() + else: + error = self.error + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "domain_id": domain_id, + "type": type_, + "status": status, + "source": source, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if progress is not UNSET: + field_dict["progress"] = progress + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if georeference is not UNSET: + field_dict["georeference"] = georeference + if error is not UNSET: + field_dict["error"] = error + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.feature_georeference import FeatureGeoreference + from ..models.feature_source import FeatureSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + d = dict(src_dict) + id = d.pop("id") + + domain_id = d.pop("domain_id") + + type_ = FeatureType(d.pop("type")) + + status = JobStatus(d.pop("status")) + + source = FeatureSource.from_dict(d.pop("source")) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + def _parse_progress(data: object) -> JobProgress | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + progress_type_0 = JobProgress.from_dict(data) + + return progress_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobProgress | None | Unset, data) + + progress = _parse_progress(d.pop("progress", UNSET)) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + def _parse_georeference(data: object) -> FeatureGeoreference | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + georeference_type_0 = FeatureGeoreference.from_dict(data) + + return georeference_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FeatureGeoreference | None | Unset, data) + + georeference = _parse_georeference(d.pop("georeference", UNSET)) + + def _parse_error(data: object) -> JobError | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + error_type_0 = JobError.from_dict(data) + + return error_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobError | None | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + tags = cast(list[str], d.pop("tags", UNSET)) + + feature = cls( + id=id, + domain_id=domain_id, + type_=type_, + status=status, + source=source, + name=name, + description=description, + progress=progress, + created_on=created_on, + modified_on=modified_on, + georeference=georeference, + error=error, + tags=tags, + ) + + feature.additional_properties = d + return feature + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/feature_data_metadata.py b/fastfuels_sdk/v2/client_library/models/feature_data_metadata.py new file mode 100644 index 0000000..322906e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_data_metadata.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.feature_partition_info import FeaturePartitionInfo + + +T = TypeVar("T", bound="FeatureDataMetadata") + + +@_attrs_define +class FeatureDataMetadata: + """Partition layout for a feature blob. + + Attributes: + total_features (int): Total number of features across all partitions. + partition_count (int): Number of valid `partition_index` values. Iterate from 0 to `partition_count - 1` to + retrieve every feature exactly once. + partitions (list[FeaturePartitionInfo]): Per-partition row counts read from the GeoParquet footer. Useful for + sizing client-side buffers. + """ + + total_features: int + partition_count: int + partitions: list[FeaturePartitionInfo] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + total_features = self.total_features + + partition_count = self.partition_count + + partitions = [] + for partitions_item_data in self.partitions: + partitions_item = partitions_item_data.to_dict() + partitions.append(partitions_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "total_features": total_features, + "partition_count": partition_count, + "partitions": partitions, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.feature_partition_info import FeaturePartitionInfo + + d = dict(src_dict) + total_features = d.pop("total_features") + + partition_count = d.pop("partition_count") + + partitions = [] + _partitions = d.pop("partitions") + for partitions_item_data in _partitions: + partitions_item = FeaturePartitionInfo.from_dict(partitions_item_data) + + partitions.append(partitions_item) + + feature_data_metadata = cls( + total_features=total_features, + partition_count=partition_count, + partitions=partitions, + ) + + feature_data_metadata.additional_properties = d + return feature_data_metadata + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/feature_georeference.py b/fastfuels_sdk/v2/client_library/models/feature_georeference.py new file mode 100644 index 0000000..b398998 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_georeference.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="FeatureGeoreference") + + +@_attrs_define +class FeatureGeoreference: + """Spatial reference for a feature, computed from the domain geometry. + + Attributes: + crs (str): + bounds (list[float]): + """ + + crs: str + bounds: list[float] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + bounds = [] + for bounds_item_data in self.bounds: + bounds_item: float + bounds_item = bounds_item_data + bounds.append(bounds_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "bounds": bounds, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + bounds = [] + _bounds = d.pop("bounds") + for bounds_item_data in _bounds: + + def _parse_bounds_item(data: object) -> float: + return cast(float, data) + + bounds_item = _parse_bounds_item(bounds_item_data) + + bounds.append(bounds_item) + + feature_georeference = cls( + crs=crs, + bounds=bounds, + ) + + feature_georeference.additional_properties = d + return feature_georeference + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/feature_partition_info.py b/fastfuels_sdk/v2/client_library/models/feature_partition_info.py new file mode 100644 index 0000000..5de7399 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_partition_info.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="FeaturePartitionInfo") + + +@_attrs_define +class FeaturePartitionInfo: + """One row group's feature count, surfaced via ``/data/metadata``. + + Attributes: + index (int): Zero-based partition index. + num_features (int): Number of features in this partition. + """ + + index: int + num_features: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + index = self.index + + num_features = self.num_features + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "index": index, + "num_features": num_features, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + index = d.pop("index") + + num_features = d.pop("num_features") + + feature_partition_info = cls( + index=index, + num_features=num_features, + ) + + feature_partition_info.additional_properties = d + return feature_partition_info + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/feature_sort_field.py b/fastfuels_sdk/v2/client_library/models/feature_sort_field.py new file mode 100644 index 0000000..8f8b98d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class FeatureSortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/feature_source.py b/fastfuels_sdk/v2/client_library/models/feature_source.py new file mode 100644 index 0000000..7c79b13 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_source.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="FeatureSource") + + +@_attrs_define +class FeatureSource: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + feature_source = cls() + + feature_source.additional_properties = d + return feature_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/feature_type.py b/fastfuels_sdk/v2/client_library/models/feature_type.py new file mode 100644 index 0000000..551898b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/feature_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class FeatureType(str, Enum): + LAYERSET = "layerset" + ROAD = "road" + WATER = "water" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/fia_species_group_share.py b/fastfuels_sdk/v2/client_library/models/fia_species_group_share.py new file mode 100644 index 0000000..17a5b7f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/fia_species_group_share.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="FIASpeciesGroupShare") + + +@_attrs_define +class FIASpeciesGroupShare: + """Basal area share for a single FIA species group. + + Attributes: + spgrpcd (int): FIA Species Group Code (SPGRPCD). + name (str): Common group name, e.g. 'Douglas-fir'. + basal_area_share (float): Fraction of total stand basal area, 0..1. + """ + + spgrpcd: int + name: str + basal_area_share: float + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + spgrpcd = self.spgrpcd + + name = self.name + + basal_area_share = self.basal_area_share + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "spgrpcd": spgrpcd, + "name": name, + "basal_area_share": basal_area_share, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + spgrpcd = d.pop("spgrpcd") + + name = d.pop("name") + + basal_area_share = d.pop("basal_area_share") + + fia_species_group_share = cls( + spgrpcd=spgrpcd, + name=name, + basal_area_share=basal_area_share, + ) + + fia_species_group_share.additional_properties = d + return fia_species_group_share + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/field_source.py b/fastfuels_sdk/v2/client_library/models/field_source.py new file mode 100644 index 0000000..a5aa117 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/field_source.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="FieldSource") + + +@_attrs_define +class FieldSource: + """A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + + Attributes: + grid_id (str): Grid containing the source band. + band (str): Band key on that grid (e.g. 'fuel_load.1hr'). + """ + + grid_id: str + band: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + grid_id = self.grid_id + + band = self.band + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "grid_id": grid_id, + "band": band, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_id = d.pop("grid_id") + + band = d.pop("band") + + field_source = cls( + grid_id=grid_id, + band=band, + ) + + field_source.additional_properties = d + return field_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/fine_biomass_config.py b/fastfuels_sdk/v2/client_library/models/fine_biomass_config.py new file mode 100644 index 0000000..120a5f5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/fine_biomass_config.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="FineBiomassConfig") + + +@_attrs_define +class FineBiomassConfig: + """Configuration for derived fine biomass. + + Attributes: + recipe (Literal['foliage_plus_branchwood_fraction']): + branchwood_fraction (float): + """ + + recipe: Literal["foliage_plus_branchwood_fraction"] + branchwood_fraction: float + + def to_dict(self) -> dict[str, Any]: + recipe = self.recipe + + branchwood_fraction = self.branchwood_fraction + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "recipe": recipe, + "branchwood_fraction": branchwood_fraction, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + recipe = cast(Literal["foliage_plus_branchwood_fraction"], d.pop("recipe")) + if recipe != "foliage_plus_branchwood_fraction": + raise ValueError( + f"recipe must match const 'foliage_plus_branchwood_fraction', got '{recipe}'" + ) + + branchwood_fraction = d.pop("branchwood_fraction") + + fine_biomass_config = cls( + recipe=recipe, + branchwood_fraction=branchwood_fraction, + ) + + return fine_biomass_config diff --git a/fastfuels_sdk/v2/client_library/models/geo_json_crs.py b/fastfuels_sdk/v2/client_library/models/geo_json_crs.py new file mode 100644 index 0000000..89e9dfc --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geo_json_crs.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.geo_json_crs_properties import GeoJsonCRSProperties + + +T = TypeVar("T", bound="GeoJsonCRS") + + +@_attrs_define +class GeoJsonCRS: + """ + Attributes: + properties (GeoJsonCRSProperties): + type_ (Literal['name'] | Unset): Default: 'name'. + """ + + properties: GeoJsonCRSProperties + type_: Literal["name"] | Unset = "name" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + properties = self.properties.to_dict() + + type_ = self.type_ + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "properties": properties, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.geo_json_crs_properties import GeoJsonCRSProperties + + d = dict(src_dict) + properties = GeoJsonCRSProperties.from_dict(d.pop("properties")) + + type_ = cast(Literal["name"] | Unset, d.pop("type", UNSET)) + if type_ != "name" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'name', got '{type_}'") + + geo_json_crs = cls( + properties=properties, + type_=type_, + ) + + geo_json_crs.additional_properties = d + return geo_json_crs + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/geo_json_crs_properties.py b/fastfuels_sdk/v2/client_library/models/geo_json_crs_properties.py new file mode 100644 index 0000000..7c682a2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geo_json_crs_properties.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GeoJsonCRSProperties") + + +@_attrs_define +class GeoJsonCRSProperties: + """ + Attributes: + name (str | Unset): Default: 'EPSG:4326'. + """ + + name: str | Unset = "EPSG:4326" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name", UNSET) + + geo_json_crs_properties = cls( + name=name, + ) + + geo_json_crs_properties.additional_properties = d + return geo_json_crs_properties + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/geo_json_feature.py b/fastfuels_sdk/v2/client_library/models/geo_json_feature.py new file mode 100644 index 0000000..5f569e0 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geo_json_feature.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.base_model import BaseModel + from ..models.geo_json_feature_properties_type_0 import ( + GeoJsonFeaturePropertiesType0, + ) + from ..models.geometry_collection import GeometryCollection + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + +T = TypeVar("T", bound="GeoJsonFeature") + + +@_attrs_define +class GeoJsonFeature: + """Generic GeoJSON feature with a generator-safe OpenAPI title. + + Attributes: + type_ (Literal['Feature']): + geometry (GeometryCollection | LineString | MultiLineString | MultiPoint | MultiPolygon | None | Point | + Polygon): + properties (BaseModel | GeoJsonFeaturePropertiesType0 | None): + bbox (list[float] | None | Unset): + id (int | None | str | Unset): + """ + + type_: Literal["Feature"] + geometry: ( + GeometryCollection + | LineString + | MultiLineString + | MultiPoint + | MultiPolygon + | None + | Point + | Polygon + ) + properties: BaseModel | GeoJsonFeaturePropertiesType0 | None + bbox: list[float] | None | Unset = UNSET + id: int | None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.base_model import BaseModel + from ..models.geo_json_feature_properties_type_0 import ( + GeoJsonFeaturePropertiesType0, + ) + from ..models.geometry_collection import GeometryCollection + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + type_ = self.type_ + + geometry: dict[str, Any] | None + if ( + isinstance(self.geometry, Point) + or isinstance(self.geometry, MultiPoint) + or isinstance(self.geometry, LineString) + or isinstance(self.geometry, MultiLineString) + or isinstance(self.geometry, Polygon) + or isinstance(self.geometry, MultiPolygon) + or isinstance(self.geometry, GeometryCollection) + ): + geometry = self.geometry.to_dict() + else: + geometry = self.geometry + + properties: dict[str, Any] | None + if isinstance(self.properties, GeoJsonFeaturePropertiesType0) or isinstance( + self.properties, BaseModel + ): + properties = self.properties.to_dict() + else: + properties = self.properties + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + id: int | None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "geometry": geometry, + "properties": properties, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + if id is not UNSET: + field_dict["id"] = id + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.base_model import BaseModel + from ..models.geo_json_feature_properties_type_0 import ( + GeoJsonFeaturePropertiesType0, + ) + from ..models.geometry_collection import GeometryCollection + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + d = dict(src_dict) + type_ = cast(Literal["Feature"], d.pop("type")) + if type_ != "Feature": + raise ValueError(f"type must match const 'Feature', got '{type_}'") + + def _parse_geometry( + data: object, + ) -> ( + GeometryCollection + | LineString + | MultiLineString + | MultiPoint + | MultiPolygon + | None + | Point + | Polygon + ): + if data is None: + return data + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_0 = Point.from_dict(data) + + return geometry_type_0_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_1 = MultiPoint.from_dict(data) + + return geometry_type_0_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_2 = LineString.from_dict(data) + + return geometry_type_0_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_3 = MultiLineString.from_dict(data) + + return geometry_type_0_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_4 = Polygon.from_dict(data) + + return geometry_type_0_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_5 = MultiPolygon.from_dict(data) + + return geometry_type_0_type_5 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0_type_6 = GeometryCollection.from_dict(data) + + return geometry_type_0_type_6 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + GeometryCollection + | LineString + | MultiLineString + | MultiPoint + | MultiPolygon + | None + | Point + | Polygon, + data, + ) + + geometry = _parse_geometry(d.pop("geometry")) + + def _parse_properties( + data: object, + ) -> BaseModel | GeoJsonFeaturePropertiesType0 | None: + if data is None: + return data + try: + if not isinstance(data, dict): + raise TypeError() + properties_type_0 = GeoJsonFeaturePropertiesType0.from_dict(data) + + return properties_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + properties_type_1 = BaseModel.from_dict(data) + + return properties_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(BaseModel | GeoJsonFeaturePropertiesType0 | None, data) + + properties = _parse_properties(d.pop("properties")) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + def _parse_id(data: object) -> int | None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + geo_json_feature = cls( + type_=type_, + geometry=geometry, + properties=properties, + bbox=bbox, + id=id, + ) + + geo_json_feature.additional_properties = d + return geo_json_feature + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/geo_json_feature_collection.py b/fastfuels_sdk/v2/client_library/models/geo_json_feature_collection.py new file mode 100644 index 0000000..0f1039b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geo_json_feature_collection.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.domain_style import DomainStyle + from ..models.geo_json_crs import GeoJsonCRS + from ..models.geo_json_feature import GeoJsonFeature + + +T = TypeVar("T", bound="GeoJsonFeatureCollection") + + +@_attrs_define +class GeoJsonFeatureCollection: + """ + Attributes: + type_ (Literal['FeatureCollection']): + features (list[GeoJsonFeature]): + bbox (list[float] | None | Unset): + name (str | Unset): The name of the domain. Default: ''. + description (str | Unset): A description of the domain. Default: ''. + crs (GeoJsonCRS | Unset): + tags (list[str] | None | Unset): A list of tags associated with the domain. + pad_to_resolution (float | None | Unset): Optional resolution in meters to snap the domain bounding box to. When + set, the bounding box (the 'domain' feature) is snapped outward to the nearest multiple of this value. Grids + whose resolutions divide evenly into this value will produce identical, aligned footprints on this domain. + style (DomainStyle | None | Unset): Optional visual style for rendering the domain on a map. + """ + + type_: Literal["FeatureCollection"] + features: list[GeoJsonFeature] + bbox: list[float] | None | Unset = UNSET + name: str | Unset = "" + description: str | Unset = "" + crs: GeoJsonCRS | Unset = UNSET + tags: list[str] | None | Unset = UNSET + pad_to_resolution: float | None | Unset = UNSET + style: DomainStyle | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.domain_style import DomainStyle + + type_ = self.type_ + + features = [] + for features_item_data in self.features: + features_item = features_item_data.to_dict() + features.append(features_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + name = self.name + + description = self.description + + crs: dict[str, Any] | Unset = UNSET + if not isinstance(self.crs, Unset): + crs = self.crs.to_dict() + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + pad_to_resolution: float | None | Unset + if isinstance(self.pad_to_resolution, Unset): + pad_to_resolution = UNSET + else: + pad_to_resolution = self.pad_to_resolution + + style: dict[str, Any] | None | Unset + if isinstance(self.style, Unset): + style = UNSET + elif isinstance(self.style, DomainStyle): + style = self.style.to_dict() + else: + style = self.style + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "features": features, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if crs is not UNSET: + field_dict["crs"] = crs + if tags is not UNSET: + field_dict["tags"] = tags + if pad_to_resolution is not UNSET: + field_dict["pad_to_resolution"] = pad_to_resolution + if style is not UNSET: + field_dict["style"] = style + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.domain_style import DomainStyle + from ..models.geo_json_crs import GeoJsonCRS + from ..models.geo_json_feature import GeoJsonFeature + + d = dict(src_dict) + type_ = cast(Literal["FeatureCollection"], d.pop("type")) + if type_ != "FeatureCollection": + raise ValueError( + f"type must match const 'FeatureCollection', got '{type_}'" + ) + + features = [] + _features = d.pop("features") + for features_item_data in _features: + features_item = GeoJsonFeature.from_dict(features_item_data) + + features.append(features_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + _crs = d.pop("crs", UNSET) + crs: GeoJsonCRS | Unset + if isinstance(_crs, Unset): + crs = UNSET + else: + crs = GeoJsonCRS.from_dict(_crs) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + def _parse_pad_to_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + pad_to_resolution = _parse_pad_to_resolution(d.pop("pad_to_resolution", UNSET)) + + def _parse_style(data: object) -> DomainStyle | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + style_type_0 = DomainStyle.from_dict(data) + + return style_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DomainStyle | None | Unset, data) + + style = _parse_style(d.pop("style", UNSET)) + + geo_json_feature_collection = cls( + type_=type_, + features=features, + bbox=bbox, + name=name, + description=description, + crs=crs, + tags=tags, + pad_to_resolution=pad_to_resolution, + style=style, + ) + + geo_json_feature_collection.additional_properties = d + return geo_json_feature_collection + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/geo_json_feature_properties_type_0.py b/fastfuels_sdk/v2/client_library/models/geo_json_feature_properties_type_0.py new file mode 100644 index 0000000..ec84fbe --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geo_json_feature_properties_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GeoJsonFeaturePropertiesType0") + + +@_attrs_define +class GeoJsonFeaturePropertiesType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + geo_json_feature_properties_type_0 = cls() + + geo_json_feature_properties_type_0.additional_properties = d + return geo_json_feature_properties_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/geometry_collection.py b/fastfuels_sdk/v2/client_library/models/geometry_collection.py new file mode 100644 index 0000000..fb1975b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/geometry_collection.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + +T = TypeVar("T", bound="GeometryCollection") + + +@_attrs_define +class GeometryCollection: + """GeometryCollection Model + + Attributes: + type_ (Literal['GeometryCollection']): + geometries (list[GeometryCollection | LineString | MultiLineString | MultiPoint | MultiPolygon | Point | + Polygon]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["GeometryCollection"] + geometries: list[ + GeometryCollection + | LineString + | MultiLineString + | MultiPoint + | MultiPolygon + | Point + | Polygon + ] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + type_ = self.type_ + + geometries = [] + for geometries_item_data in self.geometries: + geometries_item: dict[str, Any] + if ( + isinstance(geometries_item_data, Point) + or isinstance(geometries_item_data, MultiPoint) + or isinstance(geometries_item_data, LineString) + or isinstance(geometries_item_data, MultiLineString) + or isinstance(geometries_item_data, Polygon) + or isinstance(geometries_item_data, MultiPolygon) + ): + geometries_item = geometries_item_data.to_dict() + else: + geometries_item = geometries_item_data.to_dict() + + geometries.append(geometries_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "geometries": geometries, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.line_string import LineString + from ..models.multi_line_string import MultiLineString + from ..models.multi_point import MultiPoint + from ..models.multi_polygon import MultiPolygon + from ..models.point import Point + from ..models.polygon import Polygon + + d = dict(src_dict) + type_ = cast(Literal["GeometryCollection"], d.pop("type")) + if type_ != "GeometryCollection": + raise ValueError( + f"type must match const 'GeometryCollection', got '{type_}'" + ) + + geometries = [] + _geometries = d.pop("geometries") + for geometries_item_data in _geometries: + + def _parse_geometries_item( + data: object, + ) -> ( + GeometryCollection + | LineString + | MultiLineString + | MultiPoint + | MultiPolygon + | Point + | Polygon + ): + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_0 = Point.from_dict(data) + + return geometries_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_1 = MultiPoint.from_dict(data) + + return geometries_item_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_2 = LineString.from_dict(data) + + return geometries_item_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_3 = MultiLineString.from_dict(data) + + return geometries_item_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_4 = Polygon.from_dict(data) + + return geometries_item_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_5 = MultiPolygon.from_dict(data) + + return geometries_item_type_5 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + geometries_item_type_6 = GeometryCollection.from_dict(data) + + return geometries_item_type_6 + + geometries_item = _parse_geometries_item(geometries_item_data) + + geometries.append(geometries_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + geometry_collection = cls( + type_=type_, + geometries=geometries, + bbox=bbox, + ) + + geometry_collection.additional_properties = d + return geometry_collection + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/georeference.py b/fastfuels_sdk/v2/client_library/models/georeference.py new file mode 100644 index 0000000..c71034f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/georeference.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="Georeference") + + +@_attrs_define +class Georeference: + """Spatial reference for a 2D grid. + + Uses rasterio/GDAL conventions: + - transform: Affine coefficients [a, b, c, d, e, f] where: + x = a * col + b * row + c + y = d * col + e * row + f + For north-up images: a = pixel_width, e = -pixel_height, b = d = 0 + - shape: (height, width) in pixels + + Attributes: + crs (str): e.g., 'EPSG:32610' + transform (list[float]): Affine transform [a, b, c, d, e, f] + shape (list[int]): (height, width) + """ + + crs: str + transform: list[float] + shape: list[int] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + transform = [] + for transform_item_data in self.transform: + transform_item: float + transform_item = transform_item_data + transform.append(transform_item) + + shape = [] + for shape_item_data in self.shape: + shape_item: int + shape_item = shape_item_data + shape.append(shape_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "transform": transform, + "shape": shape, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + transform = [] + _transform = d.pop("transform") + for transform_item_data in _transform: + + def _parse_transform_item(data: object) -> float: + return cast(float, data) + + transform_item = _parse_transform_item(transform_item_data) + + transform.append(transform_item) + + shape = [] + _shape = d.pop("shape") + for shape_item_data in _shape: + + def _parse_shape_item(data: object) -> int: + return cast(int, data) + + shape_item = _parse_shape_item(shape_item_data) + + shape.append(shape_item) + + georeference = cls( + crs=crs, + transform=transform, + shape=shape, + ) + + georeference.additional_properties = d + return georeference + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/georeference_3d.py b/fastfuels_sdk/v2/client_library/models/georeference_3d.py new file mode 100644 index 0000000..73c7e8d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/georeference_3d.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="Georeference3D") + + +@_attrs_define +class Georeference3D: + """Spatial reference for a 3D grid. + + Attributes: + crs (str): e.g., 'EPSG:32610' + transform (list[float]): Affine transform [a, b, c, d, e, f] + shape (list[int]): (z, height, width) + z_resolution (float): + z_origin (float): + """ + + crs: str + transform: list[float] + shape: list[int] + z_resolution: float + z_origin: float + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + transform = [] + for transform_item_data in self.transform: + transform_item: float + transform_item = transform_item_data + transform.append(transform_item) + + shape = [] + for shape_item_data in self.shape: + shape_item: int + shape_item = shape_item_data + shape.append(shape_item) + + z_resolution = self.z_resolution + + z_origin = self.z_origin + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "transform": transform, + "shape": shape, + "z_resolution": z_resolution, + "z_origin": z_origin, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + transform = [] + _transform = d.pop("transform") + for transform_item_data in _transform: + + def _parse_transform_item(data: object) -> float: + return cast(float, data) + + transform_item = _parse_transform_item(transform_item_data) + + transform.append(transform_item) + + shape = [] + _shape = d.pop("shape") + for shape_item_data in _shape: + + def _parse_shape_item(data: object) -> int: + return cast(int, data) + + shape_item = _parse_shape_item(shape_item_data) + + shape.append(shape_item) + + z_resolution = d.pop("z_resolution") + + z_origin = d.pop("z_origin") + + georeference_3d = cls( + crs=crs, + transform=transform, + shape=shape, + z_resolution=z_resolution, + z_origin=z_origin, + ) + + georeference_3d.additional_properties = d + return georeference_3d + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid.py b/fastfuels_sdk/v2/client_library/models/grid.py new file mode 100644 index 0000000..dfd6097 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.job_status import JobStatus +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.band import Band + from ..models.chunks import Chunks + from ..models.georeference import Georeference + from ..models.georeference_3d import Georeference3D + from ..models.grid_modification import GridModification + from ..models.grid_source import GridSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + +T = TypeVar("T", bound="Grid") + + +@_attrs_define +class Grid: + """The Grid resource. + + When status is "pending" or "running", georeference will be null. + The backend populates georeference after successfully fetching data, + at which point status transitions to "completed". + + When status is "failed", the error field contains details about what + went wrong and suggestions for the user. The full traceback is stored + in Firestore but not exposed in API responses. + + Attributes: + id (str): + domain_id (str): + status (JobStatus): Status of an async job. + source (GridSource): + bands (list[Band]): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + progress (JobProgress | None | Unset): Progress info when status is 'running'. Null otherwise. + created_on (datetime.datetime | None | Unset): + modified_on (datetime.datetime | None | Unset): + checksum (None | str | Unset): Version marker for this grid's content. It changes each time the grid is rebuilt + and is unaffected by metadata-only edits (name, description, tags). A resource derived from this grid stores the + checksum it was built from; comparing that stored value against this field reveals whether this grid has changed + since. May be null for grids created before checksums were introduced. + modifications (list[GridModification] | Unset): + georeference (Georeference | Georeference3D | None | Unset): Spatial reference. Null until backend completes + data fetch. + error (JobError | None | Unset): Error details if status is 'failed'. Traceback stored but not exposed. + chunks (Chunks | None | Unset): Chunk layout. Null until the grid finishes processing. Use chunks.count to know + how many chunks are available to fetch. + tags (list[str] | Unset): + """ + + id: str + domain_id: str + status: JobStatus + source: GridSource + bands: list[Band] + name: str | Unset = "" + description: str | Unset = "" + progress: JobProgress | None | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + checksum: None | str | Unset = UNSET + modifications: list[GridModification] | Unset = UNSET + georeference: Georeference | Georeference3D | None | Unset = UNSET + error: JobError | None | Unset = UNSET + chunks: Chunks | None | Unset = UNSET + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.chunks import Chunks + from ..models.georeference import Georeference + from ..models.georeference_3d import Georeference3D + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + id = self.id + + domain_id = self.domain_id + + status = self.status.value + + source = self.source.to_dict() + + bands = [] + for bands_item_data in self.bands: + bands_item = bands_item_data.to_dict() + bands.append(bands_item) + + name = self.name + + description = self.description + + progress: dict[str, Any] | None | Unset + if isinstance(self.progress, Unset): + progress = UNSET + elif isinstance(self.progress, JobProgress): + progress = self.progress.to_dict() + else: + progress = self.progress + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + checksum: None | str | Unset + if isinstance(self.checksum, Unset): + checksum = UNSET + else: + checksum = self.checksum + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + georeference: dict[str, Any] | None | Unset + if isinstance(self.georeference, Unset): + georeference = UNSET + elif isinstance(self.georeference, Georeference) or isinstance( + self.georeference, Georeference3D + ): + georeference = self.georeference.to_dict() + else: + georeference = self.georeference + + error: dict[str, Any] | None | Unset + if isinstance(self.error, Unset): + error = UNSET + elif isinstance(self.error, JobError): + error = self.error.to_dict() + else: + error = self.error + + chunks: dict[str, Any] | None | Unset + if isinstance(self.chunks, Unset): + chunks = UNSET + elif isinstance(self.chunks, Chunks): + chunks = self.chunks.to_dict() + else: + chunks = self.chunks + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "domain_id": domain_id, + "status": status, + "source": source, + "bands": bands, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if progress is not UNSET: + field_dict["progress"] = progress + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if checksum is not UNSET: + field_dict["checksum"] = checksum + if modifications is not UNSET: + field_dict["modifications"] = modifications + if georeference is not UNSET: + field_dict["georeference"] = georeference + if error is not UNSET: + field_dict["error"] = error + if chunks is not UNSET: + field_dict["chunks"] = chunks + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.band import Band + from ..models.chunks import Chunks + from ..models.georeference import Georeference + from ..models.georeference_3d import Georeference3D + from ..models.grid_modification import GridModification + from ..models.grid_source import GridSource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + + d = dict(src_dict) + id = d.pop("id") + + domain_id = d.pop("domain_id") + + status = JobStatus(d.pop("status")) + + source = GridSource.from_dict(d.pop("source")) + + bands = [] + _bands = d.pop("bands") + for bands_item_data in _bands: + bands_item = Band.from_dict(bands_item_data) + + bands.append(bands_item) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + def _parse_progress(data: object) -> JobProgress | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + progress_type_0 = JobProgress.from_dict(data) + + return progress_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobProgress | None | Unset, data) + + progress = _parse_progress(d.pop("progress", UNSET)) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + def _parse_checksum(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + checksum = _parse_checksum(d.pop("checksum", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[GridModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = GridModification.from_dict(modifications_item_data) + + modifications.append(modifications_item) + + def _parse_georeference( + data: object, + ) -> Georeference | Georeference3D | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + georeference_type_0 = Georeference.from_dict(data) + + return georeference_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + georeference_type_1 = Georeference3D.from_dict(data) + + return georeference_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(Georeference | Georeference3D | None | Unset, data) + + georeference = _parse_georeference(d.pop("georeference", UNSET)) + + def _parse_error(data: object) -> JobError | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + error_type_0 = JobError.from_dict(data) + + return error_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobError | None | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + def _parse_chunks(data: object) -> Chunks | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + chunks_type_0 = Chunks.from_dict(data) + + return chunks_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(Chunks | None | Unset, data) + + chunks = _parse_chunks(d.pop("chunks", UNSET)) + + tags = cast(list[str], d.pop("tags", UNSET)) + + grid = cls( + id=id, + domain_id=domain_id, + status=status, + source=source, + bands=bands, + name=name, + description=description, + progress=progress, + created_on=created_on, + modified_on=modified_on, + checksum=checksum, + modifications=modifications, + georeference=georeference, + error=error, + chunks=chunks, + tags=tags, + ) + + grid.additional_properties = d + return grid + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_alignment_domain_target.py b/fastfuels_sdk/v2/client_library/models/grid_alignment_domain_target.py new file mode 100644 index 0000000..6cdf971 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_alignment_domain_target.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.resampling_method import ResamplingMethod +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GridAlignmentDomainTarget") + + +@_attrs_define +class GridAlignmentDomainTarget: + """Anchor output to the domain origin. + + `resolution=None` uses the source's native cell size. Output cells tile + the domain bounding box (already snapped at domain creation if + `pad_to_resolution` was set). + + Attributes: + target (Literal['domain'] | Unset): Default: 'domain'. + resolution (float | None | Unset): + method (None | ResamplingMethod | Unset): + """ + + target: Literal["domain"] | Unset = "domain" + resolution: float | None | Unset = UNSET + method: None | ResamplingMethod | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + resolution: float | None | Unset + if isinstance(self.resolution, Unset): + resolution = UNSET + else: + resolution = self.resolution + + method: None | str | Unset + if isinstance(self.method, Unset): + method = UNSET + elif isinstance(self.method, ResamplingMethod): + method = self.method.value + else: + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if target is not UNSET: + field_dict["target"] = target + if resolution is not UNSET: + field_dict["resolution"] = resolution + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["domain"] | Unset, d.pop("target", UNSET)) + if target != "domain" and not isinstance(target, Unset): + raise ValueError(f"target must match const 'domain', got '{target}'") + + def _parse_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + resolution = _parse_resolution(d.pop("resolution", UNSET)) + + def _parse_method(data: object) -> None | ResamplingMethod | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + method_type_0 = ResamplingMethod(data) + + return method_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ResamplingMethod | Unset, data) + + method = _parse_method(d.pop("method", UNSET)) + + grid_alignment_domain_target = cls( + target=target, + resolution=resolution, + method=method, + ) + + grid_alignment_domain_target.additional_properties = d + return grid_alignment_domain_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_alignment_grid_target.py b/fastfuels_sdk/v2/client_library/models/grid_alignment_grid_target.py new file mode 100644 index 0000000..59debcb --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_alignment_grid_target.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.resampling_method import ResamplingMethod +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GridAlignmentGridTarget") + + +@_attrs_define +class GridAlignmentGridTarget: + """Align to an existing grid by id. + + `resolution=None` produces an exact lattice match (CRS, transform, and + shape from the target grid). With an explicit `resolution`, the output + keeps the target's CRS and origin but uses the new cell size; shape is + recomputed from the target grid's bounds. + + Attributes: + target (Literal['grid']): + grid_id (str): + resolution (float | None | Unset): + method (None | ResamplingMethod | Unset): + """ + + target: Literal["grid"] + grid_id: str + resolution: float | None | Unset = UNSET + method: None | ResamplingMethod | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + grid_id = self.grid_id + + resolution: float | None | Unset + if isinstance(self.resolution, Unset): + resolution = UNSET + else: + resolution = self.resolution + + method: None | str | Unset + if isinstance(self.method, Unset): + method = UNSET + elif isinstance(self.method, ResamplingMethod): + method = self.method.value + else: + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "target": target, + "grid_id": grid_id, + } + ) + if resolution is not UNSET: + field_dict["resolution"] = resolution + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["grid"], d.pop("target")) + if target != "grid": + raise ValueError(f"target must match const 'grid', got '{target}'") + + grid_id = d.pop("grid_id") + + def _parse_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + resolution = _parse_resolution(d.pop("resolution", UNSET)) + + def _parse_method(data: object) -> None | ResamplingMethod | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + method_type_0 = ResamplingMethod(data) + + return method_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ResamplingMethod | Unset, data) + + method = _parse_method(d.pop("method", UNSET)) + + grid_alignment_grid_target = cls( + target=target, + grid_id=grid_id, + resolution=resolution, + method=method, + ) + + grid_alignment_grid_target.additional_properties = d + return grid_alignment_grid_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_alignment_native_target.py b/fastfuels_sdk/v2/client_library/models/grid_alignment_native_target.py new file mode 100644 index 0000000..1166eff --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_alignment_native_target.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.resampling_method import ResamplingMethod +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GridAlignmentNativeTarget") + + +@_attrs_define +class GridAlignmentNativeTarget: + """Preserve the source raster's pixel anchor. + + `resolution=None` is exactly today's behavior — no anchor or resolution + change beyond the standard ROI-CRS reprojection. + + Attributes: + target (Literal['native']): + resolution (float | None | Unset): + method (None | ResamplingMethod | Unset): + """ + + target: Literal["native"] + resolution: float | None | Unset = UNSET + method: None | ResamplingMethod | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + resolution: float | None | Unset + if isinstance(self.resolution, Unset): + resolution = UNSET + else: + resolution = self.resolution + + method: None | str | Unset + if isinstance(self.method, Unset): + method = UNSET + elif isinstance(self.method, ResamplingMethod): + method = self.method.value + else: + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "target": target, + } + ) + if resolution is not UNSET: + field_dict["resolution"] = resolution + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["native"], d.pop("target")) + if target != "native": + raise ValueError(f"target must match const 'native', got '{target}'") + + def _parse_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + resolution = _parse_resolution(d.pop("resolution", UNSET)) + + def _parse_method(data: object) -> None | ResamplingMethod | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + method_type_0 = ResamplingMethod(data) + + return method_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ResamplingMethod | Unset, data) + + method = _parse_method(d.pop("method", UNSET)) + + grid_alignment_native_target = cls( + target=target, + resolution=resolution, + method=method, + ) + + grid_alignment_native_target.additional_properties = d + return grid_alignment_native_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_data_array_format.py b/fastfuels_sdk/v2/client_library/models/grid_data_array_format.py new file mode 100644 index 0000000..517d421 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_data_array_format.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class GridDataArrayFormat(str, Enum): + DENSE = "dense" + SPARSE = "sparse" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_data_chunk_metadata.py b/fastfuels_sdk/v2/client_library/models/grid_data_chunk_metadata.py new file mode 100644 index 0000000..351c64f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_data_chunk_metadata.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GridDataChunkMetadata") + + +@_attrs_define +class GridDataChunkMetadata: + """ + Attributes: + index (int): + shape (list[int]): + offset (list[int]): + transform (list[float]): + z_origin (float | None | Unset): + z_resolution (float | None | Unset): + """ + + index: int + shape: list[int] + offset: list[int] + transform: list[float] + z_origin: float | None | Unset = UNSET + z_resolution: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + index = self.index + + shape: list[int] + if isinstance(self.shape, list): + shape = [] + for shape_type_0_item_data in self.shape: + shape_type_0_item: int + shape_type_0_item = shape_type_0_item_data + shape.append(shape_type_0_item) + + offset: list[int] + if isinstance(self.offset, list): + offset = [] + for offset_type_0_item_data in self.offset: + offset_type_0_item: int + offset_type_0_item = offset_type_0_item_data + offset.append(offset_type_0_item) + + transform = [] + for transform_item_data in self.transform: + transform_item: float + transform_item = transform_item_data + transform.append(transform_item) + + z_origin: float | None | Unset + if isinstance(self.z_origin, Unset): + z_origin = UNSET + else: + z_origin = self.z_origin + + z_resolution: float | None | Unset + if isinstance(self.z_resolution, Unset): + z_resolution = UNSET + else: + z_resolution = self.z_resolution + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "index": index, + "shape": shape, + "offset": offset, + "transform": transform, + } + ) + if z_origin is not UNSET: + field_dict["z_origin"] = z_origin + if z_resolution is not UNSET: + field_dict["z_resolution"] = z_resolution + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + index = d.pop("index") + + def _parse_shape(data: object) -> list[int]: + if not isinstance(data, list): + raise TypeError() + shape_type_0 = [] + _shape_type_0 = data + for shape_type_0_item_data in _shape_type_0: + + def _parse_shape_type_0_item(data: object) -> int: + return cast(int, data) + + shape_type_0_item = _parse_shape_type_0_item(shape_type_0_item_data) + + shape_type_0.append(shape_type_0_item) + + return shape_type_0 + + shape = _parse_shape(d.pop("shape")) + + def _parse_offset(data: object) -> list[int]: + if not isinstance(data, list): + raise TypeError() + offset_type_0 = [] + _offset_type_0 = data + for offset_type_0_item_data in _offset_type_0: + + def _parse_offset_type_0_item(data: object) -> int: + return cast(int, data) + + offset_type_0_item = _parse_offset_type_0_item(offset_type_0_item_data) + + offset_type_0.append(offset_type_0_item) + + return offset_type_0 + + offset = _parse_offset(d.pop("offset")) + + transform = [] + _transform = d.pop("transform") + for transform_item_data in _transform: + + def _parse_transform_item(data: object) -> float: + return cast(float, data) + + transform_item = _parse_transform_item(transform_item_data) + + transform.append(transform_item) + + def _parse_z_origin(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + z_origin = _parse_z_origin(d.pop("z_origin", UNSET)) + + def _parse_z_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + z_resolution = _parse_z_resolution(d.pop("z_resolution", UNSET)) + + grid_data_chunk_metadata = cls( + index=index, + shape=shape, + offset=offset, + transform=transform, + z_origin=z_origin, + z_resolution=z_resolution, + ) + + grid_data_chunk_metadata.additional_properties = d + return grid_data_chunk_metadata + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_data_order.py b/fastfuels_sdk/v2/client_library/models/grid_data_order.py new file mode 100644 index 0000000..15eaee3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_data_order.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class GridDataOrder(str, Enum): + C = "C" + F = "F" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_data_response.py b/fastfuels_sdk/v2/client_library/models/grid_data_response.py new file mode 100644 index 0000000..50ef9df --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_data_response.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.grid_data_response_order import GridDataResponseOrder + +if TYPE_CHECKING: + from ..models.dense_grid_data import DenseGridData + from ..models.grid_data_chunk_metadata import GridDataChunkMetadata + from ..models.sparse_grid_data import SparseGridData + + +T = TypeVar("T", bound="GridDataResponse") + + +@_attrs_define +class GridDataResponse: + """ + Attributes: + shape (list[int]): + order (GridDataResponseOrder): + metadata (GridDataChunkMetadata): + data (DenseGridData | SparseGridData): + """ + + shape: list[int] + order: GridDataResponseOrder + metadata: GridDataChunkMetadata + data: DenseGridData | SparseGridData + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.dense_grid_data import DenseGridData + + shape = self.shape + + order = self.order.value + + metadata = self.metadata.to_dict() + + data: dict[str, Any] + if isinstance(self.data, DenseGridData): + data = self.data.to_dict() + else: + data = self.data.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "shape": shape, + "order": order, + "metadata": metadata, + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.dense_grid_data import DenseGridData + from ..models.grid_data_chunk_metadata import GridDataChunkMetadata + from ..models.sparse_grid_data import SparseGridData + + d = dict(src_dict) + shape = cast(list[int], d.pop("shape")) + + order = GridDataResponseOrder(d.pop("order")) + + metadata = GridDataChunkMetadata.from_dict(d.pop("metadata")) + + def _parse_data(data: object) -> DenseGridData | SparseGridData: + try: + if not isinstance(data, dict): + raise TypeError() + data_type_0 = DenseGridData.from_dict(data) + + return data_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + data_type_1 = SparseGridData.from_dict(data) + + return data_type_1 + + data = _parse_data(d.pop("data")) + + grid_data_response = cls( + shape=shape, + order=order, + metadata=metadata, + data=data, + ) + + grid_data_response.additional_properties = d + return grid_data_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_data_response_order.py b/fastfuels_sdk/v2/client_library/models/grid_data_response_order.py new file mode 100644 index 0000000..1c9e0c3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_data_response_order.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class GridDataResponseOrder(str, Enum): + C = "C" + F = "F" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_export_format.py b/fastfuels_sdk/v2/client_library/models/grid_export_format.py new file mode 100644 index 0000000..065b15d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_export_format.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class GridExportFormat(str, Enum): + GEOTIFF = "geotiff" + NETCDF = "netcdf" + ZARR = "zarr" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_feature_spatial_condition.py b/fastfuels_sdk/v2/client_library/models/grid_feature_spatial_condition.py new file mode 100644 index 0000000..f1f45f3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_feature_spatial_condition.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.grid_spatial_target import GridSpatialTarget +from ..models.spatial_operator import SpatialOperator +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GridFeatureSpatialCondition") + + +@_attrs_define +class GridFeatureSpatialCondition: + """Spatial condition that tests cells against a persisted Feature resource. + + The referenced Feature must belong to the same domain as the grid being + modified. The processing service loads the feature's geometry, reprojects + it into the domain CRS, optionally buffers it, and then evaluates the + spatial operator against grid cells. + + Attributes: + source (Literal['feature']): Discriminator selecting this variant. Must be the literal string `"feature"`. Use + `"geometry"` instead to supply inline GeoJSON. + operator (SpatialOperator): Spatial relationship operators for geometry-based conditions. + + - within: Select items whose target (centroid or cell) is inside the geometry + - outside: Select items whose target is outside the geometry (inverse of within) + - intersects: Select items whose target overlaps with the geometry + feature_id (str): ID of a Feature resource (road, water, or layerset) hosted in the same domain as the grid. + Cross-domain references are rejected; the feature must be in `completed` status. + buffer_m (float | None | Unset): Optional buffer distance in meters applied to the feature geometry (in the + domain's projected CRS) before testing. With `target="centroid"` (the default), a buffer is typically needed for + linestring features such as roads since a bare linestring rarely passes through a cell centroid; `target="cell"` + catches every cell the line crosses without a buffer. + target (GridSpatialTarget | Unset): Specifies which part of a grid cell is tested against the geometry. + + - centroid: Test only the cell's centroid point against the geometry + - cell: Test the entire cell bounds against the geometry + """ + + source: Literal["feature"] + operator: SpatialOperator + feature_id: str + buffer_m: float | None | Unset = UNSET + target: GridSpatialTarget | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source = self.source + + operator = self.operator.value + + feature_id = self.feature_id + + buffer_m: float | None | Unset + if isinstance(self.buffer_m, Unset): + buffer_m = UNSET + else: + buffer_m = self.buffer_m + + target: str | Unset = UNSET + if not isinstance(self.target, Unset): + target = self.target.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source": source, + "operator": operator, + "feature_id": feature_id, + } + ) + if buffer_m is not UNSET: + field_dict["buffer_m"] = buffer_m + if target is not UNSET: + field_dict["target"] = target + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + source = cast(Literal["feature"], d.pop("source")) + if source != "feature": + raise ValueError(f"source must match const 'feature', got '{source}'") + + operator = SpatialOperator(d.pop("operator")) + + feature_id = d.pop("feature_id") + + def _parse_buffer_m(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + buffer_m = _parse_buffer_m(d.pop("buffer_m", UNSET)) + + _target = d.pop("target", UNSET) + target: GridSpatialTarget | Unset + if isinstance(_target, Unset): + target = UNSET + else: + target = GridSpatialTarget(_target) + + grid_feature_spatial_condition = cls( + source=source, + operator=operator, + feature_id=feature_id, + buffer_m=buffer_m, + target=target, + ) + + grid_feature_spatial_condition.additional_properties = d + return grid_feature_spatial_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition.py b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition.py new file mode 100644 index 0000000..0e97b0b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.grid_spatial_target import GridSpatialTarget +from ..models.spatial_operator import SpatialOperator +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_geometry_spatial_condition_crs_type_0 import ( + GridGeometrySpatialConditionCrsType0, + ) + from ..models.grid_geometry_spatial_condition_geometry import ( + GridGeometrySpatialConditionGeometry, + ) + + +T = TypeVar("T", bound="GridGeometrySpatialCondition") + + +@_attrs_define +class GridGeometrySpatialCondition: + """Spatial condition that tests cells against an inline GeoJSON geometry. + + Use this variant when the geometry is supplied directly in the request + (e.g. a hand-drawn polygon). For a persisted geometry hosted as a + Feature resource, use ``GridFeatureSpatialCondition`` instead. + + Attributes: + source (Literal['geometry']): Discriminator selecting this variant. Must be the literal string `"geometry"`. Use + `"feature"` instead to reference a persisted Feature resource by id. + operator (SpatialOperator): Spatial relationship operators for geometry-based conditions. + + - within: Select items whose target (centroid or cell) is inside the geometry + - outside: Select items whose target is outside the geometry (inverse of within) + - intersects: Select items whose target overlaps with the geometry + geometry (GridGeometrySpatialConditionGeometry): Inline GeoJSON geometry. Polygon and MultiPolygon are the + common shapes; LineString works in combination with `target="cell"` since centroid-mode rarely matches a bare + line. + crs (GridGeometrySpatialConditionCrsType0 | None | Unset): CRS of `geometry`, expressed as a GeoJSON CRS object + (`{"type": "name", "properties": {"name": "EPSG:..."}}`). Defaults to the domain CRS when null. + buffer_m (float | None | Unset): Optional buffer distance in meters applied to the geometry (in the domain's + projected CRS) before testing. Use a non-zero buffer to widen the masked region beyond the literal geometry + (e.g. shoreline tolerance around a water polygon). + target (GridSpatialTarget | Unset): Specifies which part of a grid cell is tested against the geometry. + + - centroid: Test only the cell's centroid point against the geometry + - cell: Test the entire cell bounds against the geometry + """ + + source: Literal["geometry"] + operator: SpatialOperator + geometry: GridGeometrySpatialConditionGeometry + crs: GridGeometrySpatialConditionCrsType0 | None | Unset = UNSET + buffer_m: float | None | Unset = UNSET + target: GridSpatialTarget | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_geometry_spatial_condition_crs_type_0 import ( + GridGeometrySpatialConditionCrsType0, + ) + + source = self.source + + operator = self.operator.value + + geometry = self.geometry.to_dict() + + crs: dict[str, Any] | None | Unset + if isinstance(self.crs, Unset): + crs = UNSET + elif isinstance(self.crs, GridGeometrySpatialConditionCrsType0): + crs = self.crs.to_dict() + else: + crs = self.crs + + buffer_m: float | None | Unset + if isinstance(self.buffer_m, Unset): + buffer_m = UNSET + else: + buffer_m = self.buffer_m + + target: str | Unset = UNSET + if not isinstance(self.target, Unset): + target = self.target.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source": source, + "operator": operator, + "geometry": geometry, + } + ) + if crs is not UNSET: + field_dict["crs"] = crs + if buffer_m is not UNSET: + field_dict["buffer_m"] = buffer_m + if target is not UNSET: + field_dict["target"] = target + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_geometry_spatial_condition_crs_type_0 import ( + GridGeometrySpatialConditionCrsType0, + ) + from ..models.grid_geometry_spatial_condition_geometry import ( + GridGeometrySpatialConditionGeometry, + ) + + d = dict(src_dict) + source = cast(Literal["geometry"], d.pop("source")) + if source != "geometry": + raise ValueError(f"source must match const 'geometry', got '{source}'") + + operator = SpatialOperator(d.pop("operator")) + + geometry = GridGeometrySpatialConditionGeometry.from_dict(d.pop("geometry")) + + def _parse_crs( + data: object, + ) -> GridGeometrySpatialConditionCrsType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + crs_type_0 = GridGeometrySpatialConditionCrsType0.from_dict(data) + + return crs_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(GridGeometrySpatialConditionCrsType0 | None | Unset, data) + + crs = _parse_crs(d.pop("crs", UNSET)) + + def _parse_buffer_m(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + buffer_m = _parse_buffer_m(d.pop("buffer_m", UNSET)) + + _target = d.pop("target", UNSET) + target: GridSpatialTarget | Unset + if isinstance(_target, Unset): + target = UNSET + else: + target = GridSpatialTarget(_target) + + grid_geometry_spatial_condition = cls( + source=source, + operator=operator, + geometry=geometry, + crs=crs, + buffer_m=buffer_m, + target=target, + ) + + grid_geometry_spatial_condition.additional_properties = d + return grid_geometry_spatial_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_crs_type_0.py b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_crs_type_0.py new file mode 100644 index 0000000..86582c7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_crs_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GridGeometrySpatialConditionCrsType0") + + +@_attrs_define +class GridGeometrySpatialConditionCrsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_geometry_spatial_condition_crs_type_0 = cls() + + grid_geometry_spatial_condition_crs_type_0.additional_properties = d + return grid_geometry_spatial_condition_crs_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_geometry.py b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_geometry.py new file mode 100644 index 0000000..c1e383d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_geometry_spatial_condition_geometry.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GridGeometrySpatialConditionGeometry") + + +@_attrs_define +class GridGeometrySpatialConditionGeometry: + """Inline GeoJSON geometry. Polygon and MultiPolygon are the common shapes; LineString works in combination with + `target="cell"` since centroid-mode rarely matches a bare line. + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_geometry_spatial_condition_geometry = cls() + + grid_geometry_spatial_condition_geometry.additional_properties = d + return grid_geometry_spatial_condition_geometry + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_modification.py b/fastfuels_sdk/v2/client_library/models/grid_modification.py new file mode 100644 index 0000000..05d7e32 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_modification.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import GridGeometrySpatialCondition + from ..models.grid_modification_action import GridModificationAction + from ..models.grid_modification_condition import GridModificationCondition + + +T = TypeVar("T", bound="GridModification") + + +@_attrs_define +class GridModification: + """A grid modification rule: a list of conditions and a list of actions. + + Conditions can be band-based (checking values) or spatial (checking + location). All conditions in a rule are **ANDed** — the actions apply only + to cells that satisfy *every* condition, so adding a condition narrows the + selection (the intersection). + + There is no OR within a rule. To act on a **union** of selections (e.g. + roads *or* water bodies), supply multiple rules: each rule is applied + independently, so adding a rule widens the overall selection. Putting two + mutually exclusive conditions (a road feature AND a water feature) in one + rule selects cells that are both at once — usually none. + + Attributes: + conditions (list[GridFeatureSpatialCondition | GridGeometrySpatialCondition | GridModificationCondition]): + Conditions that must all be true + actions (list[GridModificationAction]): Actions to apply when conditions match + """ + + conditions: list[ + GridFeatureSpatialCondition + | GridGeometrySpatialCondition + | GridModificationCondition + ] + actions: list[GridModificationAction] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.grid_modification_condition import GridModificationCondition + + conditions = [] + for conditions_item_data in self.conditions: + conditions_item: dict[str, Any] + if isinstance( + conditions_item_data, GridModificationCondition + ) or isinstance(conditions_item_data, GridGeometrySpatialCondition): + conditions_item = conditions_item_data.to_dict() + else: + conditions_item = conditions_item_data.to_dict() + + conditions.append(conditions_item) + + actions = [] + for actions_item_data in self.actions: + actions_item = actions_item_data.to_dict() + actions.append(actions_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "conditions": conditions, + "actions": actions, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_feature_spatial_condition import GridFeatureSpatialCondition + from ..models.grid_geometry_spatial_condition import ( + GridGeometrySpatialCondition, + ) + from ..models.grid_modification_action import GridModificationAction + from ..models.grid_modification_condition import GridModificationCondition + + d = dict(src_dict) + conditions = [] + _conditions = d.pop("conditions") + for conditions_item_data in _conditions: + + def _parse_conditions_item( + data: object, + ) -> ( + GridFeatureSpatialCondition + | GridGeometrySpatialCondition + | GridModificationCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_0 = GridModificationCondition.from_dict(data) + + return conditions_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_1_type_0 = ( + GridGeometrySpatialCondition.from_dict(data) + ) + + return conditions_item_type_1_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_1_type_1 = GridFeatureSpatialCondition.from_dict( + data + ) + + return conditions_item_type_1_type_1 + + conditions_item = _parse_conditions_item(conditions_item_data) + + conditions.append(conditions_item) + + actions = [] + _actions = d.pop("actions") + for actions_item_data in _actions: + actions_item = GridModificationAction.from_dict(actions_item_data) + + actions.append(actions_item) + + grid_modification = cls( + conditions=conditions, + actions=actions, + ) + + grid_modification.additional_properties = d + return grid_modification + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_modification_action.py b/fastfuels_sdk/v2/client_library/models/grid_modification_action.py new file mode 100644 index 0000000..654ae9c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_modification_action.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.modifier import Modifier + +T = TypeVar("T", bound="GridModificationAction") + + +@_attrs_define +class GridModificationAction: + """Action to perform when grid modification conditions are met. + + Uses dot-notation band keys (e.g., "fuel_load.1hr", "fbfm"). + + Attributes: + band (str): The band to modify (dot-notation key) + modifier (Modifier): Modifiers for modification actions. + value (float | int | str): The value to use with the modifier + """ + + band: str + modifier: Modifier + value: float | int | str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + band = self.band + + modifier = self.modifier.value + + value: float | int | str + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "band": band, + "modifier": modifier, + "value": value, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + band = d.pop("band") + + modifier = Modifier(d.pop("modifier")) + + def _parse_value(data: object) -> float | int | str: + return cast(float | int | str, data) + + value = _parse_value(d.pop("value")) + + grid_modification_action = cls( + band=band, + modifier=modifier, + value=value, + ) + + grid_modification_action.additional_properties = d + return grid_modification_action + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_modification_condition.py b/fastfuels_sdk/v2/client_library/models/grid_modification_condition.py new file mode 100644 index 0000000..3e31b51 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_modification_condition.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.operator import Operator + +T = TypeVar("T", bound="GridModificationCondition") + + +@_attrs_define +class GridModificationCondition: + """Attribute-based condition for grid modifications. + + Uses dot-notation band keys (e.g., "fuel_load.1hr", "fbfm"). + + Attributes: + band (str): The band to check (dot-notation key) + operator (Operator): Comparison operators for attribute-based conditions. + value (float | int | list[float | int | str] | str): The value(s) to compare against + """ + + band: str + operator: Operator + value: float | int | list[float | int | str] | str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + band = self.band + + operator = self.operator.value + + value: float | int | list[float | int | str] | str + if isinstance(self.value, list): + value = [] + for value_type_3_item_data in self.value: + value_type_3_item: float | int | str + value_type_3_item = value_type_3_item_data + value.append(value_type_3_item) + + else: + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "band": band, + "operator": operator, + "value": value, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + band = d.pop("band") + + operator = Operator(d.pop("operator")) + + def _parse_value(data: object) -> float | int | list[float | int | str] | str: + try: + if not isinstance(data, list): + raise TypeError() + value_type_3 = [] + _value_type_3 = data + for value_type_3_item_data in _value_type_3: + + def _parse_value_type_3_item(data: object) -> float | int | str: + return cast(float | int | str, data) + + value_type_3_item = _parse_value_type_3_item(value_type_3_item_data) + + value_type_3.append(value_type_3_item) + + return value_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(float | int | list[float | int | str] | str, data) + + value = _parse_value(d.pop("value")) + + grid_modification_condition = cls( + band=band, + operator=operator, + value=value, + ) + + grid_modification_condition.additional_properties = d + return grid_modification_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_sort_field.py b/fastfuels_sdk/v2/client_library/models/grid_sort_field.py new file mode 100644 index 0000000..7a0f04e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class GridSortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_source.py b/fastfuels_sdk/v2/client_library/models/grid_source.py new file mode 100644 index 0000000..817c367 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_source.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GridSource") + + +@_attrs_define +class GridSource: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_source = cls() + + grid_source.additional_properties = d + return grid_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_spatial_target.py b/fastfuels_sdk/v2/client_library/models/grid_spatial_target.py new file mode 100644 index 0000000..469d2e7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_spatial_target.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class GridSpatialTarget(str, Enum): + CELL = "cell" + CENTROID = "centroid" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/grid_upload_created_response.py b/fastfuels_sdk/v2/client_library/models/grid_upload_created_response.py new file mode 100644 index 0000000..1904764 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_upload_created_response.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.grid import Grid + from ..models.grid_upload_spec import GridUploadSpec + + +T = TypeVar("T", bound="GridUploadCreatedResponse") + + +@_attrs_define +class GridUploadCreatedResponse: + """ + Attributes: + grid (Grid): The Grid resource. + + When status is "pending" or "running", georeference will be null. + The backend populates georeference after successfully fetching data, + at which point status transitions to "completed". + + When status is "failed", the error field contains details about what + went wrong and suggestions for the user. The full traceback is stored + in Firestore but not exposed in API responses. + upload (GridUploadSpec): + """ + + grid: Grid + upload: GridUploadSpec + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + grid = self.grid.to_dict() + + upload = self.upload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "grid": grid, + "upload": upload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid import Grid + from ..models.grid_upload_spec import GridUploadSpec + + d = dict(src_dict) + grid = Grid.from_dict(d.pop("grid")) + + upload = GridUploadSpec.from_dict(d.pop("upload")) + + grid_upload_created_response = cls( + grid=grid, + upload=upload, + ) + + grid_upload_created_response.additional_properties = d + return grid_upload_created_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_upload_spec.py b/fastfuels_sdk/v2/client_library/models/grid_upload_spec.py new file mode 100644 index 0000000..0290085 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_upload_spec.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.grid_upload_spec_headers import GridUploadSpecHeaders + + +T = TypeVar("T", bound="GridUploadSpec") + + +@_attrs_define +class GridUploadSpec: + """ + Attributes: + url (str): + headers (GridUploadSpecHeaders): + content_type (str): + expires_at (datetime.datetime): + max_size_bytes (int): + method (Literal['PUT'] | Unset): Default: 'PUT'. + """ + + url: str + headers: GridUploadSpecHeaders + content_type: str + expires_at: datetime.datetime + max_size_bytes: int + method: Literal["PUT"] | Unset = "PUT" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + headers = self.headers.to_dict() + + content_type = self.content_type + + expires_at = self.expires_at.isoformat() + + max_size_bytes = self.max_size_bytes + + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "url": url, + "headers": headers, + "content_type": content_type, + "expires_at": expires_at, + "max_size_bytes": max_size_bytes, + } + ) + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid_upload_spec_headers import GridUploadSpecHeaders + + d = dict(src_dict) + url = d.pop("url") + + headers = GridUploadSpecHeaders.from_dict(d.pop("headers")) + + content_type = d.pop("content_type") + + expires_at = datetime.datetime.fromisoformat(d.pop("expires_at")) + + max_size_bytes = d.pop("max_size_bytes") + + method = cast(Literal["PUT"] | Unset, d.pop("method", UNSET)) + if method != "PUT" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'PUT', got '{method}'") + + grid_upload_spec = cls( + url=url, + headers=headers, + content_type=content_type, + expires_at=expires_at, + max_size_bytes=max_size_bytes, + method=method, + ) + + grid_upload_spec.additional_properties = d + return grid_upload_spec + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/grid_upload_spec_headers.py b/fastfuels_sdk/v2/client_library/models/grid_upload_spec_headers.py new file mode 100644 index 0000000..c107541 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/grid_upload_spec_headers.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GridUploadSpecHeaders") + + +@_attrs_define +class GridUploadSpecHeaders: + """ """ + + additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_upload_spec_headers = cls() + + grid_upload_spec_headers.additional_properties = d + return grid_upload_spec_headers + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> str: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: str) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/http_validation_error.py b/fastfuels_sdk/v2/client_library/models/http_validation_error.py new file mode 100644 index 0000000..69fd68a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/http_validation_error.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.validation_error import ValidationError + + +T = TypeVar("T", bound="HTTPValidationError") + + +@_attrs_define +class HTTPValidationError: + """ + Attributes: + detail (list[ValidationError] | Unset): + """ + + detail: list[ValidationError] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + detail: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.detail, Unset): + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() + detail.append(detail_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.validation_error import ValidationError + + d = dict(src_dict) + _detail = d.pop("detail", UNSET) + detail: list[ValidationError] | Unset = UNSET + if _detail is not UNSET: + detail = [] + for detail_item_data in _detail: + detail_item = ValidationError.from_dict(detail_item_data) + + detail.append(detail_item) + + http_validation_error = cls( + detail=detail, + ) + + http_validation_error.additional_properties = d + return http_validation_error + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inline_compute.py b/fastfuels_sdk/v2/client_library/models/inline_compute.py new file mode 100644 index 0000000..615356f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inline_compute.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.compose_operator import ComposeOperator + +if TYPE_CHECKING: + from ..models.compose_literal import ComposeLiteral + + +T = TypeVar("T", bound="InlineCompute") + + +@_attrs_define +class InlineCompute: + """A computation body: an operator over operands. + + Usable on its own as a conditional-fallback value; `ComposeCompute` + extends it with an output target and optional conditions. + + Attributes: + operator (ComposeOperator): Operators available for compose computations. + operands (list[ComposeLiteral | float | int | str]): + """ + + operator: ComposeOperator + operands: list[ComposeLiteral | float | int | str] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.compose_literal import ComposeLiteral + + operator = self.operator.value + + operands = [] + for operands_item_data in self.operands: + operands_item: dict[str, Any] | float | int | str + if isinstance(operands_item_data, ComposeLiteral): + operands_item = operands_item_data.to_dict() + else: + operands_item = operands_item_data + operands.append(operands_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "operator": operator, + "operands": operands, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.compose_literal import ComposeLiteral + + d = dict(src_dict) + operator = ComposeOperator(d.pop("operator")) + + operands = [] + _operands = d.pop("operands") + for operands_item_data in _operands: + + def _parse_operands_item( + data: object, + ) -> ComposeLiteral | float | int | str: + try: + if not isinstance(data, dict): + raise TypeError() + operands_item_type_3 = ComposeLiteral.from_dict(data) + + return operands_item_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(ComposeLiteral | float | int | str, data) + + operands_item = _parse_operands_item(operands_item_data) + + operands.append(operands_item) + + inline_compute = cls( + operator=operator, + operands=operands, + ) + + inline_compute.additional_properties = d + return inline_compute + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory.py b/fastfuels_sdk/v2/client_library/models/inventory.py new file mode 100644 index 0000000..dc09807 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_type import InventoryType +from ..models.job_status import JobStatus +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.column import Column + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_georeference import InventoryGeoreference + from ..models.inventory_modification import InventoryModification + from ..models.inventory_source import InventorySource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.tree_forestry_metrics import TreeForestryMetrics + + +T = TypeVar("T", bound="Inventory") + + +@_attrs_define +class Inventory: + """The Inventory resource. + + When status is "pending" or "running", georeference will be null. + The backend populates it after successfully processing data, + at which point status transitions to "completed". + + Attributes: + id (str): + domain_id (str): + type_ (InventoryType): Type of entities in the inventory. + status (JobStatus): Status of an async job. + source (InventorySource): + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + progress (JobProgress | None | Unset): Progress info when status is 'running'. Null otherwise. + created_on (datetime.datetime | None | Unset): + modified_on (datetime.datetime | None | Unset): + checksum (None | str | Unset): Version marker for this inventory's content. It changes each time the inventory + is rebuilt and is unaffected by metadata-only edits (name, description, tags). A resource derived from this + inventory stores the checksum it was built from; comparing that stored value against this field reveals whether + this inventory has changed since. May be null for inventories created before checksums were introduced. + modifications (list[InventoryModification] | Unset): + treatments (list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset): + columns (list[Column] | Unset): + forestry_metrics (None | TreeForestryMetrics | Unset): + georeference (InventoryGeoreference | None | Unset): Spatial reference. Null until backend completes processing. + error (JobError | None | Unset): Error details if status is 'failed'. + tags (list[str] | Unset): + """ + + id: str + domain_id: str + type_: InventoryType + status: JobStatus + source: InventorySource + name: str | Unset = "" + description: str | Unset = "" + progress: JobProgress | None | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + checksum: None | str | Unset = UNSET + modifications: list[InventoryModification] | Unset = UNSET + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + columns: list[Column] | Unset = UNSET + forestry_metrics: None | TreeForestryMetrics | Unset = UNSET + georeference: InventoryGeoreference | None | Unset = UNSET + error: JobError | None | Unset = UNSET + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_georeference import InventoryGeoreference + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.tree_forestry_metrics import TreeForestryMetrics + + id = self.id + + domain_id = self.domain_id + + type_ = self.type_.value + + status = self.status.value + + source = self.source.to_dict() + + name = self.name + + description = self.description + + progress: dict[str, Any] | None | Unset + if isinstance(self.progress, Unset): + progress = UNSET + elif isinstance(self.progress, JobProgress): + progress = self.progress.to_dict() + else: + progress = self.progress + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + checksum: None | str | Unset + if isinstance(self.checksum, Unset): + checksum = UNSET + else: + checksum = self.checksum + + modifications: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.modifications, Unset): + modifications = [] + for modifications_item_data in self.modifications: + modifications_item = modifications_item_data.to_dict() + modifications.append(modifications_item) + + treatments: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.treatments, Unset): + treatments = [] + for treatments_item_data in self.treatments: + treatments_item: dict[str, Any] + if isinstance(treatments_item_data, InventoryDiameterTreatment): + treatments_item = treatments_item_data.to_dict() + else: + treatments_item = treatments_item_data.to_dict() + + treatments.append(treatments_item) + + columns: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.columns, Unset): + columns = [] + for columns_item_data in self.columns: + columns_item = columns_item_data.to_dict() + columns.append(columns_item) + + forestry_metrics: dict[str, Any] | None | Unset + if isinstance(self.forestry_metrics, Unset): + forestry_metrics = UNSET + elif isinstance(self.forestry_metrics, TreeForestryMetrics): + forestry_metrics = self.forestry_metrics.to_dict() + else: + forestry_metrics = self.forestry_metrics + + georeference: dict[str, Any] | None | Unset + if isinstance(self.georeference, Unset): + georeference = UNSET + elif isinstance(self.georeference, InventoryGeoreference): + georeference = self.georeference.to_dict() + else: + georeference = self.georeference + + error: dict[str, Any] | None | Unset + if isinstance(self.error, Unset): + error = UNSET + elif isinstance(self.error, JobError): + error = self.error.to_dict() + else: + error = self.error + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "domain_id": domain_id, + "type": type_, + "status": status, + "source": source, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if progress is not UNSET: + field_dict["progress"] = progress + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if checksum is not UNSET: + field_dict["checksum"] = checksum + if modifications is not UNSET: + field_dict["modifications"] = modifications + if treatments is not UNSET: + field_dict["treatments"] = treatments + if columns is not UNSET: + field_dict["columns"] = columns + if forestry_metrics is not UNSET: + field_dict["forestry_metrics"] = forestry_metrics + if georeference is not UNSET: + field_dict["georeference"] = georeference + if error is not UNSET: + field_dict["error"] = error + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.column import Column + from ..models.inventory_basal_area_treatment import InventoryBasalAreaTreatment + from ..models.inventory_diameter_treatment import InventoryDiameterTreatment + from ..models.inventory_georeference import InventoryGeoreference + from ..models.inventory_modification import InventoryModification + from ..models.inventory_source import InventorySource + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.tree_forestry_metrics import TreeForestryMetrics + + d = dict(src_dict) + id = d.pop("id") + + domain_id = d.pop("domain_id") + + type_ = InventoryType(d.pop("type")) + + status = JobStatus(d.pop("status")) + + source = InventorySource.from_dict(d.pop("source")) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + def _parse_progress(data: object) -> JobProgress | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + progress_type_0 = JobProgress.from_dict(data) + + return progress_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobProgress | None | Unset, data) + + progress = _parse_progress(d.pop("progress", UNSET)) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + def _parse_checksum(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + checksum = _parse_checksum(d.pop("checksum", UNSET)) + + _modifications = d.pop("modifications", UNSET) + modifications: list[InventoryModification] | Unset = UNSET + if _modifications is not UNSET: + modifications = [] + for modifications_item_data in _modifications: + modifications_item = InventoryModification.from_dict( + modifications_item_data + ) + + modifications.append(modifications_item) + + _treatments = d.pop("treatments", UNSET) + treatments: ( + list[InventoryBasalAreaTreatment | InventoryDiameterTreatment] | Unset + ) = UNSET + if _treatments is not UNSET: + treatments = [] + for treatments_item_data in _treatments: + + def _parse_treatments_item( + data: object, + ) -> InventoryBasalAreaTreatment | InventoryDiameterTreatment: + try: + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_0 = InventoryDiameterTreatment.from_dict( + data + ) + + return treatments_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + treatments_item_type_1 = InventoryBasalAreaTreatment.from_dict(data) + + return treatments_item_type_1 + + treatments_item = _parse_treatments_item(treatments_item_data) + + treatments.append(treatments_item) + + _columns = d.pop("columns", UNSET) + columns: list[Column] | Unset = UNSET + if _columns is not UNSET: + columns = [] + for columns_item_data in _columns: + columns_item = Column.from_dict(columns_item_data) + + columns.append(columns_item) + + def _parse_forestry_metrics(data: object) -> None | TreeForestryMetrics | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + forestry_metrics_type_0 = TreeForestryMetrics.from_dict(data) + + return forestry_metrics_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | TreeForestryMetrics | Unset, data) + + forestry_metrics = _parse_forestry_metrics(d.pop("forestry_metrics", UNSET)) + + def _parse_georeference(data: object) -> InventoryGeoreference | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + georeference_type_0 = InventoryGeoreference.from_dict(data) + + return georeference_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(InventoryGeoreference | None | Unset, data) + + georeference = _parse_georeference(d.pop("georeference", UNSET)) + + def _parse_error(data: object) -> JobError | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + error_type_0 = JobError.from_dict(data) + + return error_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobError | None | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + tags = cast(list[str], d.pop("tags", UNSET)) + + inventory = cls( + id=id, + domain_id=domain_id, + type_=type_, + status=status, + source=source, + name=name, + description=description, + progress=progress, + created_on=created_on, + modified_on=modified_on, + checksum=checksum, + modifications=modifications, + treatments=treatments, + columns=columns, + forestry_metrics=forestry_metrics, + georeference=georeference, + error=error, + tags=tags, + ) + + inventory.additional_properties = d + return inventory + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_attribute.py b/fastfuels_sdk/v2/client_library/models/inventory_attribute.py new file mode 100644 index 0000000..1ca571d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_attribute.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class InventoryAttribute(str, Enum): + CROWN_RATIO = "crown_ratio" + DBH = "dbh" + FIA_SPECIES_CODE = "fia_species_code" + HEIGHT = "height" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_basal_area_treatment.py b/fastfuels_sdk/v2/client_library/models/inventory_basal_area_treatment.py new file mode 100644 index 0000000..07da686 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_basal_area_treatment.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_treatment_method import InventoryTreatmentMethod +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + +T = TypeVar("T", bound="InventoryBasalAreaTreatment") + + +@_attrs_define +class InventoryBasalAreaTreatment: + """Thin to a residual basal area. + + ``from_below``/``from_above`` remove the smallest/largest trees first until + the target is reached; ``proportional`` removes across all diameter classes. + + Attributes: + method (InventoryTreatmentMethod): Tree-selection strategy for a silvicultural treatment. + + - from_below: low thinning — remove smaller/suppressed trees first + - from_above: crown thinning — remove larger/dominant trees first + - proportional: remove across all diameter classes proportionally + value (float): Target residual basal area, in m**2/ha unless `unit` is set. + unit (None | str | Unset): Optional unit for `value`. Must be canonical and dimensionally compatible with the + metric's native unit; converted before the treatment is applied. + conditions (list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] | Unset): Spatial + conditions restricting the treatment to a region (within/outside/intersects a geometry or Feature). An empty + list applies the treatment to the entire inventory. + metric (Literal['basal_area'] | Unset): Default: 'basal_area'. + """ + + method: InventoryTreatmentMethod + value: float + unit: None | str | Unset = UNSET + conditions: ( + list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] + | Unset + ) = UNSET + metric: Literal["basal_area"] | Unset = "basal_area" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + method = self.method.value + + value = self.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + conditions: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.conditions, Unset): + conditions = [] + for conditions_item_data in self.conditions: + conditions_item: dict[str, Any] + if isinstance(conditions_item_data, InventoryGeometrySpatialCondition): + conditions_item = conditions_item_data.to_dict() + else: + conditions_item = conditions_item_data.to_dict() + + conditions.append(conditions_item) + + metric = self.metric + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "method": method, + "value": value, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + if conditions is not UNSET: + field_dict["conditions"] = conditions + if metric is not UNSET: + field_dict["metric"] = metric + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + d = dict(src_dict) + method = InventoryTreatmentMethod(d.pop("method")) + + value = d.pop("value") + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + _conditions = d.pop("conditions", UNSET) + conditions: ( + list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] + | Unset + ) = UNSET + if _conditions is not UNSET: + conditions = [] + for conditions_item_data in _conditions: + + def _parse_conditions_item( + data: object, + ) -> ( + InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_0 = ( + InventoryGeometrySpatialCondition.from_dict(data) + ) + + return conditions_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_1 = InventoryFeatureSpatialCondition.from_dict( + data + ) + + return conditions_item_type_1 + + conditions_item = _parse_conditions_item(conditions_item_data) + + conditions.append(conditions_item) + + metric = cast(Literal["basal_area"] | Unset, d.pop("metric", UNSET)) + if metric != "basal_area" and not isinstance(metric, Unset): + raise ValueError(f"metric must match const 'basal_area', got '{metric}'") + + inventory_basal_area_treatment = cls( + method=method, + value=value, + unit=unit, + conditions=conditions, + metric=metric, + ) + + inventory_basal_area_treatment.additional_properties = d + return inventory_basal_area_treatment + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_biomass_column.py b/fastfuels_sdk/v2/client_library/models/inventory_biomass_column.py new file mode 100644 index 0000000..514a873 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_biomass_column.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define + +from ..models.biomass_unit import BiomassUnit +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryBiomassColumn") + + +@_attrs_define +class InventoryBiomassColumn: + """Inventory column containing per-tree biomass for one component. + + Attributes: + column (str): + unit (BiomassUnit | Unset): Accepted inventory biomass units. + """ + + column: str + unit: BiomassUnit | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + column = self.column + + unit: str | Unset = UNSET + if not isinstance(self.unit, Unset): + unit = self.unit.value + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "column": column, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + column = d.pop("column") + + _unit = d.pop("unit", UNSET) + unit: BiomassUnit | Unset + if isinstance(_unit, Unset): + unit = UNSET + else: + unit = BiomassUnit(_unit) + + inventory_biomass_column = cls( + column=column, + unit=unit, + ) + + return inventory_biomass_column diff --git a/fastfuels_sdk/v2/client_library/models/inventory_column_mapping.py b/fastfuels_sdk/v2/client_library/models/inventory_column_mapping.py new file mode 100644 index 0000000..b0e2563 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_column_mapping.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryColumnMapping") + + +@_attrs_define +class InventoryColumnMapping: + """Maps v2 column names to the corresponding column names in the uploaded file. + + Omit any entry whose column already uses the v2 name. For GeoJSON and + GeoPackage formats, x and y are extracted from geometry — their mapping + entries are ignored. + + Attributes: + x (None | str | Unset): + y (None | str | Unset): + height (None | str | Unset): + fia_species_code (None | str | Unset): + fia_status_code (None | str | Unset): + dbh (None | str | Unset): + crown_ratio (None | str | Unset): + """ + + x: None | str | Unset = UNSET + y: None | str | Unset = UNSET + height: None | str | Unset = UNSET + fia_species_code: None | str | Unset = UNSET + fia_status_code: None | str | Unset = UNSET + dbh: None | str | Unset = UNSET + crown_ratio: None | str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + x: None | str | Unset + if isinstance(self.x, Unset): + x = UNSET + else: + x = self.x + + y: None | str | Unset + if isinstance(self.y, Unset): + y = UNSET + else: + y = self.y + + height: None | str | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + fia_species_code: None | str | Unset + if isinstance(self.fia_species_code, Unset): + fia_species_code = UNSET + else: + fia_species_code = self.fia_species_code + + fia_status_code: None | str | Unset + if isinstance(self.fia_status_code, Unset): + fia_status_code = UNSET + else: + fia_status_code = self.fia_status_code + + dbh: None | str | Unset + if isinstance(self.dbh, Unset): + dbh = UNSET + else: + dbh = self.dbh + + crown_ratio: None | str | Unset + if isinstance(self.crown_ratio, Unset): + crown_ratio = UNSET + else: + crown_ratio = self.crown_ratio + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if x is not UNSET: + field_dict["x"] = x + if y is not UNSET: + field_dict["y"] = y + if height is not UNSET: + field_dict["height"] = height + if fia_species_code is not UNSET: + field_dict["fia_species_code"] = fia_species_code + if fia_status_code is not UNSET: + field_dict["fia_status_code"] = fia_status_code + if dbh is not UNSET: + field_dict["dbh"] = dbh + if crown_ratio is not UNSET: + field_dict["crown_ratio"] = crown_ratio + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_x(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + x = _parse_x(d.pop("x", UNSET)) + + def _parse_y(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + y = _parse_y(d.pop("y", UNSET)) + + def _parse_height(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + def _parse_fia_species_code(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + fia_species_code = _parse_fia_species_code(d.pop("fia_species_code", UNSET)) + + def _parse_fia_status_code(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + fia_status_code = _parse_fia_status_code(d.pop("fia_status_code", UNSET)) + + def _parse_dbh(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + dbh = _parse_dbh(d.pop("dbh", UNSET)) + + def _parse_crown_ratio(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + crown_ratio = _parse_crown_ratio(d.pop("crown_ratio", UNSET)) + + inventory_column_mapping = cls( + x=x, + y=y, + height=height, + fia_species_code=fia_species_code, + fia_status_code=fia_status_code, + dbh=dbh, + crown_ratio=crown_ratio, + ) + + return inventory_column_mapping diff --git a/fastfuels_sdk/v2/client_library/models/inventory_column_max_crown_radius_source.py b/fastfuels_sdk/v2/client_library/models/inventory_column_max_crown_radius_source.py new file mode 100644 index 0000000..cccd085 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_column_max_crown_radius_source.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..models.max_crown_radius_unit import MaxCrownRadiusUnit +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryColumnMaxCrownRadiusSource") + + +@_attrs_define +class InventoryColumnMaxCrownRadiusSource: + """Read per-tree max crown radius from an inventory column. + + The crown profile model still drives the crown shape — the supplied + radius rescales it so the maximum radius matches the per-tree value. + + Attributes: + column (str): + type_ (Literal['inventory_column'] | Unset): Default: 'inventory_column'. + unit (MaxCrownRadiusUnit | Unset): Accepted inventory max crown radius units. + """ + + column: str + type_: Literal["inventory_column"] | Unset = "inventory_column" + unit: MaxCrownRadiusUnit | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + column = self.column + + type_ = self.type_ + + unit: str | Unset = UNSET + if not isinstance(self.unit, Unset): + unit = self.unit.value + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "column": column, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + column = d.pop("column") + + type_ = cast(Literal["inventory_column"] | Unset, d.pop("type", UNSET)) + if type_ != "inventory_column" and not isinstance(type_, Unset): + raise ValueError(f"type must match const 'inventory_column', got '{type_}'") + + _unit = d.pop("unit", UNSET) + unit: MaxCrownRadiusUnit | Unset + if isinstance(_unit, Unset): + unit = UNSET + else: + unit = MaxCrownRadiusUnit(_unit) + + inventory_column_max_crown_radius_source = cls( + column=column, + type_=type_, + unit=unit, + ) + + return inventory_column_max_crown_radius_source diff --git a/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source.py b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source.py new file mode 100644 index 0000000..271b599 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define + +from ..models.biomass_component import BiomassComponent +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.fine_biomass_config import FineBiomassConfig + from ..models.inventory_columns_biomass_source_columns import ( + InventoryColumnsBiomassSourceColumns, + ) + from ..models.inventory_columns_biomass_source_component_states import ( + InventoryColumnsBiomassSourceComponentStates, + ) + + +T = TypeVar("T", bound="InventoryColumnsBiomassSource") + + +@_attrs_define +class InventoryColumnsBiomassSource: + """Read per-tree component biomass from inventory columns. + + Attributes: + columns (InventoryColumnsBiomassSourceColumns): Per-component inventory columns. Values must be per-tree kg. + type_ (Literal['inventory_columns'] | Unset): Default: 'inventory_columns'. + components (list[BiomassComponent] | Unset): + component_states (InventoryColumnsBiomassSourceComponentStates | Unset): Per-component live/dead biomass + partition fractions. + fine (FineBiomassConfig | None | Unset): + """ + + columns: InventoryColumnsBiomassSourceColumns + type_: Literal["inventory_columns"] | Unset = "inventory_columns" + components: list[BiomassComponent] | Unset = UNSET + component_states: InventoryColumnsBiomassSourceComponentStates | Unset = UNSET + fine: FineBiomassConfig | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.fine_biomass_config import FineBiomassConfig + + columns = self.columns.to_dict() + + type_ = self.type_ + + components: list[str] | Unset = UNSET + if not isinstance(self.components, Unset): + components = [] + for components_item_data in self.components: + components_item = components_item_data.value + components.append(components_item) + + component_states: dict[str, Any] | Unset = UNSET + if not isinstance(self.component_states, Unset): + component_states = self.component_states.to_dict() + + fine: dict[str, Any] | None | Unset + if isinstance(self.fine, Unset): + fine = UNSET + elif isinstance(self.fine, FineBiomassConfig): + fine = self.fine.to_dict() + else: + fine = self.fine + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "columns": columns, + } + ) + if type_ is not UNSET: + field_dict["type"] = type_ + if components is not UNSET: + field_dict["components"] = components + if component_states is not UNSET: + field_dict["component_states"] = component_states + if fine is not UNSET: + field_dict["fine"] = fine + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.fine_biomass_config import FineBiomassConfig + from ..models.inventory_columns_biomass_source_columns import ( + InventoryColumnsBiomassSourceColumns, + ) + from ..models.inventory_columns_biomass_source_component_states import ( + InventoryColumnsBiomassSourceComponentStates, + ) + + d = dict(src_dict) + columns = InventoryColumnsBiomassSourceColumns.from_dict(d.pop("columns")) + + type_ = cast(Literal["inventory_columns"] | Unset, d.pop("type", UNSET)) + if type_ != "inventory_columns" and not isinstance(type_, Unset): + raise ValueError( + f"type must match const 'inventory_columns', got '{type_}'" + ) + + _components = d.pop("components", UNSET) + components: list[BiomassComponent] | Unset = UNSET + if _components is not UNSET: + components = [] + for components_item_data in _components: + components_item = BiomassComponent(components_item_data) + + components.append(components_item) + + _component_states = d.pop("component_states", UNSET) + component_states: InventoryColumnsBiomassSourceComponentStates | Unset + if isinstance(_component_states, Unset): + component_states = UNSET + else: + component_states = InventoryColumnsBiomassSourceComponentStates.from_dict( + _component_states + ) + + def _parse_fine(data: object) -> FineBiomassConfig | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fine_type_0 = FineBiomassConfig.from_dict(data) + + return fine_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FineBiomassConfig | None | Unset, data) + + fine = _parse_fine(d.pop("fine", UNSET)) + + inventory_columns_biomass_source = cls( + columns=columns, + type_=type_, + components=components, + component_states=component_states, + fine=fine, + ) + + return inventory_columns_biomass_source diff --git a/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_columns.py b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_columns.py new file mode 100644 index 0000000..38ea05c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_columns.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_biomass_column import InventoryBiomassColumn + + +T = TypeVar("T", bound="InventoryColumnsBiomassSourceColumns") + + +@_attrs_define +class InventoryColumnsBiomassSourceColumns: + """Per-component inventory columns. Values must be per-tree kg.""" + + additional_properties: dict[str, InventoryBiomassColumn] = _attrs_field( + init=False, factory=dict + ) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_biomass_column import InventoryBiomassColumn + + d = dict(src_dict) + inventory_columns_biomass_source_columns = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = InventoryBiomassColumn.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + inventory_columns_biomass_source_columns.additional_properties = ( + additional_properties + ) + return inventory_columns_biomass_source_columns + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> InventoryBiomassColumn: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: InventoryBiomassColumn) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_component_states.py b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_component_states.py new file mode 100644 index 0000000..b611df4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_columns_biomass_source_component_states.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.biomass_component_state import BiomassComponentState + + +T = TypeVar("T", bound="InventoryColumnsBiomassSourceComponentStates") + + +@_attrs_define +class InventoryColumnsBiomassSourceComponentStates: + """Per-component live/dead biomass partition fractions.""" + + additional_properties: dict[str, BiomassComponentState] = _attrs_field( + init=False, factory=dict + ) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.biomass_component_state import BiomassComponentState + + d = dict(src_dict) + inventory_columns_biomass_source_component_states = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = BiomassComponentState.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + inventory_columns_biomass_source_component_states.additional_properties = ( + additional_properties + ) + return inventory_columns_biomass_source_component_states + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> BiomassComponentState: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: BiomassComponentState) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_data_metadata.py b/fastfuels_sdk/v2/client_library/models/inventory_data_metadata.py new file mode 100644 index 0000000..1f25cef --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_data_metadata.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_partition_info import InventoryPartitionInfo + + +T = TypeVar("T", bound="InventoryDataMetadata") + + +@_attrs_define +class InventoryDataMetadata: + """ + Attributes: + inventory_id (str): + num_partitions (int): + total_rows (int): + columns (list[str]): + partitions (list[InventoryPartitionInfo]): + """ + + inventory_id: str + num_partitions: int + total_rows: int + columns: list[str] + partitions: list[InventoryPartitionInfo] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + inventory_id = self.inventory_id + + num_partitions = self.num_partitions + + total_rows = self.total_rows + + columns = self.columns + + partitions = [] + for partitions_item_data in self.partitions: + partitions_item = partitions_item_data.to_dict() + partitions.append(partitions_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "inventory_id": inventory_id, + "num_partitions": num_partitions, + "total_rows": total_rows, + "columns": columns, + "partitions": partitions, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_partition_info import InventoryPartitionInfo + + d = dict(src_dict) + inventory_id = d.pop("inventory_id") + + num_partitions = d.pop("num_partitions") + + total_rows = d.pop("total_rows") + + columns = cast(list[str], d.pop("columns")) + + partitions = [] + _partitions = d.pop("partitions") + for partitions_item_data in _partitions: + partitions_item = InventoryPartitionInfo.from_dict(partitions_item_data) + + partitions.append(partitions_item) + + inventory_data_metadata = cls( + inventory_id=inventory_id, + num_partitions=num_partitions, + total_rows=total_rows, + columns=columns, + partitions=partitions, + ) + + inventory_data_metadata.additional_properties = d + return inventory_data_metadata + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_data_response.py b/fastfuels_sdk/v2/client_library/models/inventory_data_response.py new file mode 100644 index 0000000..559f448 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_data_response.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_data_response_data_type_1_item import ( + InventoryDataResponseDataType1Item, + ) + + +T = TypeVar("T", bound="InventoryDataResponse") + + +@_attrs_define +class InventoryDataResponse: + """ + Attributes: + partition (int): + num_rows (int): + columns (list[str]): + data (list[InventoryDataResponseDataType1Item] | list[list[Any]]): + """ + + partition: int + num_rows: int + columns: list[str] + data: list[InventoryDataResponseDataType1Item] | list[list[Any]] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + partition = self.partition + + num_rows = self.num_rows + + columns = self.columns + + data: list[dict[str, Any]] | list[list[Any]] + if isinstance(self.data, list): + data = [] + for data_type_0_item_data in self.data: + data_type_0_item = data_type_0_item_data + + data.append(data_type_0_item) + + else: + data = [] + for data_type_1_item_data in self.data: + data_type_1_item = data_type_1_item_data.to_dict() + data.append(data_type_1_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "partition": partition, + "num_rows": num_rows, + "columns": columns, + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_data_response_data_type_1_item import ( + InventoryDataResponseDataType1Item, + ) + + d = dict(src_dict) + partition = d.pop("partition") + + num_rows = d.pop("num_rows") + + columns = cast(list[str], d.pop("columns")) + + def _parse_data( + data: object, + ) -> list[InventoryDataResponseDataType1Item] | list[list[Any]]: + try: + if not isinstance(data, list): + raise TypeError() + data_type_0 = [] + _data_type_0 = data + for data_type_0_item_data in _data_type_0: + data_type_0_item = cast(list[Any], data_type_0_item_data) + + data_type_0.append(data_type_0_item) + + return data_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, list): + raise TypeError() + data_type_1 = [] + _data_type_1 = data + for data_type_1_item_data in _data_type_1: + data_type_1_item = InventoryDataResponseDataType1Item.from_dict( + data_type_1_item_data + ) + + data_type_1.append(data_type_1_item) + + return data_type_1 + + data = _parse_data(d.pop("data")) + + inventory_data_response = cls( + partition=partition, + num_rows=num_rows, + columns=columns, + data=data, + ) + + inventory_data_response.additional_properties = d + return inventory_data_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_data_response_data_type_1_item.py b/fastfuels_sdk/v2/client_library/models/inventory_data_response_data_type_1_item.py new file mode 100644 index 0000000..a82bdb4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_data_response_data_type_1_item.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryDataResponseDataType1Item") + + +@_attrs_define +class InventoryDataResponseDataType1Item: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + inventory_data_response_data_type_1_item = cls() + + inventory_data_response_data_type_1_item.additional_properties = d + return inventory_data_response_data_type_1_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment.py b/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment.py new file mode 100644 index 0000000..ec11248 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_diameter_treatment_method import ( + InventoryDiameterTreatmentMethod, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + +T = TypeVar("T", bound="InventoryDiameterTreatment") + + +@_attrs_define +class InventoryDiameterTreatment: + """Thin to a diameter-at-breast-height limit. + + A hard cutoff: ``from_below`` removes trees smaller than ``value``, + ``from_above`` removes trees larger than ``value``. ``proportional`` does not + apply to a diameter limit and is not an option here. + + Attributes: + method (InventoryDiameterTreatmentMethod): `from_below` removes trees below the limit; `from_above` removes + trees above the limit. + value (float): Diameter-at-breast-height limit, in cm unless `unit` is set. + unit (None | str | Unset): Optional unit for `value`. Must be canonical and dimensionally compatible with the + metric's native unit; converted before the treatment is applied. + conditions (list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] | Unset): Spatial + conditions restricting the treatment to a region (within/outside/intersects a geometry or Feature). An empty + list applies the treatment to the entire inventory. + metric (Literal['diameter'] | Unset): Default: 'diameter'. + """ + + method: InventoryDiameterTreatmentMethod + value: float + unit: None | str | Unset = UNSET + conditions: ( + list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] + | Unset + ) = UNSET + metric: Literal["diameter"] | Unset = "diameter" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + method = self.method.value + + value = self.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + conditions: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.conditions, Unset): + conditions = [] + for conditions_item_data in self.conditions: + conditions_item: dict[str, Any] + if isinstance(conditions_item_data, InventoryGeometrySpatialCondition): + conditions_item = conditions_item_data.to_dict() + else: + conditions_item = conditions_item_data.to_dict() + + conditions.append(conditions_item) + + metric = self.metric + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "method": method, + "value": value, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + if conditions is not UNSET: + field_dict["conditions"] = conditions + if metric is not UNSET: + field_dict["metric"] = metric + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + + d = dict(src_dict) + method = InventoryDiameterTreatmentMethod(d.pop("method")) + + value = d.pop("value") + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + _conditions = d.pop("conditions", UNSET) + conditions: ( + list[InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition] + | Unset + ) = UNSET + if _conditions is not UNSET: + conditions = [] + for conditions_item_data in _conditions: + + def _parse_conditions_item( + data: object, + ) -> ( + InventoryFeatureSpatialCondition | InventoryGeometrySpatialCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_0 = ( + InventoryGeometrySpatialCondition.from_dict(data) + ) + + return conditions_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_1 = InventoryFeatureSpatialCondition.from_dict( + data + ) + + return conditions_item_type_1 + + conditions_item = _parse_conditions_item(conditions_item_data) + + conditions.append(conditions_item) + + metric = cast(Literal["diameter"] | Unset, d.pop("metric", UNSET)) + if metric != "diameter" and not isinstance(metric, Unset): + raise ValueError(f"metric must match const 'diameter', got '{metric}'") + + inventory_diameter_treatment = cls( + method=method, + value=value, + unit=unit, + conditions=conditions, + metric=metric, + ) + + inventory_diameter_treatment.additional_properties = d + return inventory_diameter_treatment + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment_method.py b/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment_method.py new file mode 100644 index 0000000..5641b1c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_diameter_treatment_method.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class InventoryDiameterTreatmentMethod(str, Enum): + FROM_ABOVE = "from_above" + FROM_BELOW = "from_below" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_export_format.py b/fastfuels_sdk/v2/client_library/models/inventory_export_format.py new file mode 100644 index 0000000..f1258c7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_export_format.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class InventoryExportFormat(str, Enum): + CSV = "csv" + GEOJSON = "geojson" + GEOPACKAGE = "geopackage" + PARQUET = "parquet" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_expression_condition.py b/fastfuels_sdk/v2/client_library/models/inventory_expression_condition.py new file mode 100644 index 0000000..941c805 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_expression_condition.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryExpressionCondition") + + +@_attrs_define +class InventoryExpressionCondition: + """Boolean expression condition evaluated against tree attributes. + + Expressions use native units (cm, m, 0-1 fraction). No unit field + is provided — convert values in the expression yourself. + + Example: "dbh < 5 and height < 2" + + Attributes: + expression (str): Boolean expression using dbh, height, crown_ratio + """ + + expression: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + expression = self.expression + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "expression": expression, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + expression = d.pop("expression") + + inventory_expression_condition = cls( + expression=expression, + ) + + inventory_expression_condition.additional_properties = d + return inventory_expression_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_feature_spatial_condition.py b/fastfuels_sdk/v2/client_library/models/inventory_feature_spatial_condition.py new file mode 100644 index 0000000..9263e08 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_feature_spatial_condition.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.spatial_operator import SpatialOperator +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryFeatureSpatialCondition") + + +@_attrs_define +class InventoryFeatureSpatialCondition: + """Spatial condition that tests tree locations against a persisted Feature + resource. + + The referenced Feature must belong to the same domain as the inventory. + The processing service loads the feature's geometry, reprojects it into + the domain CRS, optionally buffers it, and then evaluates the spatial + operator against each tree's point coordinate. + + A non-zero ``buffer_m`` is typically required for linestring features + (e.g. roads), since tree points almost never intersect a bare linestring. + + Attributes: + source (Literal['feature']): Discriminator selecting this variant. Must be the literal string `"feature"`. Use + `"geometry"` instead to supply inline GeoJSON. + operator (SpatialOperator): Spatial relationship operators for geometry-based conditions. + + - within: Select items whose target (centroid or cell) is inside the geometry + - outside: Select items whose target is outside the geometry (inverse of within) + - intersects: Select items whose target overlaps with the geometry + feature_id (str): ID of a Feature resource (road, water, or layerset) hosted in the same domain as the + inventory. Cross-domain references are rejected; the feature must be in `completed` status. + buffer_m (float | None | Unset): Optional buffer distance in meters applied to the feature geometry (in the + domain's projected CRS) before testing. Effectively required for linestring features such as roads since a tree + point almost never intersects a bare linestring. + """ + + source: Literal["feature"] + operator: SpatialOperator + feature_id: str + buffer_m: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + source = self.source + + operator = self.operator.value + + feature_id = self.feature_id + + buffer_m: float | None | Unset + if isinstance(self.buffer_m, Unset): + buffer_m = UNSET + else: + buffer_m = self.buffer_m + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source": source, + "operator": operator, + "feature_id": feature_id, + } + ) + if buffer_m is not UNSET: + field_dict["buffer_m"] = buffer_m + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + source = cast(Literal["feature"], d.pop("source")) + if source != "feature": + raise ValueError(f"source must match const 'feature', got '{source}'") + + operator = SpatialOperator(d.pop("operator")) + + feature_id = d.pop("feature_id") + + def _parse_buffer_m(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + buffer_m = _parse_buffer_m(d.pop("buffer_m", UNSET)) + + inventory_feature_spatial_condition = cls( + source=source, + operator=operator, + feature_id=feature_id, + buffer_m=buffer_m, + ) + + inventory_feature_spatial_condition.additional_properties = d + return inventory_feature_spatial_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition.py b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition.py new file mode 100644 index 0000000..7556759 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.spatial_operator import SpatialOperator +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_geometry_spatial_condition_crs_type_0 import ( + InventoryGeometrySpatialConditionCrsType0, + ) + from ..models.inventory_geometry_spatial_condition_geometry import ( + InventoryGeometrySpatialConditionGeometry, + ) + + +T = TypeVar("T", bound="InventoryGeometrySpatialCondition") + + +@_attrs_define +class InventoryGeometrySpatialCondition: + """Spatial condition that tests tree locations against an inline GeoJSON + geometry. + + Trees are points, so the test is always point-in-(optionally-buffered)-geometry. + Use this variant when the geometry is supplied directly in the request; for a + persisted geometry hosted as a Feature resource, use + ``InventoryFeatureSpatialCondition``. + + Attributes: + source (Literal['geometry']): Discriminator selecting this variant. Must be the literal string `"geometry"`. Use + `"feature"` instead to reference a persisted Feature resource by id. + operator (SpatialOperator): Spatial relationship operators for geometry-based conditions. + + - within: Select items whose target (centroid or cell) is inside the geometry + - outside: Select items whose target is outside the geometry (inverse of within) + - intersects: Select items whose target overlaps with the geometry + geometry (InventoryGeometrySpatialConditionGeometry): Inline GeoJSON geometry. Polygon and MultiPolygon are the + common shapes; LineString geometries should typically be paired with a non-zero `buffer_m` since a tree point + almost never lies exactly on a line. + crs (InventoryGeometrySpatialConditionCrsType0 | None | Unset): CRS of `geometry`, expressed as a GeoJSON CRS + object (`{"type": "name", "properties": {"name": "EPSG:..."}}`). Defaults to the domain CRS when null. + buffer_m (float | None | Unset): Optional buffer distance in meters applied to the geometry (in the domain's + projected CRS) before testing. Use a non-zero buffer to widen the masked region beyond the literal geometry. + """ + + source: Literal["geometry"] + operator: SpatialOperator + geometry: InventoryGeometrySpatialConditionGeometry + crs: InventoryGeometrySpatialConditionCrsType0 | None | Unset = UNSET + buffer_m: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_geometry_spatial_condition_crs_type_0 import ( + InventoryGeometrySpatialConditionCrsType0, + ) + + source = self.source + + operator = self.operator.value + + geometry = self.geometry.to_dict() + + crs: dict[str, Any] | None | Unset + if isinstance(self.crs, Unset): + crs = UNSET + elif isinstance(self.crs, InventoryGeometrySpatialConditionCrsType0): + crs = self.crs.to_dict() + else: + crs = self.crs + + buffer_m: float | None | Unset + if isinstance(self.buffer_m, Unset): + buffer_m = UNSET + else: + buffer_m = self.buffer_m + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "source": source, + "operator": operator, + "geometry": geometry, + } + ) + if crs is not UNSET: + field_dict["crs"] = crs + if buffer_m is not UNSET: + field_dict["buffer_m"] = buffer_m + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_geometry_spatial_condition_crs_type_0 import ( + InventoryGeometrySpatialConditionCrsType0, + ) + from ..models.inventory_geometry_spatial_condition_geometry import ( + InventoryGeometrySpatialConditionGeometry, + ) + + d = dict(src_dict) + source = cast(Literal["geometry"], d.pop("source")) + if source != "geometry": + raise ValueError(f"source must match const 'geometry', got '{source}'") + + operator = SpatialOperator(d.pop("operator")) + + geometry = InventoryGeometrySpatialConditionGeometry.from_dict( + d.pop("geometry") + ) + + def _parse_crs( + data: object, + ) -> InventoryGeometrySpatialConditionCrsType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + crs_type_0 = InventoryGeometrySpatialConditionCrsType0.from_dict(data) + + return crs_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(InventoryGeometrySpatialConditionCrsType0 | None | Unset, data) + + crs = _parse_crs(d.pop("crs", UNSET)) + + def _parse_buffer_m(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + buffer_m = _parse_buffer_m(d.pop("buffer_m", UNSET)) + + inventory_geometry_spatial_condition = cls( + source=source, + operator=operator, + geometry=geometry, + crs=crs, + buffer_m=buffer_m, + ) + + inventory_geometry_spatial_condition.additional_properties = d + return inventory_geometry_spatial_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_crs_type_0.py b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_crs_type_0.py new file mode 100644 index 0000000..dfc6ea2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_crs_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryGeometrySpatialConditionCrsType0") + + +@_attrs_define +class InventoryGeometrySpatialConditionCrsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + inventory_geometry_spatial_condition_crs_type_0 = cls() + + inventory_geometry_spatial_condition_crs_type_0.additional_properties = d + return inventory_geometry_spatial_condition_crs_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_geometry.py b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_geometry.py new file mode 100644 index 0000000..a606aa2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_geometry_spatial_condition_geometry.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryGeometrySpatialConditionGeometry") + + +@_attrs_define +class InventoryGeometrySpatialConditionGeometry: + """Inline GeoJSON geometry. Polygon and MultiPolygon are the common shapes; LineString geometries should typically be + paired with a non-zero `buffer_m` since a tree point almost never lies exactly on a line. + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + inventory_geometry_spatial_condition_geometry = cls() + + inventory_geometry_spatial_condition_geometry.additional_properties = d + return inventory_geometry_spatial_condition_geometry + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_georeference.py b/fastfuels_sdk/v2/client_library/models/inventory_georeference.py new file mode 100644 index 0000000..23fdba5 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_georeference.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryGeoreference") + + +@_attrs_define +class InventoryGeoreference: + """Spatial reference for an inventory, computed from the domain geometry. + + Attributes: + crs (str): + bounds (list[float]): + """ + + crs: str + bounds: list[float] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + bounds = [] + for bounds_item_data in self.bounds: + bounds_item: float + bounds_item = bounds_item_data + bounds.append(bounds_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "bounds": bounds, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + bounds = [] + _bounds = d.pop("bounds") + for bounds_item_data in _bounds: + + def _parse_bounds_item(data: object) -> float: + return cast(float, data) + + bounds_item = _parse_bounds_item(bounds_item_data) + + bounds.append(bounds_item) + + inventory_georeference = cls( + crs=crs, + bounds=bounds, + ) + + inventory_georeference.additional_properties = d + return inventory_georeference + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_json_orientation.py b/fastfuels_sdk/v2/client_library/models/inventory_json_orientation.py new file mode 100644 index 0000000..a7d208d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_json_orientation.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class InventoryJsonOrientation(str, Enum): + RECORDS = "records" + SPLIT = "split" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_modification.py b/fastfuels_sdk/v2/client_library/models/inventory_modification.py new file mode 100644 index 0000000..0651b54 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_modification.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory_expression_condition import InventoryExpressionCondition + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + from ..models.inventory_modification_action import InventoryModificationAction + from ..models.inventory_modification_condition import InventoryModificationCondition + from ..models.remove_action import RemoveAction + + +T = TypeVar("T", bound="InventoryModification") + + +@_attrs_define +class InventoryModification: + """A modification rule: when all conditions match, apply actions. + + If a RemoveAction is present, it must be the only action. + + Attributes: + conditions (list[InventoryExpressionCondition | InventoryFeatureSpatialCondition | + InventoryGeometrySpatialCondition | InventoryModificationCondition]): + actions (list[InventoryModificationAction | RemoveAction]): + """ + + conditions: list[ + InventoryExpressionCondition + | InventoryFeatureSpatialCondition + | InventoryGeometrySpatialCondition + | InventoryModificationCondition + ] + actions: list[InventoryModificationAction | RemoveAction] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.inventory_expression_condition import InventoryExpressionCondition + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + from ..models.inventory_modification_action import InventoryModificationAction + from ..models.inventory_modification_condition import ( + InventoryModificationCondition, + ) + + conditions = [] + for conditions_item_data in self.conditions: + conditions_item: dict[str, Any] + if ( + isinstance(conditions_item_data, InventoryModificationCondition) + or isinstance(conditions_item_data, InventoryExpressionCondition) + or isinstance(conditions_item_data, InventoryGeometrySpatialCondition) + ): + conditions_item = conditions_item_data.to_dict() + else: + conditions_item = conditions_item_data.to_dict() + + conditions.append(conditions_item) + + actions = [] + for actions_item_data in self.actions: + actions_item: dict[str, Any] + if isinstance(actions_item_data, InventoryModificationAction): + actions_item = actions_item_data.to_dict() + else: + actions_item = actions_item_data.to_dict() + + actions.append(actions_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "conditions": conditions, + "actions": actions, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_expression_condition import InventoryExpressionCondition + from ..models.inventory_feature_spatial_condition import ( + InventoryFeatureSpatialCondition, + ) + from ..models.inventory_geometry_spatial_condition import ( + InventoryGeometrySpatialCondition, + ) + from ..models.inventory_modification_action import InventoryModificationAction + from ..models.inventory_modification_condition import ( + InventoryModificationCondition, + ) + from ..models.remove_action import RemoveAction + + d = dict(src_dict) + conditions = [] + _conditions = d.pop("conditions") + for conditions_item_data in _conditions: + + def _parse_conditions_item( + data: object, + ) -> ( + InventoryExpressionCondition + | InventoryFeatureSpatialCondition + | InventoryGeometrySpatialCondition + | InventoryModificationCondition + ): + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_0 = InventoryModificationCondition.from_dict( + data + ) + + return conditions_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_1 = InventoryExpressionCondition.from_dict( + data + ) + + return conditions_item_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_2_type_0 = ( + InventoryGeometrySpatialCondition.from_dict(data) + ) + + return conditions_item_type_2_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + conditions_item_type_2_type_1 = ( + InventoryFeatureSpatialCondition.from_dict(data) + ) + + return conditions_item_type_2_type_1 + + conditions_item = _parse_conditions_item(conditions_item_data) + + conditions.append(conditions_item) + + actions = [] + _actions = d.pop("actions") + for actions_item_data in _actions: + + def _parse_actions_item( + data: object, + ) -> InventoryModificationAction | RemoveAction: + try: + if not isinstance(data, dict): + raise TypeError() + actions_item_type_0 = InventoryModificationAction.from_dict(data) + + return actions_item_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + actions_item_type_1 = RemoveAction.from_dict(data) + + return actions_item_type_1 + + actions_item = _parse_actions_item(actions_item_data) + + actions.append(actions_item) + + inventory_modification = cls( + conditions=conditions, + actions=actions, + ) + + inventory_modification.additional_properties = d + return inventory_modification + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_modification_action.py b/fastfuels_sdk/v2/client_library/models/inventory_modification_action.py new file mode 100644 index 0000000..95d27ad --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_modification_action.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_attribute import InventoryAttribute +from ..models.modifier import Modifier +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryModificationAction") + + +@_attrs_define +class InventoryModificationAction: + """Action that modifies a tree attribute value. + + Optionally specify a unit to convert the value to the attribute's + native unit before applying the modifier. + + Attributes: + attribute (InventoryAttribute): Attributes available for inventory modifications. + modifier (Modifier): Modifiers for modification actions. + value (float | int | str): The value to use with the modifier + unit (None | str | Unset): Optional pint-compatible unit for the value. + """ + + attribute: InventoryAttribute + modifier: Modifier + value: float | int | str + unit: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + attribute = self.attribute.value + + modifier = self.modifier.value + + value: float | int | str + value = self.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "attribute": attribute, + "modifier": modifier, + "value": value, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + attribute = InventoryAttribute(d.pop("attribute")) + + modifier = Modifier(d.pop("modifier")) + + def _parse_value(data: object) -> float | int | str: + return cast(float | int | str, data) + + value = _parse_value(d.pop("value")) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + inventory_modification_action = cls( + attribute=attribute, + modifier=modifier, + value=value, + unit=unit, + ) + + inventory_modification_action.additional_properties = d + return inventory_modification_action + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_modification_condition.py b/fastfuels_sdk/v2/client_library/models/inventory_modification_condition.py new file mode 100644 index 0000000..2af741e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_modification_condition.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.inventory_attribute import InventoryAttribute +from ..models.operator import Operator +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InventoryModificationCondition") + + +@_attrs_define +class InventoryModificationCondition: + """Condition that checks a tree attribute against a value. + + Optionally specify a unit (e.g., "in", "ft") to convert the value + to the attribute's native unit before comparison. + + Attributes: + attribute (InventoryAttribute): Attributes available for inventory modifications. + operator (Operator): Comparison operators for attribute-based conditions. + value (float | int | list[float | int | str] | str): The value(s) to compare against + unit (None | str | Unset): Optional pint-compatible unit for the value (e.g., 'in', 'ft', 'mm'). Converted to + the attribute's native unit before comparison. + """ + + attribute: InventoryAttribute + operator: Operator + value: float | int | list[float | int | str] | str + unit: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + attribute = self.attribute.value + + operator = self.operator.value + + value: float | int | list[float | int | str] | str + if isinstance(self.value, list): + value = [] + for value_type_3_item_data in self.value: + value_type_3_item: float | int | str + value_type_3_item = value_type_3_item_data + value.append(value_type_3_item) + + else: + value = self.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "attribute": attribute, + "operator": operator, + "value": value, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + attribute = InventoryAttribute(d.pop("attribute")) + + operator = Operator(d.pop("operator")) + + def _parse_value(data: object) -> float | int | list[float | int | str] | str: + try: + if not isinstance(data, list): + raise TypeError() + value_type_3 = [] + _value_type_3 = data + for value_type_3_item_data in _value_type_3: + + def _parse_value_type_3_item(data: object) -> float | int | str: + return cast(float | int | str, data) + + value_type_3_item = _parse_value_type_3_item(value_type_3_item_data) + + value_type_3.append(value_type_3_item) + + return value_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(float | int | list[float | int | str] | str, data) + + value = _parse_value(d.pop("value")) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + inventory_modification_condition = cls( + attribute=attribute, + operator=operator, + value=value, + unit=unit, + ) + + inventory_modification_condition.additional_properties = d + return inventory_modification_condition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_partition_info.py b/fastfuels_sdk/v2/client_library/models/inventory_partition_info.py new file mode 100644 index 0000000..0e85381 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_partition_info.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryPartitionInfo") + + +@_attrs_define +class InventoryPartitionInfo: + """ + Attributes: + index (int): + num_rows (int): + """ + + index: int + num_rows: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + index = self.index + + num_rows = self.num_rows + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "index": index, + "num_rows": num_rows, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + index = d.pop("index") + + num_rows = d.pop("num_rows") + + inventory_partition_info = cls( + index=index, + num_rows=num_rows, + ) + + inventory_partition_info.additional_properties = d + return inventory_partition_info + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_sort_field.py b/fastfuels_sdk/v2/client_library/models/inventory_sort_field.py new file mode 100644 index 0000000..d1e3323 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InventorySortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_source.py b/fastfuels_sdk/v2/client_library/models/inventory_source.py new file mode 100644 index 0000000..8d2eaff --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_source.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventorySource") + + +@_attrs_define +class InventorySource: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + inventory_source = cls() + + inventory_source.additional_properties = d + return inventory_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_treatment_method.py b/fastfuels_sdk/v2/client_library/models/inventory_treatment_method.py new file mode 100644 index 0000000..5eb416a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_treatment_method.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InventoryTreatmentMethod(str, Enum): + FROM_ABOVE = "from_above" + FROM_BELOW = "from_below" + PROPORTIONAL = "proportional" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_type.py b/fastfuels_sdk/v2/client_library/models/inventory_type.py new file mode 100644 index 0000000..b158ca4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_type.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class InventoryType(str, Enum): + TREE = "tree" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_upload_created_response.py b/fastfuels_sdk/v2/client_library/models/inventory_upload_created_response.py new file mode 100644 index 0000000..6eefcf8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_upload_created_response.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory import Inventory + from ..models.inventory_upload_spec import InventoryUploadSpec + + +T = TypeVar("T", bound="InventoryUploadCreatedResponse") + + +@_attrs_define +class InventoryUploadCreatedResponse: + """ + Attributes: + inventory (Inventory): The Inventory resource. + + When status is "pending" or "running", georeference will be null. + The backend populates it after successfully processing data, + at which point status transitions to "completed". + upload (InventoryUploadSpec): + """ + + inventory: Inventory + upload: InventoryUploadSpec + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + inventory = self.inventory.to_dict() + + upload = self.upload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "inventory": inventory, + "upload": upload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory import Inventory + from ..models.inventory_upload_spec import InventoryUploadSpec + + d = dict(src_dict) + inventory = Inventory.from_dict(d.pop("inventory")) + + upload = InventoryUploadSpec.from_dict(d.pop("upload")) + + inventory_upload_created_response = cls( + inventory=inventory, + upload=upload, + ) + + inventory_upload_created_response.additional_properties = d + return inventory_upload_created_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_upload_format.py b/fastfuels_sdk/v2/client_library/models/inventory_upload_format.py new file mode 100644 index 0000000..3a02930 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_upload_format.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InventoryUploadFormat(str, Enum): + CSV = "csv" + GEOJSON = "geojson" + GEOPACKAGE = "geopackage" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/inventory_upload_spec.py b/fastfuels_sdk/v2/client_library/models/inventory_upload_spec.py new file mode 100644 index 0000000..f33710a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_upload_spec.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.inventory_upload_spec_headers import InventoryUploadSpecHeaders + + +T = TypeVar("T", bound="InventoryUploadSpec") + + +@_attrs_define +class InventoryUploadSpec: + """ + Attributes: + url (str): + headers (InventoryUploadSpecHeaders): + content_type (str): + expires_at (datetime.datetime): + max_size_bytes (int): + method (Literal['PUT'] | Unset): Default: 'PUT'. + """ + + url: str + headers: InventoryUploadSpecHeaders + content_type: str + expires_at: datetime.datetime + max_size_bytes: int + method: Literal["PUT"] | Unset = "PUT" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + headers = self.headers.to_dict() + + content_type = self.content_type + + expires_at = self.expires_at.isoformat() + + max_size_bytes = self.max_size_bytes + + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "url": url, + "headers": headers, + "content_type": content_type, + "expires_at": expires_at, + "max_size_bytes": max_size_bytes, + } + ) + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory_upload_spec_headers import InventoryUploadSpecHeaders + + d = dict(src_dict) + url = d.pop("url") + + headers = InventoryUploadSpecHeaders.from_dict(d.pop("headers")) + + content_type = d.pop("content_type") + + expires_at = datetime.datetime.fromisoformat(d.pop("expires_at")) + + max_size_bytes = d.pop("max_size_bytes") + + method = cast(Literal["PUT"] | Unset, d.pop("method", UNSET)) + if method != "PUT" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'PUT', got '{method}'") + + inventory_upload_spec = cls( + url=url, + headers=headers, + content_type=content_type, + expires_at=expires_at, + max_size_bytes=max_size_bytes, + method=method, + ) + + inventory_upload_spec.additional_properties = d + return inventory_upload_spec + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/inventory_upload_spec_headers.py b/fastfuels_sdk/v2/client_library/models/inventory_upload_spec_headers.py new file mode 100644 index 0000000..1b2bcc1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/inventory_upload_spec_headers.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InventoryUploadSpecHeaders") + + +@_attrs_define +class InventoryUploadSpecHeaders: + """ """ + + additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + inventory_upload_spec_headers = cls() + + inventory_upload_spec_headers.additional_properties = d + return inventory_upload_spec_headers + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> str: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: str) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/job_error.py b/fastfuels_sdk/v2/client_library/models/job_error.py new file mode 100644 index 0000000..e5086cb --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/job_error.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="JobError") + + +@_attrs_define +class JobError: + """Error information for failed async jobs. + + Provides structured error information with user-facing messages and + developer debugging information. The traceback is stored in Firestore + but excluded from API responses. + + Attributes: + code: Machine-readable error code for programmatic handling. + Examples: "LANDFIRE_COVERAGE_ERROR", "SOURCE_GRID_NOT_FOUND" + message: User-friendly explanation of what went wrong. + suggestion: Optional actionable advice for resolving the error. + traceback: Full Python stack trace for debugging. Stored in Firestore + but not included in API responses. + + Attributes: + code (str): Machine-readable error code + message (str): User-friendly error message + suggestion (None | str | Unset): Actionable suggestion for the user + traceback (None | str | Unset): Full stack trace (stored but not exposed in API responses) + """ + + code: str + message: str + suggestion: None | str | Unset = UNSET + traceback: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + code = self.code + + message = self.message + + suggestion: None | str | Unset + if isinstance(self.suggestion, Unset): + suggestion = UNSET + else: + suggestion = self.suggestion + + traceback: None | str | Unset + if isinstance(self.traceback, Unset): + traceback = UNSET + else: + traceback = self.traceback + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "code": code, + "message": message, + } + ) + if suggestion is not UNSET: + field_dict["suggestion"] = suggestion + if traceback is not UNSET: + field_dict["traceback"] = traceback + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + code = d.pop("code") + + message = d.pop("message") + + def _parse_suggestion(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + suggestion = _parse_suggestion(d.pop("suggestion", UNSET)) + + def _parse_traceback(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + traceback = _parse_traceback(d.pop("traceback", UNSET)) + + job_error = cls( + code=code, + message=message, + suggestion=suggestion, + traceback=traceback, + ) + + job_error.additional_properties = d + return job_error + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/job_progress.py b/fastfuels_sdk/v2/client_library/models/job_progress.py new file mode 100644 index 0000000..39a12d2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/job_progress.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="JobProgress") + + +@_attrs_define +class JobProgress: + """Progress information for running async jobs. + + Provides real-time feedback during long-running operations. + + Attributes: + percent: Completion percentage (0-100). Null for indeterminate operations + like "Connecting to LANDFIRE..." where progress can't be quantified. + message: Human-readable status message describing what's happening. + + Attributes: + message (str): Human-readable status message, e.g. 'Fetching LANDFIRE data...' + percent (int | None | Unset): Completion percentage (0-100), null if indeterminate + """ + + message: str + percent: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + message = self.message + + percent: int | None | Unset + if isinstance(self.percent, Unset): + percent = UNSET + else: + percent = self.percent + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "message": message, + } + ) + if percent is not UNSET: + field_dict["percent"] = percent + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + message = d.pop("message") + + def _parse_percent(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + percent = _parse_percent(d.pop("percent", UNSET)) + + job_progress = cls( + message=message, + percent=percent, + ) + + job_progress.additional_properties = d + return job_progress + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/job_resource_usage.py b/fastfuels_sdk/v2/client_library/models/job_resource_usage.py new file mode 100644 index 0000000..fc77e1a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/job_resource_usage.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.usage_count import UsageCount + from ..models.usage_storage import UsageStorage + + +T = TypeVar("T", bound="JobResourceUsage") + + +@_attrs_define +class JobResourceUsage: + """Usage for a resource type that produces jobs and stores artifacts. + + Attributes: + active (UsageCount): A count-based usage/limit pair (resources or concurrent jobs). + total (UsageCount): A count-based usage/limit pair (resources or concurrent jobs). + storage (UsageStorage): A storage usage/limit pair, in bytes. + """ + + active: UsageCount + total: UsageCount + storage: UsageStorage + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + active = self.active.to_dict() + + total = self.total.to_dict() + + storage = self.storage.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "active": active, + "total": total, + "storage": storage, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.usage_count import UsageCount + from ..models.usage_storage import UsageStorage + + d = dict(src_dict) + active = UsageCount.from_dict(d.pop("active")) + + total = UsageCount.from_dict(d.pop("total")) + + storage = UsageStorage.from_dict(d.pop("storage")) + + job_resource_usage = cls( + active=active, + total=total, + storage=storage, + ) + + job_resource_usage.additional_properties = d + return job_resource_usage + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/job_status.py b/fastfuels_sdk/v2/client_library/models/job_status.py new file mode 100644 index 0000000..f4045d2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/job_status.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class JobStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + RUNNING = "running" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/key.py b/fastfuels_sdk/v2/client_library/models/key.py new file mode 100644 index 0000000..59fb99d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/key.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.access import Access +from ..models.scope import Scope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Key") + + +@_attrs_define +class Key: + """Represents an API key for authenticating programmatic API access. + + Attributes: + id (str): Unique identifier for the key (SHA-256 hash of the secret). + owner_id (str): The unique ID of the user or application who owns the key. + creator_id (str): The unique ID of the human user who created the key. + name (str): A name to semantically identify the key. + description (None | str | Unset): An optional description of the key's purpose. + valid_days (int | Unset): Number of days for which this key will be valid. Default: 30. + scopes (list[Scope] | Unset): A list of scopes available to the key. + access (Access | Unset): Access types for an API key. + application_id (None | str | Unset): Application ID accessed by the API key. + created_on (datetime.datetime | Unset): The date and time the key was created. + expires_on (datetime.datetime | Unset): The date at which this key is no longer valid. + """ + + id: str + owner_id: str + creator_id: str + name: str + description: None | str | Unset = UNSET + valid_days: int | Unset = 30 + scopes: list[Scope] | Unset = UNSET + access: Access | Unset = UNSET + application_id: None | str | Unset = UNSET + created_on: datetime.datetime | Unset = UNSET + expires_on: datetime.datetime | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + owner_id = self.owner_id + + creator_id = self.creator_id + + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + valid_days = self.valid_days + + scopes: list[str] | Unset = UNSET + if not isinstance(self.scopes, Unset): + scopes = [] + for scopes_item_data in self.scopes: + scopes_item = scopes_item_data.value + scopes.append(scopes_item) + + access: str | Unset = UNSET + if not isinstance(self.access, Unset): + access = self.access.value + + application_id: None | str | Unset + if isinstance(self.application_id, Unset): + application_id = UNSET + else: + application_id = self.application_id + + created_on: str | Unset = UNSET + if not isinstance(self.created_on, Unset): + created_on = self.created_on.isoformat() + + expires_on: str | Unset = UNSET + if not isinstance(self.expires_on, Unset): + expires_on = self.expires_on.isoformat() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "owner_id": owner_id, + "creator_id": creator_id, + "name": name, + } + ) + if description is not UNSET: + field_dict["description"] = description + if valid_days is not UNSET: + field_dict["valid_days"] = valid_days + if scopes is not UNSET: + field_dict["scopes"] = scopes + if access is not UNSET: + field_dict["access"] = access + if application_id is not UNSET: + field_dict["application_id"] = application_id + if created_on is not UNSET: + field_dict["created_on"] = created_on + if expires_on is not UNSET: + field_dict["expires_on"] = expires_on + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + id = d.pop("id") + + owner_id = d.pop("owner_id") + + creator_id = d.pop("creator_id") + + name = d.pop("name") + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + valid_days = d.pop("valid_days", UNSET) + + _scopes = d.pop("scopes", UNSET) + scopes: list[Scope] | Unset = UNSET + if _scopes is not UNSET: + scopes = [] + for scopes_item_data in _scopes: + scopes_item = Scope(scopes_item_data) + + scopes.append(scopes_item) + + _access = d.pop("access", UNSET) + access: Access | Unset + if isinstance(_access, Unset): + access = UNSET + else: + access = Access(_access) + + def _parse_application_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + application_id = _parse_application_id(d.pop("application_id", UNSET)) + + _created_on = d.pop("created_on", UNSET) + created_on: datetime.datetime | Unset + if isinstance(_created_on, Unset): + created_on = UNSET + else: + created_on = datetime.datetime.fromisoformat(_created_on) + + _expires_on = d.pop("expires_on", UNSET) + expires_on: datetime.datetime | Unset + if isinstance(_expires_on, Unset): + expires_on = UNSET + else: + expires_on = datetime.datetime.fromisoformat(_expires_on) + + key = cls( + id=id, + owner_id=owner_id, + creator_id=creator_id, + name=name, + description=description, + valid_days=valid_days, + scopes=scopes, + access=access, + application_id=application_id, + created_on=created_on, + expires_on=expires_on, + ) + + key.additional_properties = d + return key + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/landfire_canopy_fuel_band.py b/fastfuels_sdk/v2/client_library/models/landfire_canopy_fuel_band.py new file mode 100644 index 0000000..3769c03 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_canopy_fuel_band.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class LandfireCanopyFuelBand(str, Enum): + CBD = "cbd" + CBH = "cbh" + CC = "cc" + CHM = "chm" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landfire_canopy_version.py b/fastfuels_sdk/v2/client_library/models/landfire_canopy_version.py new file mode 100644 index 0000000..d4e4f60 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_canopy_version.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class LandfireCanopyVersion(str, Enum): + VALUE_0 = "2024" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landfire_fbfm_13_version.py b/fastfuels_sdk/v2/client_library/models/landfire_fbfm_13_version.py new file mode 100644 index 0000000..72f2ec6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_fbfm_13_version.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class LandfireFbfm13Version(str, Enum): + VALUE_0 = "2023" + VALUE_1 = "2024" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landfire_fbfm_40_version.py b/fastfuels_sdk/v2/client_library/models/landfire_fbfm_40_version.py new file mode 100644 index 0000000..30d20d4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_fbfm_40_version.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class LandfireFbfm40Version(str, Enum): + VALUE_0 = "2019" + VALUE_1 = "2020" + VALUE_2 = "2022" + VALUE_3 = "2023" + VALUE_4 = "2024" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landfire_fccs_version.py b/fastfuels_sdk/v2/client_library/models/landfire_fccs_version.py new file mode 100644 index 0000000..2324894 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_fccs_version.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class LandfireFccsVersion(str, Enum): + VALUE_0 = "2023" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landfire_topography_version.py b/fastfuels_sdk/v2/client_library/models/landfire_topography_version.py new file mode 100644 index 0000000..fa9ffff --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landfire_topography_version.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class LandfireTopographyVersion(str, Enum): + VALUE_0 = "2020" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_domain_target.py b/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_domain_target.py new file mode 100644 index 0000000..a1bf524 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_domain_target.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LandscapeExportAlignmentDomainTarget") + + +@_attrs_define +class LandscapeExportAlignmentDomainTarget: + """Anchor the landscape to the Domain bounding box. + + Output cells tile the Domain bbox at `resolution`, padded outward if the + bbox isn't already a whole multiple. The default 30 m matches LANDFIRE's + native resolution. + + Attributes: + target (Literal['domain'] | Unset): Default: 'domain'. + resolution (float | Unset): Landscape cell size in meters. Defaults to 30 m, LANDFIRE's native resolution. + Default: 30.0. + """ + + target: Literal["domain"] | Unset = "domain" + resolution: float | Unset = 30.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + resolution = self.resolution + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if target is not UNSET: + field_dict["target"] = target + if resolution is not UNSET: + field_dict["resolution"] = resolution + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["domain"] | Unset, d.pop("target", UNSET)) + if target != "domain" and not isinstance(target, Unset): + raise ValueError(f"target must match const 'domain', got '{target}'") + + resolution = d.pop("resolution", UNSET) + + landscape_export_alignment_domain_target = cls( + target=target, + resolution=resolution, + ) + + landscape_export_alignment_domain_target.additional_properties = d + return landscape_export_alignment_domain_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_grid_target.py b/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_grid_target.py new file mode 100644 index 0000000..6b8143e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landscape_export_alignment_grid_target.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LandscapeExportAlignmentGridTarget") + + +@_attrs_define +class LandscapeExportAlignmentGridTarget: + """Anchor the landscape to an existing grid's lattice. + + Useful when role grids share a non-Domain-anchored lattice (e.g. all + chained off a `target="native"` master grid). The referenced grid's + CRS, transform, and shape become the landscape lattice. + + Attributes: + target (Literal['grid']): + grid_id (str): Existing grid whose lattice (CRS, transform, shape) the landscape should match exactly. + """ + + target: Literal["grid"] + grid_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + grid_id = self.grid_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "target": target, + "grid_id": grid_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["grid"], d.pop("target")) + if target != "grid": + raise ValueError(f"target must match const 'grid', got '{target}'") + + grid_id = d.pop("grid_id") + + landscape_export_alignment_grid_target = cls( + target=target, + grid_id=grid_id, + ) + + landscape_export_alignment_grid_target.additional_properties = d + return landscape_export_alignment_grid_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/landscape_export_request.py b/fastfuels_sdk/v2/client_library/models/landscape_export_request.py new file mode 100644 index 0000000..19c5825 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landscape_export_request.py @@ -0,0 +1,254 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.landscape_export_request_fire_behavior_fuel_model import ( + LandscapeExportRequestFireBehaviorFuelModel, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.landscape_export_alignment_domain_target import ( + LandscapeExportAlignmentDomainTarget, + ) + from ..models.landscape_export_alignment_grid_target import ( + LandscapeExportAlignmentGridTarget, + ) + from ..models.landscape_field_source import LandscapeFieldSource + + +T = TypeVar("T", bound="LandscapeExportRequest") + + +@_attrs_define +class LandscapeExportRequest: + """Request body for creating a landscape export. + + Eight required roles produce an 8-band landscape GeoTIFF in LANDFIRE band + order: elevation, slope, aspect, fuel model, canopy cover, canopy height, + canopy base height, canopy bulk density. This is the shape modern fire + behavior tools consume — IFTDSS requires all eight bands for upload. + + The landscape lattice is defined by the `alignment` field — either the + Domain bounding box tiled at `resolution` (default 30 m, LANDFIRE-native), + or the lattice of an existing grid. Every role grid must be lattice-aligned + to the landscape and cover its full extent; otherwise the request is + rejected with 422. The exporter only crops oversized roles by integer + slicing — it never resamples or reprojects. To change a grid's resolution + or anchor, use `POST /v2/domains/{domain_id}/grids/{grid_id}/resample`. + + Attributes: + fire_behavior_fuel_model (LandscapeExportRequestFireBehaviorFuelModel): How the `fuel_model` band's codes should + be interpreted: `'fbfm40'` (Scott-Burgan 40) or `'fbfm13'` (Anderson 13). Recorded in the landscape file so fire + behavior tools apply the right classification. + elevation (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + slope (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + aspect (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + fuel_model (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + canopy_cover (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + canopy_height (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + canopy_base_height (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + canopy_bulk_density (LandscapeFieldSource): A single landscape band drawn from one band on one grid. + alignment (LandscapeExportAlignmentDomainTarget | LandscapeExportAlignmentGridTarget | Unset): How the landscape + lattice is defined. Discriminated by `target`: `'domain'` (default) tiles the Domain bbox at `resolution`; + `'grid'` matches an existing grid's lattice exactly. Omit for the default Domain-anchored 30 m landscape. + expiration_days (int | Unset): Days until the signed download URL expires (max 7). Default: 7. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + fire_behavior_fuel_model: LandscapeExportRequestFireBehaviorFuelModel + elevation: LandscapeFieldSource + slope: LandscapeFieldSource + aspect: LandscapeFieldSource + fuel_model: LandscapeFieldSource + canopy_cover: LandscapeFieldSource + canopy_height: LandscapeFieldSource + canopy_base_height: LandscapeFieldSource + canopy_bulk_density: LandscapeFieldSource + alignment: ( + LandscapeExportAlignmentDomainTarget + | LandscapeExportAlignmentGridTarget + | Unset + ) = UNSET + expiration_days: int | Unset = 7 + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.landscape_export_alignment_domain_target import ( + LandscapeExportAlignmentDomainTarget, + ) + + fire_behavior_fuel_model = self.fire_behavior_fuel_model.value + + elevation = self.elevation.to_dict() + + slope = self.slope.to_dict() + + aspect = self.aspect.to_dict() + + fuel_model = self.fuel_model.to_dict() + + canopy_cover = self.canopy_cover.to_dict() + + canopy_height = self.canopy_height.to_dict() + + canopy_base_height = self.canopy_base_height.to_dict() + + canopy_bulk_density = self.canopy_bulk_density.to_dict() + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, LandscapeExportAlignmentDomainTarget): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + expiration_days = self.expiration_days + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "fire_behavior_fuel_model": fire_behavior_fuel_model, + "elevation": elevation, + "slope": slope, + "aspect": aspect, + "fuel_model": fuel_model, + "canopy_cover": canopy_cover, + "canopy_height": canopy_height, + "canopy_base_height": canopy_base_height, + "canopy_bulk_density": canopy_bulk_density, + } + ) + if alignment is not UNSET: + field_dict["alignment"] = alignment + if expiration_days is not UNSET: + field_dict["expiration_days"] = expiration_days + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.landscape_export_alignment_domain_target import ( + LandscapeExportAlignmentDomainTarget, + ) + from ..models.landscape_export_alignment_grid_target import ( + LandscapeExportAlignmentGridTarget, + ) + from ..models.landscape_field_source import LandscapeFieldSource + + d = dict(src_dict) + fire_behavior_fuel_model = LandscapeExportRequestFireBehaviorFuelModel( + d.pop("fire_behavior_fuel_model") + ) + + elevation = LandscapeFieldSource.from_dict(d.pop("elevation")) + + slope = LandscapeFieldSource.from_dict(d.pop("slope")) + + aspect = LandscapeFieldSource.from_dict(d.pop("aspect")) + + fuel_model = LandscapeFieldSource.from_dict(d.pop("fuel_model")) + + canopy_cover = LandscapeFieldSource.from_dict(d.pop("canopy_cover")) + + canopy_height = LandscapeFieldSource.from_dict(d.pop("canopy_height")) + + canopy_base_height = LandscapeFieldSource.from_dict(d.pop("canopy_base_height")) + + canopy_bulk_density = LandscapeFieldSource.from_dict( + d.pop("canopy_bulk_density") + ) + + def _parse_alignment( + data: object, + ) -> ( + LandscapeExportAlignmentDomainTarget + | LandscapeExportAlignmentGridTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = LandscapeExportAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = LandscapeExportAlignmentGridTarget.from_dict(data) + + return alignment_type_1 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + expiration_days = d.pop("expiration_days", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + landscape_export_request = cls( + fire_behavior_fuel_model=fire_behavior_fuel_model, + elevation=elevation, + slope=slope, + aspect=aspect, + fuel_model=fuel_model, + canopy_cover=canopy_cover, + canopy_height=canopy_height, + canopy_base_height=canopy_base_height, + canopy_bulk_density=canopy_bulk_density, + alignment=alignment, + expiration_days=expiration_days, + name=name, + description=description, + tags=tags, + ) + + landscape_export_request.additional_properties = d + return landscape_export_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/landscape_export_request_fire_behavior_fuel_model.py b/fastfuels_sdk/v2/client_library/models/landscape_export_request_fire_behavior_fuel_model.py new file mode 100644 index 0000000..2d9a922 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landscape_export_request_fire_behavior_fuel_model.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class LandscapeExportRequestFireBehaviorFuelModel(str, Enum): + FBFM13 = "fbfm13" + FBFM40 = "fbfm40" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/landscape_field_source.py b/fastfuels_sdk/v2/client_library/models/landscape_field_source.py new file mode 100644 index 0000000..1456dd8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/landscape_field_source.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LandscapeFieldSource") + + +@_attrs_define +class LandscapeFieldSource: + """A single landscape band drawn from one band on one grid. + + Attributes: + grid_id (str): Grid containing the source band. + band (str): Band key on that grid (e.g. 'elevation'). + """ + + grid_id: str + band: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + grid_id = self.grid_id + + band = self.band + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "grid_id": grid_id, + "band": band, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + grid_id = d.pop("grid_id") + + band = d.pop("band") + + landscape_field_source = cls( + grid_id=grid_id, + band=band, + ) + + landscape_field_source.additional_properties = d + return landscape_field_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/layerset_crs.py b/fastfuels_sdk/v2/client_library/models/layerset_crs.py new file mode 100644 index 0000000..b0a0d0f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/layerset_crs.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.layerset_crs_properties import LayersetCrsProperties + + +T = TypeVar("T", bound="LayersetCrs") + + +@_attrs_define +class LayersetCrs: + """Optional GeoJSON crs block. + + Per RFC 7946, ``crs`` is deprecated at the GeoJSON level (and + ``geojson_pydantic`` therefore omits it), but the team's pipeline emits + it and downstream consumers (geopandas, this server) read it to anchor + bounds and the projected-CRS check in the upload router. + + Attributes: + type_ (str | Unset): Default: 'name'. + properties (LayersetCrsProperties | Unset): + """ + + type_: str | Unset = "name" + properties: LayersetCrsProperties | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + properties: dict[str, Any] | Unset = UNSET + if not isinstance(self.properties, Unset): + properties = self.properties.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if type_ is not UNSET: + field_dict["type"] = type_ + if properties is not UNSET: + field_dict["properties"] = properties + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.layerset_crs_properties import LayersetCrsProperties + + d = dict(src_dict) + type_ = d.pop("type", UNSET) + + _properties = d.pop("properties", UNSET) + properties: LayersetCrsProperties | Unset + if isinstance(_properties, Unset): + properties = UNSET + else: + properties = LayersetCrsProperties.from_dict(_properties) + + layerset_crs = cls( + type_=type_, + properties=properties, + ) + + layerset_crs.additional_properties = d + return layerset_crs + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/layerset_crs_properties.py b/fastfuels_sdk/v2/client_library/models/layerset_crs_properties.py new file mode 100644 index 0000000..7376139 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/layerset_crs_properties.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LayersetCrsProperties") + + +@_attrs_define +class LayersetCrsProperties: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + layerset_crs_properties = cls() + + layerset_crs_properties.additional_properties = d + return layerset_crs_properties + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/layerset_feature.py b/fastfuels_sdk/v2/client_library/models/layerset_feature.py new file mode 100644 index 0000000..ea8cb02 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/layerset_feature.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.layerset_properties import LayersetProperties + from ..models.multi_polygon import MultiPolygon + from ..models.polygon import Polygon + + +T = TypeVar("T", bound="LayersetFeature") + + +@_attrs_define +class LayersetFeature: + """One Feature in the layerset FeatureCollection. + + Inherits coordinate validation from ``geojson_pydantic``. Both + ``Polygon`` and ``MultiPolygon`` are accepted because standard tooling + (QGIS, GDAL, geopandas) emits ``Polygon`` for single-ring features and + ``MultiPolygon`` for multi-ring ones. ``properties`` is narrowed to + non-Optional because every fuelbed row must carry the rasterizer's + required columns. + + Attributes: + type_ (Literal['Feature']): + geometry (MultiPolygon | None | Polygon): + properties (LayersetProperties): Per-feature properties — one row of input to ``rasterize_layerset``. + + Required fields match the rasterizer's required input columns. Optional + fields map to the rasterizer's optional bands; omitting them leaves the + corresponding output band as NaN. + bbox (list[float] | None | Unset): + id (int | None | str | Unset): + """ + + type_: Literal["Feature"] + geometry: MultiPolygon | None | Polygon + properties: LayersetProperties + bbox: list[float] | None | Unset = UNSET + id: int | None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.multi_polygon import MultiPolygon + from ..models.polygon import Polygon + + type_ = self.type_ + + geometry: dict[str, Any] | None + if isinstance(self.geometry, Polygon) or isinstance( + self.geometry, MultiPolygon + ): + geometry = self.geometry.to_dict() + else: + geometry = self.geometry + + properties = self.properties.to_dict() + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + id: int | None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "geometry": geometry, + "properties": properties, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + if id is not UNSET: + field_dict["id"] = id + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.layerset_properties import LayersetProperties + from ..models.multi_polygon import MultiPolygon + from ..models.polygon import Polygon + + d = dict(src_dict) + type_ = cast(Literal["Feature"], d.pop("type")) + if type_ != "Feature": + raise ValueError(f"type must match const 'Feature', got '{type_}'") + + def _parse_geometry(data: object) -> MultiPolygon | None | Polygon: + if data is None: + return data + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_0 = Polygon.from_dict(data) + + return geometry_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + geometry_type_1 = MultiPolygon.from_dict(data) + + return geometry_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(MultiPolygon | None | Polygon, data) + + geometry = _parse_geometry(d.pop("geometry")) + + properties = LayersetProperties.from_dict(d.pop("properties")) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + def _parse_id(data: object) -> int | None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + layerset_feature = cls( + type_=type_, + geometry=geometry, + properties=properties, + bbox=bbox, + id=id, + ) + + layerset_feature.additional_properties = d + return layerset_feature + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/layerset_properties.py b/fastfuels_sdk/v2/client_library/models/layerset_properties.py new file mode 100644 index 0000000..a005fbe --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/layerset_properties.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.distribution import Distribution +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LayersetProperties") + + +@_attrs_define +class LayersetProperties: + """Per-feature properties — one row of input to ``rasterize_layerset``. + + Required fields match the rasterizer's required input columns. Optional + fields map to the rasterizer's optional bands; omitting them leaves the + corresponding output band as NaN. + + Attributes: + fuel_type (str): + fuel_loading (float): + fuel_height (float): + percent_cover (float): + distribution (Distribution): Per-cell spatial-distribution mode for a fuelbed. + + Mirrors ``fastfuels_core.rasterize_layerset``'s ``distribution`` column. + strata_fb (None | str | Unset): + patch_size (float | None | Unset): + live_fuel_moisture (float | None | Unset): + dead_fuel_moisture (float | None | Unset): + heat_of_combustion (float | None | Unset): + patch_std_dev (float | None | Unset): + """ + + fuel_type: str + fuel_loading: float + fuel_height: float + percent_cover: float + distribution: Distribution + strata_fb: None | str | Unset = UNSET + patch_size: float | None | Unset = UNSET + live_fuel_moisture: float | None | Unset = UNSET + dead_fuel_moisture: float | None | Unset = UNSET + heat_of_combustion: float | None | Unset = UNSET + patch_std_dev: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + fuel_type = self.fuel_type + + fuel_loading = self.fuel_loading + + fuel_height = self.fuel_height + + percent_cover = self.percent_cover + + distribution = self.distribution.value + + strata_fb: None | str | Unset + if isinstance(self.strata_fb, Unset): + strata_fb = UNSET + else: + strata_fb = self.strata_fb + + patch_size: float | None | Unset + if isinstance(self.patch_size, Unset): + patch_size = UNSET + else: + patch_size = self.patch_size + + live_fuel_moisture: float | None | Unset + if isinstance(self.live_fuel_moisture, Unset): + live_fuel_moisture = UNSET + else: + live_fuel_moisture = self.live_fuel_moisture + + dead_fuel_moisture: float | None | Unset + if isinstance(self.dead_fuel_moisture, Unset): + dead_fuel_moisture = UNSET + else: + dead_fuel_moisture = self.dead_fuel_moisture + + heat_of_combustion: float | None | Unset + if isinstance(self.heat_of_combustion, Unset): + heat_of_combustion = UNSET + else: + heat_of_combustion = self.heat_of_combustion + + patch_std_dev: float | None | Unset + if isinstance(self.patch_std_dev, Unset): + patch_std_dev = UNSET + else: + patch_std_dev = self.patch_std_dev + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "fuel_type": fuel_type, + "fuel_loading": fuel_loading, + "fuel_height": fuel_height, + "percent_cover": percent_cover, + "distribution": distribution, + } + ) + if strata_fb is not UNSET: + field_dict["strata_fb"] = strata_fb + if patch_size is not UNSET: + field_dict["patch_size"] = patch_size + if live_fuel_moisture is not UNSET: + field_dict["live_fuel_moisture"] = live_fuel_moisture + if dead_fuel_moisture is not UNSET: + field_dict["dead_fuel_moisture"] = dead_fuel_moisture + if heat_of_combustion is not UNSET: + field_dict["heat_of_combustion"] = heat_of_combustion + if patch_std_dev is not UNSET: + field_dict["patch_std_dev"] = patch_std_dev + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + fuel_type = d.pop("fuel_type") + + fuel_loading = d.pop("fuel_loading") + + fuel_height = d.pop("fuel_height") + + percent_cover = d.pop("percent_cover") + + distribution = Distribution(d.pop("distribution")) + + def _parse_strata_fb(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + strata_fb = _parse_strata_fb(d.pop("strata_fb", UNSET)) + + def _parse_patch_size(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + patch_size = _parse_patch_size(d.pop("patch_size", UNSET)) + + def _parse_live_fuel_moisture(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + live_fuel_moisture = _parse_live_fuel_moisture( + d.pop("live_fuel_moisture", UNSET) + ) + + def _parse_dead_fuel_moisture(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + dead_fuel_moisture = _parse_dead_fuel_moisture( + d.pop("dead_fuel_moisture", UNSET) + ) + + def _parse_heat_of_combustion(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + heat_of_combustion = _parse_heat_of_combustion( + d.pop("heat_of_combustion", UNSET) + ) + + def _parse_patch_std_dev(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + patch_std_dev = _parse_patch_std_dev(d.pop("patch_std_dev", UNSET)) + + layerset_properties = cls( + fuel_type=fuel_type, + fuel_loading=fuel_loading, + fuel_height=fuel_height, + percent_cover=percent_cover, + distribution=distribution, + strata_fb=strata_fb, + patch_size=patch_size, + live_fuel_moisture=live_fuel_moisture, + dead_fuel_moisture=dead_fuel_moisture, + heat_of_combustion=heat_of_combustion, + patch_std_dev=patch_std_dev, + ) + + layerset_properties.additional_properties = d + return layerset_properties + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/line_string.py b/fastfuels_sdk/v2/client_library/models/line_string.py new file mode 100644 index 0000000..f2c03a4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/line_string.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LineString") + + +@_attrs_define +class LineString: + """LineString Model + + Attributes: + type_ (Literal['LineString']): + coordinates (list[list[float]]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["LineString"] + coordinates: list[list[float]] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates = [] + for coordinates_item_data in self.coordinates: + coordinates_item: list[float] + if isinstance(coordinates_item_data, list): + coordinates_item = [] + for componentsschemas_position_2d_item_data in coordinates_item_data: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates_item.append(componentsschemas_position_2d_item) + + coordinates.append(coordinates_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["LineString"], d.pop("type")) + if type_ != "LineString": + raise ValueError(f"type must match const 'LineString', got '{type_}'") + + coordinates = [] + _coordinates = d.pop("coordinates") + for coordinates_item_data in _coordinates: + + def _parse_coordinates_item(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_item_type_0 = [] + _coordinates_item_type_0 = data + for componentsschemas_position_2d_item_data in _coordinates_item_type_0: + + def _parse_componentsschemas_position_2d_item( + data: object, + ) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_item_type_0.append(componentsschemas_position_2d_item) + + return coordinates_item_type_0 + + coordinates_item = _parse_coordinates_item(coordinates_item_data) + + coordinates.append(coordinates_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + line_string = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + line_string.additional_properties = d + return line_string + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_applications_response.py b/fastfuels_sdk/v2/client_library/models/list_applications_response.py new file mode 100644 index 0000000..b3e2f88 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_applications_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.application import Application + + +T = TypeVar("T", bound="ListApplicationsResponse") + + +@_attrs_define +class ListApplicationsResponse: + """Paginated response for listing applications. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + applications (list[Application]): A list of applications. + """ + + current_page: int + page_size: int + total_items: int + applications: list[Application] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + applications = [] + for applications_item_data in self.applications: + applications_item = applications_item_data.to_dict() + applications.append(applications_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "applications": applications, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.application import Application + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + applications = [] + _applications = d.pop("applications") + for applications_item_data in _applications: + applications_item = Application.from_dict(applications_item_data) + + applications.append(applications_item) + + list_applications_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + applications=applications, + ) + + list_applications_response.additional_properties = d + return list_applications_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_domains_response.py b/fastfuels_sdk/v2/client_library/models/list_domains_response.py new file mode 100644 index 0000000..bb2d82e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_domains_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.domain import Domain + + +T = TypeVar("T", bound="ListDomainsResponse") + + +@_attrs_define +class ListDomainsResponse: + """Paginated response for listing domain resources. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + domains (list[Domain]): The list of domain resources for the current page. + """ + + current_page: int + page_size: int + total_items: int + domains: list[Domain] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + domains = [] + for domains_item_data in self.domains: + domains_item = domains_item_data.to_dict() + domains.append(domains_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "domains": domains, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.domain import Domain + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + domains = [] + _domains = d.pop("domains") + for domains_item_data in _domains: + domains_item = Domain.from_dict(domains_item_data) + + domains.append(domains_item) + + list_domains_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + domains=domains, + ) + + list_domains_response.additional_properties = d + return list_domains_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_exports_response.py b/fastfuels_sdk/v2/client_library/models/list_exports_response.py new file mode 100644 index 0000000..7c22339 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_exports_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.export import Export + + +T = TypeVar("T", bound="ListExportsResponse") + + +@_attrs_define +class ListExportsResponse: + """Paginated response for listing exports. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + exports (list[Export]): + """ + + current_page: int + page_size: int + total_items: int + exports: list[Export] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + exports = [] + for exports_item_data in self.exports: + exports_item = exports_item_data.to_dict() + exports.append(exports_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "exports": exports, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.export import Export + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + exports = [] + _exports = d.pop("exports") + for exports_item_data in _exports: + exports_item = Export.from_dict(exports_item_data) + + exports.append(exports_item) + + list_exports_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + exports=exports, + ) + + list_exports_response.additional_properties = d + return list_exports_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_features_response.py b/fastfuels_sdk/v2/client_library/models/list_features_response.py new file mode 100644 index 0000000..7f6991d --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_features_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.feature import Feature + + +T = TypeVar("T", bound="ListFeaturesResponse") + + +@_attrs_define +class ListFeaturesResponse: + """Paginated response for listing features. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + features (list[Feature]): + """ + + current_page: int + page_size: int + total_items: int + features: list[Feature] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + features = [] + for features_item_data in self.features: + features_item = features_item_data.to_dict() + features.append(features_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "features": features, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.feature import Feature + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + features = [] + _features = d.pop("features") + for features_item_data in _features: + features_item = Feature.from_dict(features_item_data) + + features.append(features_item) + + list_features_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + features=features, + ) + + list_features_response.additional_properties = d + return list_features_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_grids_response.py b/fastfuels_sdk/v2/client_library/models/list_grids_response.py new file mode 100644 index 0000000..096bbd9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_grids_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.grid import Grid + + +T = TypeVar("T", bound="ListGridsResponse") + + +@_attrs_define +class ListGridsResponse: + """Paginated response for listing grids. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + grids (list[Grid]): + """ + + current_page: int + page_size: int + total_items: int + grids: list[Grid] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + grids = [] + for grids_item_data in self.grids: + grids_item = grids_item_data.to_dict() + grids.append(grids_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "grids": grids, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.grid import Grid + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + grids = [] + _grids = d.pop("grids") + for grids_item_data in _grids: + grids_item = Grid.from_dict(grids_item_data) + + grids.append(grids_item) + + list_grids_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + grids=grids, + ) + + list_grids_response.additional_properties = d + return list_grids_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_inventories_response.py b/fastfuels_sdk/v2/client_library/models/list_inventories_response.py new file mode 100644 index 0000000..29f44dd --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_inventories_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.inventory import Inventory + + +T = TypeVar("T", bound="ListInventoriesResponse") + + +@_attrs_define +class ListInventoriesResponse: + """Paginated response for listing inventories. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + inventories (list[Inventory]): + """ + + current_page: int + page_size: int + total_items: int + inventories: list[Inventory] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + inventories = [] + for inventories_item_data in self.inventories: + inventories_item = inventories_item_data.to_dict() + inventories.append(inventories_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "inventories": inventories, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.inventory import Inventory + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + inventories = [] + _inventories = d.pop("inventories") + for inventories_item_data in _inventories: + inventories_item = Inventory.from_dict(inventories_item_data) + + inventories.append(inventories_item) + + list_inventories_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + inventories=inventories, + ) + + list_inventories_response.additional_properties = d + return list_inventories_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_keys_response.py b/fastfuels_sdk/v2/client_library/models/list_keys_response.py new file mode 100644 index 0000000..d8bba0e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_keys_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.key import Key + + +T = TypeVar("T", bound="ListKeysResponse") + + +@_attrs_define +class ListKeysResponse: + """Paginated response for listing API keys. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + keys (list[Key]): A list of API keys. + """ + + current_page: int + page_size: int + total_items: int + keys: list[Key] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + keys = [] + for keys_item_data in self.keys: + keys_item = keys_item_data.to_dict() + keys.append(keys_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "keys": keys, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.key import Key + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + keys = [] + _keys = d.pop("keys") + for keys_item_data in _keys: + keys_item = Key.from_dict(keys_item_data) + + keys.append(keys_item) + + list_keys_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + keys=keys, + ) + + list_keys_response.additional_properties = d + return list_keys_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/list_point_clouds_response.py b/fastfuels_sdk/v2/client_library/models/list_point_clouds_response.py new file mode 100644 index 0000000..03114b9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/list_point_clouds_response.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.point_cloud import PointCloud + + +T = TypeVar("T", bound="ListPointCloudsResponse") + + +@_attrs_define +class ListPointCloudsResponse: + """Paginated response for listing point clouds. + + Attributes: + current_page (int): The current page number (zero-indexed). + page_size (int): The number of items per page. + total_items (int): The total number of items across all pages. + point_clouds (list[PointCloud]): + """ + + current_page: int + page_size: int + total_items: int + point_clouds: list[PointCloud] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + current_page = self.current_page + + page_size = self.page_size + + total_items = self.total_items + + point_clouds = [] + for point_clouds_item_data in self.point_clouds: + point_clouds_item = point_clouds_item_data.to_dict() + point_clouds.append(point_clouds_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "current_page": current_page, + "page_size": page_size, + "total_items": total_items, + "point_clouds": point_clouds, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.point_cloud import PointCloud + + d = dict(src_dict) + current_page = d.pop("current_page") + + page_size = d.pop("page_size") + + total_items = d.pop("total_items") + + point_clouds = [] + _point_clouds = d.pop("point_clouds") + for point_clouds_item_data in _point_clouds: + point_clouds_item = PointCloud.from_dict(point_clouds_item_data) + + point_clouds.append(point_clouds_item) + + list_point_clouds_response = cls( + current_page=current_page, + page_size=page_size, + total_items=total_items, + point_clouds=point_clouds, + ) + + list_point_clouds_response.additional_properties = d + return list_point_clouds_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/max_crown_radius_unit.py b/fastfuels_sdk/v2/client_library/models/max_crown_radius_unit.py new file mode 100644 index 0000000..4d1dca9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/max_crown_radius_unit.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class MaxCrownRadiusUnit(str, Enum): + M = "m" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/meta_chm_version.py b/fastfuels_sdk/v2/client_library/models/meta_chm_version.py new file mode 100644 index 0000000..fc114b7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/meta_chm_version.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class MetaCHMVersion(str, Enum): + VALUE_0 = "1" + VALUE_1 = "2" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/modifier.py b/fastfuels_sdk/v2/client_library/models/modifier.py new file mode 100644 index 0000000..e75f638 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/modifier.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class Modifier(str, Enum): + ADD = "add" + DIVIDE = "divide" + MULTIPLY = "multiply" + REPLACE = "replace" + SUBTRACT = "subtract" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/moisture_model.py b/fastfuels_sdk/v2/client_library/models/moisture_model.py new file mode 100644 index 0000000..a3ea4aa --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/moisture_model.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.uniform_moisture_value import UniformMoistureValue + + +T = TypeVar("T", bound="MoistureModel") + + +@_attrs_define +class MoistureModel: + """Live/dead fuel moisture settings. + + Attributes: + live (None | UniformMoistureValue | Unset): + dead (None | UniformMoistureValue | Unset): + """ + + live: None | UniformMoistureValue | Unset = UNSET + dead: None | UniformMoistureValue | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.uniform_moisture_value import UniformMoistureValue + + live: dict[str, Any] | None | Unset + if isinstance(self.live, Unset): + live = UNSET + elif isinstance(self.live, UniformMoistureValue): + live = self.live.to_dict() + else: + live = self.live + + dead: dict[str, Any] | None | Unset + if isinstance(self.dead, Unset): + dead = UNSET + elif isinstance(self.dead, UniformMoistureValue): + dead = self.dead.to_dict() + else: + dead = self.dead + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if live is not UNSET: + field_dict["live"] = live + if dead is not UNSET: + field_dict["dead"] = dead + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.uniform_moisture_value import UniformMoistureValue + + d = dict(src_dict) + + def _parse_live(data: object) -> None | UniformMoistureValue | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + live_type_0 = UniformMoistureValue.from_dict(data) + + return live_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | UniformMoistureValue | Unset, data) + + live = _parse_live(d.pop("live", UNSET)) + + def _parse_dead(data: object) -> None | UniformMoistureValue | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + dead_type_0 = UniformMoistureValue.from_dict(data) + + return dead_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | UniformMoistureValue | Unset, data) + + dead = _parse_dead(d.pop("dead", UNSET)) + + moisture_model = cls( + live=live, + dead=dead, + ) + + return moisture_model diff --git a/fastfuels_sdk/v2/client_library/models/multi_line_string.py b/fastfuels_sdk/v2/client_library/models/multi_line_string.py new file mode 100644 index 0000000..dfe66d7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/multi_line_string.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="MultiLineString") + + +@_attrs_define +class MultiLineString: + """MultiLineString Model + + Attributes: + type_ (Literal['MultiLineString']): + coordinates (list[list[list[float]]]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["MultiLineString"] + coordinates: list[list[list[float]]] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates = [] + for coordinates_item_data in self.coordinates: + coordinates_item = [] + for coordinates_item_item_data in coordinates_item_data: + coordinates_item_item: list[float] + if isinstance(coordinates_item_item_data, list): + coordinates_item_item = [] + for ( + componentsschemas_position_2d_item_data + ) in coordinates_item_item_data: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates_item_item.append(componentsschemas_position_2d_item) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["MultiLineString"], d.pop("type")) + if type_ != "MultiLineString": + raise ValueError(f"type must match const 'MultiLineString', got '{type_}'") + + coordinates = [] + _coordinates = d.pop("coordinates") + for coordinates_item_data in _coordinates: + coordinates_item = [] + _coordinates_item = coordinates_item_data + for coordinates_item_item_data in _coordinates_item: + + def _parse_coordinates_item_item(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_item_item_type_0 = [] + _coordinates_item_item_type_0 = data + for ( + componentsschemas_position_2d_item_data + ) in _coordinates_item_item_type_0: + + def _parse_componentsschemas_position_2d_item( + data: object, + ) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_item_item_type_0.append( + componentsschemas_position_2d_item + ) + + return coordinates_item_item_type_0 + + coordinates_item_item = _parse_coordinates_item_item( + coordinates_item_item_data + ) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + multi_line_string = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + multi_line_string.additional_properties = d + return multi_line_string + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/multi_point.py b/fastfuels_sdk/v2/client_library/models/multi_point.py new file mode 100644 index 0000000..2ae7708 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/multi_point.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="MultiPoint") + + +@_attrs_define +class MultiPoint: + """MultiPoint Model + + Attributes: + type_ (Literal['MultiPoint']): + coordinates (list[list[float]]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["MultiPoint"] + coordinates: list[list[float]] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates = [] + for coordinates_item_data in self.coordinates: + coordinates_item: list[float] + if isinstance(coordinates_item_data, list): + coordinates_item = [] + for componentsschemas_position_2d_item_data in coordinates_item_data: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates_item.append(componentsschemas_position_2d_item) + + coordinates.append(coordinates_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["MultiPoint"], d.pop("type")) + if type_ != "MultiPoint": + raise ValueError(f"type must match const 'MultiPoint', got '{type_}'") + + coordinates = [] + _coordinates = d.pop("coordinates") + for coordinates_item_data in _coordinates: + + def _parse_coordinates_item(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_item_type_0 = [] + _coordinates_item_type_0 = data + for componentsschemas_position_2d_item_data in _coordinates_item_type_0: + + def _parse_componentsschemas_position_2d_item( + data: object, + ) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_item_type_0.append(componentsschemas_position_2d_item) + + return coordinates_item_type_0 + + coordinates_item = _parse_coordinates_item(coordinates_item_data) + + coordinates.append(coordinates_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + multi_point = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + multi_point.additional_properties = d + return multi_point + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/multi_polygon.py b/fastfuels_sdk/v2/client_library/models/multi_polygon.py new file mode 100644 index 0000000..b0edd40 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/multi_polygon.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="MultiPolygon") + + +@_attrs_define +class MultiPolygon: + """MultiPolygon Model + + Attributes: + type_ (Literal['MultiPolygon']): + coordinates (list[list[list[list[float]]]]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["MultiPolygon"] + coordinates: list[list[list[list[float]]]] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates = [] + for coordinates_item_data in self.coordinates: + coordinates_item = [] + for coordinates_item_item_data in coordinates_item_data: + coordinates_item_item = [] + for coordinates_item_item_item_data in coordinates_item_item_data: + coordinates_item_item_item: list[float] + if isinstance(coordinates_item_item_item_data, list): + coordinates_item_item_item = [] + for ( + componentsschemas_position_2d_item_data + ) in coordinates_item_item_item_data: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates_item_item_item.append( + componentsschemas_position_2d_item + ) + + coordinates_item_item.append(coordinates_item_item_item) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["MultiPolygon"], d.pop("type")) + if type_ != "MultiPolygon": + raise ValueError(f"type must match const 'MultiPolygon', got '{type_}'") + + coordinates = [] + _coordinates = d.pop("coordinates") + for coordinates_item_data in _coordinates: + coordinates_item = [] + _coordinates_item = coordinates_item_data + for coordinates_item_item_data in _coordinates_item: + coordinates_item_item = [] + _coordinates_item_item = coordinates_item_item_data + for coordinates_item_item_item_data in _coordinates_item_item: + + def _parse_coordinates_item_item_item(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_item_item_item_type_0 = [] + _coordinates_item_item_item_type_0 = data + for ( + componentsschemas_position_2d_item_data + ) in _coordinates_item_item_item_type_0: + + def _parse_componentsschemas_position_2d_item( + data: object, + ) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_item_item_item_type_0.append( + componentsschemas_position_2d_item + ) + + return coordinates_item_item_item_type_0 + + coordinates_item_item_item = _parse_coordinates_item_item_item( + coordinates_item_item_item_data + ) + + coordinates_item_item.append(coordinates_item_item_item) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + multi_polygon = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + multi_polygon.additional_properties = d + return multi_polygon + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/non_burnable_fuel_model.py b/fastfuels_sdk/v2/client_library/models/non_burnable_fuel_model.py new file mode 100644 index 0000000..f256e4f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/non_burnable_fuel_model.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class NonBurnableFuelModel(str, Enum): + NB1 = "NB1" + NB2 = "NB2" + NB3 = "NB3" + NB8 = "NB8" + NB9 = "NB9" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/operator.py b/fastfuels_sdk/v2/client_library/models/operator.py new file mode 100644 index 0000000..bfab306 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/operator.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class Operator(str, Enum): + EQ = "eq" + GE = "ge" + GT = "gt" + LE = "le" + LT = "lt" + NE = "ne" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/overlap_method.py b/fastfuels_sdk/v2/client_library/models/overlap_method.py new file mode 100644 index 0000000..31d85fd --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/overlap_method.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OverlapMethod(str, Enum): + MAX = "max" + MEAN = "mean" + MIN = "min" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/point.py b/fastfuels_sdk/v2/client_library/models/point.py new file mode 100644 index 0000000..8ad31b4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Point") + + +@_attrs_define +class Point: + """Point Model + + Attributes: + type_ (Literal['Point']): + coordinates (list[float]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["Point"] + coordinates: list[float] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates: list[float] + if isinstance(self.coordinates, list): + coordinates = [] + for componentsschemas_position_2d_item_data in self.coordinates: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates.append(componentsschemas_position_2d_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["Point"], d.pop("type")) + if type_ != "Point": + raise ValueError(f"type must match const 'Point', got '{type_}'") + + def _parse_coordinates(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_type_0 = [] + _coordinates_type_0 = data + for componentsschemas_position_2d_item_data in _coordinates_type_0: + + def _parse_componentsschemas_position_2d_item(data: object) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_type_0.append(componentsschemas_position_2d_item) + + return coordinates_type_0 + + coordinates = _parse_coordinates(d.pop("coordinates")) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + point = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + point.additional_properties = d + return point + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud.py b/fastfuels_sdk/v2/client_library/models/point_cloud.py new file mode 100644 index 0000000..b2b8a88 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.job_status import JobStatus +from ..models.point_cloud_type import PointCloudType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.point_cloud_georeference import PointCloudGeoreference + from ..models.point_cloud_source import PointCloudSource + from ..models.point_cloud_summary import PointCloudSummary + + +T = TypeVar("T", bound="PointCloud") + + +@_attrs_define +class PointCloud: + """A laser-scanned point cloud scoped to a single domain. + + Point clouds are created asynchronously: a creation request returns + immediately with ``status="pending"`` and the file is ingested in the + background. While ``status`` is ``"pending"`` or ``"running"`` the + derived fields (`georeference`) are ``null``; the backend fills them in once + ingestion succeeds and flips ``status`` to ``"completed"``. If ingestion + fails, ``status`` becomes ``"failed"`` and `error` explains why. + + A completed point cloud is an input you compose with other resources — most + directly, an ALS cloud feeds a canopy-height-model grid, which feeds a tree + inventory. + + Attributes: + id (str): Unique 32-character hex identifier for this point cloud. + domain_id (str): Identifier of the domain this point cloud belongs to. + type_ (PointCloudType): How a point cloud was acquired. + + The acquisition platform determines the cloud's geometry and which downstream + products it can feed, so it is recorded as a first-class, filterable field. + + - ``als`` — **Airborne Laser Scanning.** Captured from an aircraft or drone + looking down. Covers large areas from above and is the basis for canopy + height models and individual-tree detection. Available from an upload or + from USGS 3DEP. + - ``tls`` — **Terrestrial Laser Scanning.** Captured from a tripod-mounted + scanner on the ground looking out and up. Resolves fine sub-canopy and + stem structure over a small plot. Available from an upload only (3DEP is + airborne and cannot produce terrestrial scans). + status (JobStatus): Status of an async job. + source (PointCloudSource): Provenance of the point cloud — where its points came from. Always carries a `name` + discriminator (`upload` for a user-supplied file, `3dep` for a USGS 3DEP fetch) alongside source-specific + parameters. Stored verbatim so the cloud can be reproduced from its origin. + name (str | Unset): Human-readable name for the point cloud. Default: ''. + description (str | Unset): Longer free-text description of the point cloud. Default: ''. + progress (JobProgress | None | Unset): Progress info while `status` is `running`. Null otherwise. + created_on (datetime.datetime | None | Unset): When the point cloud was created. + modified_on (datetime.datetime | None | Unset): When the point cloud was last modified. + checksum (None | str | Unset): Version marker for this point cloud's content. It changes each time the point + cloud is rebuilt and is unaffected by metadata-only edits (name, description, tags). A resource derived from + this point cloud stores the checksum it was built from; comparing that stored value against this field reveals + whether this point cloud has changed since. May be null for point clouds created before checksums were + introduced. + georeference (None | PointCloudGeoreference | Unset): Coordinate reference system and 3D extent of the points. + Null until the backend finishes ingesting the cloud. + summary (None | PointCloudSummary | Unset): Summary statistics — point count, classification codes present, and + density — describing the cloud's contents. Null until the backend finishes ingesting the cloud. + error (JobError | None | Unset): Details when `status` is `failed`. The traceback is stored but not exposed in + API responses. + tags (list[str] | Unset): User-assigned tags for organizing and filtering point clouds. + """ + + id: str + domain_id: str + type_: PointCloudType + status: JobStatus + source: PointCloudSource + name: str | Unset = "" + description: str | Unset = "" + progress: JobProgress | None | Unset = UNSET + created_on: datetime.datetime | None | Unset = UNSET + modified_on: datetime.datetime | None | Unset = UNSET + checksum: None | str | Unset = UNSET + georeference: None | PointCloudGeoreference | Unset = UNSET + summary: None | PointCloudSummary | Unset = UNSET + error: JobError | None | Unset = UNSET + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.point_cloud_georeference import PointCloudGeoreference + from ..models.point_cloud_summary import PointCloudSummary + + id = self.id + + domain_id = self.domain_id + + type_ = self.type_.value + + status = self.status.value + + source = self.source.to_dict() + + name = self.name + + description = self.description + + progress: dict[str, Any] | None | Unset + if isinstance(self.progress, Unset): + progress = UNSET + elif isinstance(self.progress, JobProgress): + progress = self.progress.to_dict() + else: + progress = self.progress + + created_on: None | str | Unset + if isinstance(self.created_on, Unset): + created_on = UNSET + elif isinstance(self.created_on, datetime.datetime): + created_on = self.created_on.isoformat() + else: + created_on = self.created_on + + modified_on: None | str | Unset + if isinstance(self.modified_on, Unset): + modified_on = UNSET + elif isinstance(self.modified_on, datetime.datetime): + modified_on = self.modified_on.isoformat() + else: + modified_on = self.modified_on + + checksum: None | str | Unset + if isinstance(self.checksum, Unset): + checksum = UNSET + else: + checksum = self.checksum + + georeference: dict[str, Any] | None | Unset + if isinstance(self.georeference, Unset): + georeference = UNSET + elif isinstance(self.georeference, PointCloudGeoreference): + georeference = self.georeference.to_dict() + else: + georeference = self.georeference + + summary: dict[str, Any] | None | Unset + if isinstance(self.summary, Unset): + summary = UNSET + elif isinstance(self.summary, PointCloudSummary): + summary = self.summary.to_dict() + else: + summary = self.summary + + error: dict[str, Any] | None | Unset + if isinstance(self.error, Unset): + error = UNSET + elif isinstance(self.error, JobError): + error = self.error.to_dict() + else: + error = self.error + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "domain_id": domain_id, + "type": type_, + "status": status, + "source": source, + } + ) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if progress is not UNSET: + field_dict["progress"] = progress + if created_on is not UNSET: + field_dict["created_on"] = created_on + if modified_on is not UNSET: + field_dict["modified_on"] = modified_on + if checksum is not UNSET: + field_dict["checksum"] = checksum + if georeference is not UNSET: + field_dict["georeference"] = georeference + if summary is not UNSET: + field_dict["summary"] = summary + if error is not UNSET: + field_dict["error"] = error + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.job_error import JobError + from ..models.job_progress import JobProgress + from ..models.point_cloud_georeference import PointCloudGeoreference + from ..models.point_cloud_source import PointCloudSource + from ..models.point_cloud_summary import PointCloudSummary + + d = dict(src_dict) + id = d.pop("id") + + domain_id = d.pop("domain_id") + + type_ = PointCloudType(d.pop("type")) + + status = JobStatus(d.pop("status")) + + source = PointCloudSource.from_dict(d.pop("source")) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + def _parse_progress(data: object) -> JobProgress | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + progress_type_0 = JobProgress.from_dict(data) + + return progress_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobProgress | None | Unset, data) + + progress = _parse_progress(d.pop("progress", UNSET)) + + def _parse_created_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_on_type_0 = datetime.datetime.fromisoformat(data) + + return created_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_on = _parse_created_on(d.pop("created_on", UNSET)) + + def _parse_modified_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + modified_on_type_0 = datetime.datetime.fromisoformat(data) + + return modified_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + modified_on = _parse_modified_on(d.pop("modified_on", UNSET)) + + def _parse_checksum(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + checksum = _parse_checksum(d.pop("checksum", UNSET)) + + def _parse_georeference(data: object) -> None | PointCloudGeoreference | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + georeference_type_0 = PointCloudGeoreference.from_dict(data) + + return georeference_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PointCloudGeoreference | Unset, data) + + georeference = _parse_georeference(d.pop("georeference", UNSET)) + + def _parse_summary(data: object) -> None | PointCloudSummary | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + summary_type_0 = PointCloudSummary.from_dict(data) + + return summary_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PointCloudSummary | Unset, data) + + summary = _parse_summary(d.pop("summary", UNSET)) + + def _parse_error(data: object) -> JobError | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + error_type_0 = JobError.from_dict(data) + + return error_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(JobError | None | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + tags = cast(list[str], d.pop("tags", UNSET)) + + point_cloud = cls( + id=id, + domain_id=domain_id, + type_=type_, + status=status, + source=source, + name=name, + description=description, + progress=progress, + created_on=created_on, + modified_on=modified_on, + checksum=checksum, + georeference=georeference, + summary=summary, + error=error, + tags=tags, + ) + + point_cloud.additional_properties = d + return point_cloud + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_georeference.py b/fastfuels_sdk/v2/client_library/models/point_cloud_georeference.py new file mode 100644 index 0000000..bf456d4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_georeference.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PointCloudGeoreference") + + +@_attrs_define +class PointCloudGeoreference: + """The coordinate reference system and 3D extent of a point cloud. + + Populated by the backend after the cloud is ingested and inspected; it is + ``null`` while the point cloud is still ``pending`` or ``running``. + + Attributes: + crs (str): Coordinate reference system the points are stored in, as an authority code (e.g. `EPSG:32613`). This + is always the domain's CRS: uploads in a different CRS are reprojected during ingestion. Only horizontal + coordinates are transformed — elevations are stored exactly as the source provided them and are never converted + between reference surfaces. + bounds (list[float]): Axis-aligned 3D bounding box of every point, given as `[min_x, min_y, min_z, max_x, max_y, + max_z]` in the units of `crs`. Point clouds are three-dimensional, so the box includes a vertical (z) extent. + Use it to check coverage against a domain before deriving products from the cloud. + """ + + crs: str + bounds: list[float] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + crs = self.crs + + bounds = [] + for bounds_item_data in self.bounds: + bounds_item: float + bounds_item = bounds_item_data + bounds.append(bounds_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "crs": crs, + "bounds": bounds, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + crs = d.pop("crs") + + bounds = [] + _bounds = d.pop("bounds") + for bounds_item_data in _bounds: + + def _parse_bounds_item(data: object) -> float: + return cast(float, data) + + bounds_item = _parse_bounds_item(bounds_item_data) + + bounds.append(bounds_item) + + point_cloud_georeference = cls( + crs=crs, + bounds=bounds, + ) + + point_cloud_georeference.additional_properties = d + return point_cloud_georeference + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_sort_field.py b/fastfuels_sdk/v2/client_library/models/point_cloud_sort_field.py new file mode 100644 index 0000000..a89ed42 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_sort_field.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class PointCloudSortField(str, Enum): + CREATED_ON = "created_on" + MODIFIED_ON = "modified_on" + NAME = "name" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_source.py b/fastfuels_sdk/v2/client_library/models/point_cloud_source.py new file mode 100644 index 0000000..65c8ced --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_source.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PointCloudSource") + + +@_attrs_define +class PointCloudSource: + """Provenance of the point cloud — where its points came from. Always carries a `name` discriminator (`upload` for a + user-supplied file, `3dep` for a USGS 3DEP fetch) alongside source-specific parameters. Stored verbatim so the cloud + can be reproduced from its origin. + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + point_cloud_source = cls() + + point_cloud_source.additional_properties = d + return point_cloud_source + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_summary.py b/fastfuels_sdk/v2/client_library/models/point_cloud_summary.py new file mode 100644 index 0000000..71972e7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_summary.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PointCloudSummary") + + +@_attrs_define +class PointCloudSummary: + """Summary statistics describing the contents of a point cloud. + + Populated by the backend after the cloud is ingested and inspected; it is + ``null`` while the point cloud is still ``pending`` or ``running``. Use it to + gauge a cloud's size, density, and composition without downloading it. + + Attributes: + point_count (int): Total number of points in the cloud. + point_classes (list[int]): ASPRS standard classification codes present in the cloud, sorted ascending. Common + codes include `1` (unclassified), `2` (ground), and `3`, `4`, `5` (low, medium, high vegetation). + density (float): Average point density over the cloud's horizontal extent, in points per square meter. + """ + + point_count: int + point_classes: list[int] + density: float + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + point_count = self.point_count + + point_classes = self.point_classes + + density = self.density + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "point_count": point_count, + "point_classes": point_classes, + "density": density, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + point_count = d.pop("point_count") + + point_classes = cast(list[int], d.pop("point_classes")) + + density = d.pop("density") + + point_cloud_summary = cls( + point_count=point_count, + point_classes=point_classes, + density=density, + ) + + point_cloud_summary.additional_properties = d + return point_cloud_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_three_dep_coverage_response.py b/fastfuels_sdk/v2/client_library/models/point_cloud_three_dep_coverage_response.py new file mode 100644 index 0000000..1366f4f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_three_dep_coverage_response.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.three_dep_dataset_coverage import ThreeDepDatasetCoverage + + +T = TypeVar("T", bound="PointCloudThreeDepCoverageResponse") + + +@_attrs_define +class PointCloudThreeDepCoverageResponse: + """Response model for the 3DEP point cloud coverage pre-flight check. + + Attributes: + available (bool): Whether any 3DEP lidar covers this domain. When false, a create request for this domain is + rejected. + coverage_fraction (float): Fraction of the domain covered by 3DEP lidar, from `0.0` to `1.0`. This is the union + of every available acquisition, so it never exceeds 1.0 no matter how much the acquisitions overlap. + datasets (list[ThreeDepDatasetCoverage]): Acquisitions that would be read, in the order they would be used. + Empty when no lidar covers the domain. + estimated_point_count (int): Approximate total number of points a fetch would return, summed across + acquisitions. + point_budget (int): Maximum number of points a single fetch may return. Shrink the domain if the estimate + exceeds it. + exceeds_point_budget (bool): Whether the estimate is over `point_budget`. When true, a create request for this + domain is rejected, so check this before committing to a fetch. + """ + + available: bool + coverage_fraction: float + datasets: list[ThreeDepDatasetCoverage] + estimated_point_count: int + point_budget: int + exceeds_point_budget: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + available = self.available + + coverage_fraction = self.coverage_fraction + + datasets = [] + for datasets_item_data in self.datasets: + datasets_item = datasets_item_data.to_dict() + datasets.append(datasets_item) + + estimated_point_count = self.estimated_point_count + + point_budget = self.point_budget + + exceeds_point_budget = self.exceeds_point_budget + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "available": available, + "coverage_fraction": coverage_fraction, + "datasets": datasets, + "estimated_point_count": estimated_point_count, + "point_budget": point_budget, + "exceeds_point_budget": exceeds_point_budget, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.three_dep_dataset_coverage import ThreeDepDatasetCoverage + + d = dict(src_dict) + available = d.pop("available") + + coverage_fraction = d.pop("coverage_fraction") + + datasets = [] + _datasets = d.pop("datasets") + for datasets_item_data in _datasets: + datasets_item = ThreeDepDatasetCoverage.from_dict(datasets_item_data) + + datasets.append(datasets_item) + + estimated_point_count = d.pop("estimated_point_count") + + point_budget = d.pop("point_budget") + + exceeds_point_budget = d.pop("exceeds_point_budget") + + point_cloud_three_dep_coverage_response = cls( + available=available, + coverage_fraction=coverage_fraction, + datasets=datasets, + estimated_point_count=estimated_point_count, + point_budget=point_budget, + exceeds_point_budget=exceeds_point_budget, + ) + + point_cloud_three_dep_coverage_response.additional_properties = d + return point_cloud_three_dep_coverage_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_type.py b/fastfuels_sdk/v2/client_library/models/point_cloud_type.py new file mode 100644 index 0000000..89acace --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_type.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class PointCloudType(str, Enum): + ALS = "als" + TLS = "tls" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_upload_created_response.py b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_created_response.py new file mode 100644 index 0000000..e18e3a9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_created_response.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.point_cloud import PointCloud + from ..models.point_cloud_upload_spec import PointCloudUploadSpec + + +T = TypeVar("T", bound="PointCloudUploadCreatedResponse") + + +@_attrs_define +class PointCloudUploadCreatedResponse: + """Response returned when a point cloud upload is created. + + Attributes: + point_cloud (PointCloud): A laser-scanned point cloud scoped to a single domain. + + Point clouds are created asynchronously: a creation request returns + immediately with ``status="pending"`` and the file is ingested in the + background. While ``status`` is ``"pending"`` or ``"running"`` the + derived fields (`georeference`) are ``null``; the backend fills them in once + ingestion succeeds and flips ``status`` to ``"completed"``. If ingestion + fails, ``status`` becomes ``"failed"`` and `error` explains why. + + A completed point cloud is an input you compose with other resources — most + directly, an ALS cloud feeds a canopy-height-model grid, which feeds a tree + inventory. + upload (PointCloudUploadSpec): Where and how to upload the source file. + + PUT the file to `url`, sending every header in `headers` exactly as given. + The upload must complete before `expires_at` and must not exceed + `max_size_bytes`. + """ + + point_cloud: PointCloud + upload: PointCloudUploadSpec + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + point_cloud = self.point_cloud.to_dict() + + upload = self.upload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "point_cloud": point_cloud, + "upload": upload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.point_cloud import PointCloud + from ..models.point_cloud_upload_spec import PointCloudUploadSpec + + d = dict(src_dict) + point_cloud = PointCloud.from_dict(d.pop("point_cloud")) + + upload = PointCloudUploadSpec.from_dict(d.pop("upload")) + + point_cloud_upload_created_response = cls( + point_cloud=point_cloud, + upload=upload, + ) + + point_cloud_upload_created_response.additional_properties = d + return point_cloud_upload_created_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec.py b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec.py new file mode 100644 index 0000000..ba75354 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.point_cloud_upload_spec_headers import PointCloudUploadSpecHeaders + + +T = TypeVar("T", bound="PointCloudUploadSpec") + + +@_attrs_define +class PointCloudUploadSpec: + """Where and how to upload the source file. + + PUT the file to `url`, sending every header in `headers` exactly as given. + The upload must complete before `expires_at` and must not exceed + `max_size_bytes`. + + Attributes: + url (str): Signed URL to upload the source file to. + headers (PointCloudUploadSpecHeaders): HTTP headers that must be sent with the PUT request, exactly as given. + The signed URL commits to these headers; the upload is rejected if any is missing or altered. + content_type (str): Value the `Content-Type` header must use when uploading. + expires_at (datetime.datetime): When the signed URL expires. + max_size_bytes (int): Maximum allowed size of the uploaded file, in bytes. + method (Literal['PUT'] | Unset): Default: 'PUT'. + """ + + url: str + headers: PointCloudUploadSpecHeaders + content_type: str + expires_at: datetime.datetime + max_size_bytes: int + method: Literal["PUT"] | Unset = "PUT" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + headers = self.headers.to_dict() + + content_type = self.content_type + + expires_at = self.expires_at.isoformat() + + max_size_bytes = self.max_size_bytes + + method = self.method + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "url": url, + "headers": headers, + "content_type": content_type, + "expires_at": expires_at, + "max_size_bytes": max_size_bytes, + } + ) + if method is not UNSET: + field_dict["method"] = method + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.point_cloud_upload_spec_headers import PointCloudUploadSpecHeaders + + d = dict(src_dict) + url = d.pop("url") + + headers = PointCloudUploadSpecHeaders.from_dict(d.pop("headers")) + + content_type = d.pop("content_type") + + expires_at = datetime.datetime.fromisoformat(d.pop("expires_at")) + + max_size_bytes = d.pop("max_size_bytes") + + method = cast(Literal["PUT"] | Unset, d.pop("method", UNSET)) + if method != "PUT" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'PUT', got '{method}'") + + point_cloud_upload_spec = cls( + url=url, + headers=headers, + content_type=content_type, + expires_at=expires_at, + max_size_bytes=max_size_bytes, + method=method, + ) + + point_cloud_upload_spec.additional_properties = d + return point_cloud_upload_spec + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec_headers.py b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec_headers.py new file mode 100644 index 0000000..29f7370 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_cloud_upload_spec_headers.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PointCloudUploadSpecHeaders") + + +@_attrs_define +class PointCloudUploadSpecHeaders: + """HTTP headers that must be sent with the PUT request, exactly as given. The signed URL commits to these headers; the + upload is rejected if any is missing or altered. + + """ + + additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + point_cloud_upload_spec_headers = cls() + + point_cloud_upload_spec_headers.additional_properties = d + return point_cloud_upload_spec_headers + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> str: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: str) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/point_process.py b/fastfuels_sdk/v2/client_library/models/point_process.py new file mode 100644 index 0000000..4fe1a76 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/point_process.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class PointProcess(str, Enum): + INHOMOGENEOUS_POISSON = "inhomogeneous_poisson" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/polygon.py b/fastfuels_sdk/v2/client_library/models/polygon.py new file mode 100644 index 0000000..904cc72 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/polygon.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Polygon") + + +@_attrs_define +class Polygon: + """Polygon Model + + Attributes: + type_ (Literal['Polygon']): + coordinates (list[list[list[float]]]): + bbox (list[float] | None | Unset): + """ + + type_: Literal["Polygon"] + coordinates: list[list[list[float]]] + bbox: list[float] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + coordinates = [] + for coordinates_item_data in self.coordinates: + coordinates_item = [] + for coordinates_item_item_data in coordinates_item_data: + coordinates_item_item: list[float] + if isinstance(coordinates_item_item_data, list): + coordinates_item_item = [] + for ( + componentsschemas_position_2d_item_data + ) in coordinates_item_item_data: + componentsschemas_position_2d_item: float + componentsschemas_position_2d_item = ( + componentsschemas_position_2d_item_data + ) + coordinates_item_item.append(componentsschemas_position_2d_item) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + bbox: list[float] | None | Unset + if isinstance(self.bbox, Unset): + bbox = UNSET + elif isinstance(self.bbox, list): + bbox = [] + for bbox_type_0_item_data in self.bbox: + bbox_type_0_item: float + bbox_type_0_item = bbox_type_0_item_data + bbox.append(bbox_type_0_item) + + else: + bbox = self.bbox + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "coordinates": coordinates, + } + ) + if bbox is not UNSET: + field_dict["bbox"] = bbox + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + type_ = cast(Literal["Polygon"], d.pop("type")) + if type_ != "Polygon": + raise ValueError(f"type must match const 'Polygon', got '{type_}'") + + coordinates = [] + _coordinates = d.pop("coordinates") + for coordinates_item_data in _coordinates: + coordinates_item = [] + _coordinates_item = coordinates_item_data + for coordinates_item_item_data in _coordinates_item: + + def _parse_coordinates_item_item(data: object) -> list[float]: + if not isinstance(data, list): + raise TypeError() + coordinates_item_item_type_0 = [] + _coordinates_item_item_type_0 = data + for ( + componentsschemas_position_2d_item_data + ) in _coordinates_item_item_type_0: + + def _parse_componentsschemas_position_2d_item( + data: object, + ) -> float: + return cast(float, data) + + componentsschemas_position_2d_item = ( + _parse_componentsschemas_position_2d_item( + componentsschemas_position_2d_item_data + ) + ) + + coordinates_item_item_type_0.append( + componentsschemas_position_2d_item + ) + + return coordinates_item_item_type_0 + + coordinates_item_item = _parse_coordinates_item_item( + coordinates_item_item_data + ) + + coordinates_item.append(coordinates_item_item) + + coordinates.append(coordinates_item) + + def _parse_bbox(data: object) -> list[float] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + bbox_type_0 = [] + _bbox_type_0 = data + for bbox_type_0_item_data in _bbox_type_0: + + def _parse_bbox_type_0_item(data: object) -> float: + return cast(float, data) + + bbox_type_0_item = _parse_bbox_type_0_item(bbox_type_0_item_data) + + bbox_type_0.append(bbox_type_0_item) + + return bbox_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[float] | None | Unset, data) + + bbox = _parse_bbox(d.pop("bbox", UNSET)) + + polygon = cls( + type_=type_, + coordinates=coordinates, + bbox=bbox, + ) + + polygon.additional_properties = d + return polygon + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_domain_target.py b/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_domain_target.py new file mode 100644 index 0000000..3a9a9cb --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_domain_target.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="QUICFireExportAlignmentDomainTarget") + + +@_attrs_define +class QUICFireExportAlignmentDomainTarget: + """Anchor the fire grid to the Domain bounding box. + + Output cells tile the Domain bbox at the given `dx` / `dy`, padded + outward if the bbox isn't already a whole multiple. `dz` sets the + uniform vertical cell size; the exporter always writes `aa1=1` so + fuel layers map 1:1 to QUIC-Fire cells. Defaults are QUIC-Fire's + recommended values (2 m horizontal, 1 m vertical). + + Attributes: + target (Literal['domain'] | Unset): Default: 'domain'. + dx (float | Unset): Horizontal fire-grid cell size in x (UTM east-west), in meters. QUIC-Fire recommends 2 m. + Default: 2.0. + dy (float | Unset): Horizontal fire-grid cell size in y (UTM north-south), in meters. Must equal `dx` — non- + square fire-grid cells are not supported. Default: 2.0. + dz (float | Unset): Vertical fire-grid cell size, in meters. QUIC-Fire recommends 1 m. Must equal the 3D tree + grid's voxelization vertical resolution (`resolution.vertical`): the exporter never resamples vertically, so a + mismatch is rejected with 422 rather than silently applied. Default: 1.0. + """ + + target: Literal["domain"] | Unset = "domain" + dx: float | Unset = 2.0 + dy: float | Unset = 2.0 + dz: float | Unset = 1.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + dx = self.dx + + dy = self.dy + + dz = self.dz + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if target is not UNSET: + field_dict["target"] = target + if dx is not UNSET: + field_dict["dx"] = dx + if dy is not UNSET: + field_dict["dy"] = dy + if dz is not UNSET: + field_dict["dz"] = dz + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["domain"] | Unset, d.pop("target", UNSET)) + if target != "domain" and not isinstance(target, Unset): + raise ValueError(f"target must match const 'domain', got '{target}'") + + dx = d.pop("dx", UNSET) + + dy = d.pop("dy", UNSET) + + dz = d.pop("dz", UNSET) + + quic_fire_export_alignment_domain_target = cls( + target=target, + dx=dx, + dy=dy, + dz=dz, + ) + + quic_fire_export_alignment_domain_target.additional_properties = d + return quic_fire_export_alignment_domain_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_grid_target.py b/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_grid_target.py new file mode 100644 index 0000000..31466d7 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quic_fire_export_alignment_grid_target.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="QUICFireExportAlignmentGridTarget") + + +@_attrs_define +class QUICFireExportAlignmentGridTarget: + """Anchor the fire grid to an existing grid's lattice. + + Useful when role grids share a non-Domain-anchored lattice (e.g. all + chained off a `target="native"` master grid). The referenced grid's + CRS, transform, and shape become the fire grid's horizontal lattice; + vertical cell size and layer count are taken from the canopy grid. + + Attributes: + target (Literal['grid']): + grid_id (str): Existing grid whose horizontal lattice (CRS, transform, shape) the fire grid should match + exactly. + """ + + target: Literal["grid"] + grid_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + target = self.target + + grid_id = self.grid_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "target": target, + "grid_id": grid_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + target = cast(Literal["grid"], d.pop("target")) + if target != "grid": + raise ValueError(f"target must match const 'grid', got '{target}'") + + grid_id = d.pop("grid_id") + + quic_fire_export_alignment_grid_target = cls( + target=target, + grid_id=grid_id, + ) + + quic_fire_export_alignment_grid_target.additional_properties = d + return quic_fire_export_alignment_grid_target + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/quicfire_export_request.py b/fastfuels_sdk/v2/client_library/models/quicfire_export_request.py new file mode 100644 index 0000000..3c758a6 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quicfire_export_request.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.quicfire_export_request_moist_merge import QuicfireExportRequestMoistMerge +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.field_source import FieldSource + from ..models.quic_fire_export_alignment_domain_target import ( + QUICFireExportAlignmentDomainTarget, + ) + from ..models.quic_fire_export_alignment_grid_target import ( + QUICFireExportAlignmentGridTarget, + ) + + +T = TypeVar("T", bound="QuicfireExportRequest") + + +@_attrs_define +class QuicfireExportRequest: + """Request body for creating a QUIC-Fire combined export. + + Five required roles produce `treesrhof.dat`, `treesmoist.dat`, and + `treesfueldepth.dat`. `topography` (optional) produces `topo.dat`. The + SAVR pair (optional, both-or-neither) produces `treesss.dat`. + + The fire grid is defined by the `alignment` field — either the Domain + bounding box padded to `(dx, dy)` (with `dz` vertical), or the lattice + of an existing grid. Every role grid must be lattice-aligned to this + fire grid and cover its full extent; otherwise the request is rejected. + The exporter only crops oversized roles by integer slicing — it never + resamples or reprojects. + + The output resolution is set here, on the export, via `alignment.dx`/`dy` + (default 2 m, QUIC-Fire's recommended value). It is a separate setting + from the resolution of each grid you built — changing your grids does not + change the export, and vice versa. Because the exporter never resamples, + every role grid must already be built at the fire-grid resolution. To + export at 1 m, for example, set `dx`/`dy` to 1 and build all role grids at + 1 m (2D grids at 1 m via their `alignment.resolution`, and the 3D tree + grid at 1 m via `resolution.horizontal` — 3D grids cannot be resampled). + The same holds vertically: `alignment.dz` must equal the 3D tree grid's + `resolution.vertical`, or the request is rejected with 422. + + Attributes: + canopy_bulk_density (FieldSource): A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + canopy_moisture (FieldSource): A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + surface_fuel_load (FieldSource): A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + surface_fuel_depth (FieldSource): A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + surface_moisture (FieldSource): A single physical quantity drawn from one band on one grid. + + Every per-role input to the QUIC-Fire export uses this shape so the schema + is uniform across roles. The forward path for `nfuel>1` (when QUIC-Fire's + multi-fuel-type capability becomes relevant) is to allow each per-fuel-type + role to accept `FieldSource | dict[FuelType, FieldSource]`; today's scalar + requests keep working unchanged when that lands. + alignment (QUICFireExportAlignmentDomainTarget | QUICFireExportAlignmentGridTarget | Unset): How the fire grid + lattice is defined. Discriminated by `target`: `'domain'` (default) pads the Domain bbox to `(dx, dy)`; `'grid'` + matches an existing grid's lattice exactly. Omit for the default Domain-anchored 2 m / 1 m fire grid. + canopy_savr (FieldSource | None | Unset): 3D canopy SAVR (1/m), optional. When provided, must be paired with + `surface_savr`; together they produce `treesss.dat` in the output zip. + surface_savr (FieldSource | None | Unset): 2D surface SAVR (1/m), optional. Pairs with `canopy_savr`. + topography (FieldSource | None | Unset): 2D elevation (m), optional. When provided, produces `topo.dat`. + rhof_merge (Literal['sum'] | Unset): How to combine canopy and surface bulk density at the bottom slab (k=0). + Currently only 'sum' is supported: `merged[0] = canopy[0] + surface_load / dz`. Mass-additive. Default: 'sum'. + moist_merge (QuicfireExportRequestMoistMerge | Unset): How to combine canopy and surface fuel moisture at the + bottom slab (k=0). `'max'` (default, v1-parity): `merged[0] = max(canopy_moist[0], surface_moist)`. + `'weighted_avg'`: `merged[0] = (canopy_rhof[0] * canopy_moist[0] + surface_rhof_layer * surface_moist) / + (canopy_rhof[0] + surface_rhof_layer)` (both moistures already converted to fraction). Default: + QuicfireExportRequestMoistMerge.MAX. + savr_merge (Literal['weighted_avg'] | Unset): How to combine canopy and surface SAVR at the bottom slab. + Currently only 'weighted_avg' is supported (mass-weighted SAVR, then converted to particle size scale `2/SAVR` + before write). Only applies when both `canopy_savr` and `surface_savr` are set. Default: 'weighted_avg'. + expiration_days (int | Unset): Days until the signed download URL expires (max 7). Default: 7. + name (str | Unset): Default: ''. + description (str | Unset): Default: ''. + tags (list[str] | Unset): + """ + + canopy_bulk_density: FieldSource + canopy_moisture: FieldSource + surface_fuel_load: FieldSource + surface_fuel_depth: FieldSource + surface_moisture: FieldSource + alignment: ( + QUICFireExportAlignmentDomainTarget | QUICFireExportAlignmentGridTarget | Unset + ) = UNSET + canopy_savr: FieldSource | None | Unset = UNSET + surface_savr: FieldSource | None | Unset = UNSET + topography: FieldSource | None | Unset = UNSET + rhof_merge: Literal["sum"] | Unset = "sum" + moist_merge: QuicfireExportRequestMoistMerge | Unset = ( + QuicfireExportRequestMoistMerge.MAX + ) + savr_merge: Literal["weighted_avg"] | Unset = "weighted_avg" + expiration_days: int | Unset = 7 + name: str | Unset = "" + description: str | Unset = "" + tags: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.field_source import FieldSource + from ..models.quic_fire_export_alignment_domain_target import ( + QUICFireExportAlignmentDomainTarget, + ) + + canopy_bulk_density = self.canopy_bulk_density.to_dict() + + canopy_moisture = self.canopy_moisture.to_dict() + + surface_fuel_load = self.surface_fuel_load.to_dict() + + surface_fuel_depth = self.surface_fuel_depth.to_dict() + + surface_moisture = self.surface_moisture.to_dict() + + alignment: dict[str, Any] | Unset + if isinstance(self.alignment, Unset): + alignment = UNSET + elif isinstance(self.alignment, QUICFireExportAlignmentDomainTarget): + alignment = self.alignment.to_dict() + else: + alignment = self.alignment.to_dict() + + canopy_savr: dict[str, Any] | None | Unset + if isinstance(self.canopy_savr, Unset): + canopy_savr = UNSET + elif isinstance(self.canopy_savr, FieldSource): + canopy_savr = self.canopy_savr.to_dict() + else: + canopy_savr = self.canopy_savr + + surface_savr: dict[str, Any] | None | Unset + if isinstance(self.surface_savr, Unset): + surface_savr = UNSET + elif isinstance(self.surface_savr, FieldSource): + surface_savr = self.surface_savr.to_dict() + else: + surface_savr = self.surface_savr + + topography: dict[str, Any] | None | Unset + if isinstance(self.topography, Unset): + topography = UNSET + elif isinstance(self.topography, FieldSource): + topography = self.topography.to_dict() + else: + topography = self.topography + + rhof_merge = self.rhof_merge + + moist_merge: str | Unset = UNSET + if not isinstance(self.moist_merge, Unset): + moist_merge = self.moist_merge.value + + savr_merge = self.savr_merge + + expiration_days = self.expiration_days + + name = self.name + + description = self.description + + tags: list[str] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "canopy_bulk_density": canopy_bulk_density, + "canopy_moisture": canopy_moisture, + "surface_fuel_load": surface_fuel_load, + "surface_fuel_depth": surface_fuel_depth, + "surface_moisture": surface_moisture, + } + ) + if alignment is not UNSET: + field_dict["alignment"] = alignment + if canopy_savr is not UNSET: + field_dict["canopy_savr"] = canopy_savr + if surface_savr is not UNSET: + field_dict["surface_savr"] = surface_savr + if topography is not UNSET: + field_dict["topography"] = topography + if rhof_merge is not UNSET: + field_dict["rhof_merge"] = rhof_merge + if moist_merge is not UNSET: + field_dict["moist_merge"] = moist_merge + if savr_merge is not UNSET: + field_dict["savr_merge"] = savr_merge + if expiration_days is not UNSET: + field_dict["expiration_days"] = expiration_days + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.field_source import FieldSource + from ..models.quic_fire_export_alignment_domain_target import ( + QUICFireExportAlignmentDomainTarget, + ) + from ..models.quic_fire_export_alignment_grid_target import ( + QUICFireExportAlignmentGridTarget, + ) + + d = dict(src_dict) + canopy_bulk_density = FieldSource.from_dict(d.pop("canopy_bulk_density")) + + canopy_moisture = FieldSource.from_dict(d.pop("canopy_moisture")) + + surface_fuel_load = FieldSource.from_dict(d.pop("surface_fuel_load")) + + surface_fuel_depth = FieldSource.from_dict(d.pop("surface_fuel_depth")) + + surface_moisture = FieldSource.from_dict(d.pop("surface_moisture")) + + def _parse_alignment( + data: object, + ) -> ( + QUICFireExportAlignmentDomainTarget + | QUICFireExportAlignmentGridTarget + | Unset + ): + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + alignment_type_0 = QUICFireExportAlignmentDomainTarget.from_dict(data) + + return alignment_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + alignment_type_1 = QUICFireExportAlignmentGridTarget.from_dict(data) + + return alignment_type_1 + + alignment = _parse_alignment(d.pop("alignment", UNSET)) + + def _parse_canopy_savr(data: object) -> FieldSource | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + canopy_savr_type_0 = FieldSource.from_dict(data) + + return canopy_savr_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FieldSource | None | Unset, data) + + canopy_savr = _parse_canopy_savr(d.pop("canopy_savr", UNSET)) + + def _parse_surface_savr(data: object) -> FieldSource | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + surface_savr_type_0 = FieldSource.from_dict(data) + + return surface_savr_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FieldSource | None | Unset, data) + + surface_savr = _parse_surface_savr(d.pop("surface_savr", UNSET)) + + def _parse_topography(data: object) -> FieldSource | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + topography_type_0 = FieldSource.from_dict(data) + + return topography_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(FieldSource | None | Unset, data) + + topography = _parse_topography(d.pop("topography", UNSET)) + + rhof_merge = cast(Literal["sum"] | Unset, d.pop("rhof_merge", UNSET)) + if rhof_merge != "sum" and not isinstance(rhof_merge, Unset): + raise ValueError(f"rhof_merge must match const 'sum', got '{rhof_merge}'") + + _moist_merge = d.pop("moist_merge", UNSET) + moist_merge: QuicfireExportRequestMoistMerge | Unset + if isinstance(_moist_merge, Unset): + moist_merge = UNSET + else: + moist_merge = QuicfireExportRequestMoistMerge(_moist_merge) + + savr_merge = cast(Literal["weighted_avg"] | Unset, d.pop("savr_merge", UNSET)) + if savr_merge != "weighted_avg" and not isinstance(savr_merge, Unset): + raise ValueError( + f"savr_merge must match const 'weighted_avg', got '{savr_merge}'" + ) + + expiration_days = d.pop("expiration_days", UNSET) + + name = d.pop("name", UNSET) + + description = d.pop("description", UNSET) + + tags = cast(list[str], d.pop("tags", UNSET)) + + quicfire_export_request = cls( + canopy_bulk_density=canopy_bulk_density, + canopy_moisture=canopy_moisture, + surface_fuel_load=surface_fuel_load, + surface_fuel_depth=surface_fuel_depth, + surface_moisture=surface_moisture, + alignment=alignment, + canopy_savr=canopy_savr, + surface_savr=surface_savr, + topography=topography, + rhof_merge=rhof_merge, + moist_merge=moist_merge, + savr_merge=savr_merge, + expiration_days=expiration_days, + name=name, + description=description, + tags=tags, + ) + + quicfire_export_request.additional_properties = d + return quicfire_export_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/quicfire_export_request_moist_merge.py b/fastfuels_sdk/v2/client_library/models/quicfire_export_request_moist_merge.py new file mode 100644 index 0000000..b8dafc9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quicfire_export_request_moist_merge.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class QuicfireExportRequestMoistMerge(str, Enum): + MAX = "max" + WEIGHTED_AVG = "weighted_avg" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/quota_exceeded_detail.py b/fastfuels_sdk/v2/client_library/models/quota_exceeded_detail.py new file mode 100644 index 0000000..e8e2201 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quota_exceeded_detail.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="QuotaExceededDetail") + + +@_attrs_define +class QuotaExceededDetail: + """Structured ``detail`` for a 429 quota rejection. + + The flat ``{reason, quota, message, current, limit}`` shape is the template + for future structured error details: a machine-readable ``reason`` code plus + flat, typed context fields. + + Attributes: + quota (str): The Quotas field that was exceeded. + message (str): Human-readable explanation and next steps. + current (int): The owner's current usage for this quota. + limit (int): The limit that was reached. + reason (str | Unset): Machine-readable error code. Default: 'QUOTA_EXCEEDED'. + window_reset_on (datetime.datetime | None | Unset): When a windowed (weekly) quota resets; absent for non- + windowed quotas. + """ + + quota: str + message: str + current: int + limit: int + reason: str | Unset = "QUOTA_EXCEEDED" + window_reset_on: datetime.datetime | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + quota = self.quota + + message = self.message + + current = self.current + + limit = self.limit + + reason = self.reason + + window_reset_on: None | str | Unset + if isinstance(self.window_reset_on, Unset): + window_reset_on = UNSET + elif isinstance(self.window_reset_on, datetime.datetime): + window_reset_on = self.window_reset_on.isoformat() + else: + window_reset_on = self.window_reset_on + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "quota": quota, + "message": message, + "current": current, + "limit": limit, + } + ) + if reason is not UNSET: + field_dict["reason"] = reason + if window_reset_on is not UNSET: + field_dict["window_reset_on"] = window_reset_on + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + quota = d.pop("quota") + + message = d.pop("message") + + current = d.pop("current") + + limit = d.pop("limit") + + reason = d.pop("reason", UNSET) + + def _parse_window_reset_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + window_reset_on_type_0 = datetime.datetime.fromisoformat(data) + + return window_reset_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + window_reset_on = _parse_window_reset_on(d.pop("window_reset_on", UNSET)) + + quota_exceeded_detail = cls( + quota=quota, + message=message, + current=current, + limit=limit, + reason=reason, + window_reset_on=window_reset_on, + ) + + quota_exceeded_detail.additional_properties = d + return quota_exceeded_detail + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/quotas.py b/fastfuels_sdk/v2/client_library/models/quotas.py new file mode 100644 index 0000000..b421536 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/quotas.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Quotas") + + +@_attrs_define +class Quotas: + """Usage limits for an owner. Field defaults are the standard tier. + + Attributes: + max_active_grids (int | Unset): Default: 25. + max_active_exports (int | Unset): Default: 10. + max_active_inventories (int | Unset): Default: 10. + max_active_features (int | Unset): Default: 10. + max_active_pointclouds (int | Unset): Default: 5. + max_domains (int | Unset): Default: 50. + max_grids (int | Unset): Default: 1000. + max_exports (int | Unset): Default: 500. + max_inventories (int | Unset): Default: 500. + max_features (int | Unset): Default: 500. + max_pointclouds (int | Unset): Default: 50. + max_api_keys (int | Unset): Default: 50. + max_applications (int | Unset): Default: 5. + max_grid_storage_bytes (int | Unset): Default: 53687091200. + max_export_storage_bytes (int | Unset): Default: 26843545600. + max_inventory_storage_bytes (int | Unset): Default: 10737418240. + max_feature_storage_bytes (int | Unset): Default: 1073741824. + max_pointcloud_storage_bytes (int | Unset): Default: 53687091200. + max_weekly_grid_dispatches (int | Unset): Grid worker jobs allowed per ISO week (Monday 00:00 UTC reset): + creates, modifications, duplicates, and uploads all count. Deleting grids does not refund spent budget. Default: + 500. + max_weekly_export_dispatches (int | Unset): Export worker jobs allowed per ISO week (Monday 00:00 UTC reset). + Deleting exports does not refund spent budget. Default: 250. + max_weekly_inventory_dispatches (int | Unset): Inventory worker jobs allowed per ISO week (Monday 00:00 UTC + reset): creates, modifications, treatments, duplicates, and uploads all count. Deleting inventories does not + refund spent budget. Default: 250. + max_weekly_feature_dispatches (int | Unset): Feature worker jobs allowed per ISO week (Monday 00:00 UTC reset). + Synchronous layerset creates are exempt. Deleting features does not refund spent budget. Default: 250. + max_weekly_pointcloud_dispatches (int | Unset): Point cloud worker jobs allowed per ISO week (Monday 00:00 UTC + reset): each upload counts. Deleting point clouds does not refund spent budget. Default: 50. + resource_ttl_days (int | None | Unset): Default: 180. + failed_resource_ttl_days (int | None | Unset): Default: 14. + """ + + max_active_grids: int | Unset = 25 + max_active_exports: int | Unset = 10 + max_active_inventories: int | Unset = 10 + max_active_features: int | Unset = 10 + max_active_pointclouds: int | Unset = 5 + max_domains: int | Unset = 50 + max_grids: int | Unset = 1000 + max_exports: int | Unset = 500 + max_inventories: int | Unset = 500 + max_features: int | Unset = 500 + max_pointclouds: int | Unset = 50 + max_api_keys: int | Unset = 50 + max_applications: int | Unset = 5 + max_grid_storage_bytes: int | Unset = 53687091200 + max_export_storage_bytes: int | Unset = 26843545600 + max_inventory_storage_bytes: int | Unset = 10737418240 + max_feature_storage_bytes: int | Unset = 1073741824 + max_pointcloud_storage_bytes: int | Unset = 53687091200 + max_weekly_grid_dispatches: int | Unset = 500 + max_weekly_export_dispatches: int | Unset = 250 + max_weekly_inventory_dispatches: int | Unset = 250 + max_weekly_feature_dispatches: int | Unset = 250 + max_weekly_pointcloud_dispatches: int | Unset = 50 + resource_ttl_days: int | None | Unset = 180 + failed_resource_ttl_days: int | None | Unset = 14 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + max_active_grids = self.max_active_grids + + max_active_exports = self.max_active_exports + + max_active_inventories = self.max_active_inventories + + max_active_features = self.max_active_features + + max_active_pointclouds = self.max_active_pointclouds + + max_domains = self.max_domains + + max_grids = self.max_grids + + max_exports = self.max_exports + + max_inventories = self.max_inventories + + max_features = self.max_features + + max_pointclouds = self.max_pointclouds + + max_api_keys = self.max_api_keys + + max_applications = self.max_applications + + max_grid_storage_bytes = self.max_grid_storage_bytes + + max_export_storage_bytes = self.max_export_storage_bytes + + max_inventory_storage_bytes = self.max_inventory_storage_bytes + + max_feature_storage_bytes = self.max_feature_storage_bytes + + max_pointcloud_storage_bytes = self.max_pointcloud_storage_bytes + + max_weekly_grid_dispatches = self.max_weekly_grid_dispatches + + max_weekly_export_dispatches = self.max_weekly_export_dispatches + + max_weekly_inventory_dispatches = self.max_weekly_inventory_dispatches + + max_weekly_feature_dispatches = self.max_weekly_feature_dispatches + + max_weekly_pointcloud_dispatches = self.max_weekly_pointcloud_dispatches + + resource_ttl_days: int | None | Unset + if isinstance(self.resource_ttl_days, Unset): + resource_ttl_days = UNSET + else: + resource_ttl_days = self.resource_ttl_days + + failed_resource_ttl_days: int | None | Unset + if isinstance(self.failed_resource_ttl_days, Unset): + failed_resource_ttl_days = UNSET + else: + failed_resource_ttl_days = self.failed_resource_ttl_days + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if max_active_grids is not UNSET: + field_dict["max_active_grids"] = max_active_grids + if max_active_exports is not UNSET: + field_dict["max_active_exports"] = max_active_exports + if max_active_inventories is not UNSET: + field_dict["max_active_inventories"] = max_active_inventories + if max_active_features is not UNSET: + field_dict["max_active_features"] = max_active_features + if max_active_pointclouds is not UNSET: + field_dict["max_active_pointclouds"] = max_active_pointclouds + if max_domains is not UNSET: + field_dict["max_domains"] = max_domains + if max_grids is not UNSET: + field_dict["max_grids"] = max_grids + if max_exports is not UNSET: + field_dict["max_exports"] = max_exports + if max_inventories is not UNSET: + field_dict["max_inventories"] = max_inventories + if max_features is not UNSET: + field_dict["max_features"] = max_features + if max_pointclouds is not UNSET: + field_dict["max_pointclouds"] = max_pointclouds + if max_api_keys is not UNSET: + field_dict["max_api_keys"] = max_api_keys + if max_applications is not UNSET: + field_dict["max_applications"] = max_applications + if max_grid_storage_bytes is not UNSET: + field_dict["max_grid_storage_bytes"] = max_grid_storage_bytes + if max_export_storage_bytes is not UNSET: + field_dict["max_export_storage_bytes"] = max_export_storage_bytes + if max_inventory_storage_bytes is not UNSET: + field_dict["max_inventory_storage_bytes"] = max_inventory_storage_bytes + if max_feature_storage_bytes is not UNSET: + field_dict["max_feature_storage_bytes"] = max_feature_storage_bytes + if max_pointcloud_storage_bytes is not UNSET: + field_dict["max_pointcloud_storage_bytes"] = max_pointcloud_storage_bytes + if max_weekly_grid_dispatches is not UNSET: + field_dict["max_weekly_grid_dispatches"] = max_weekly_grid_dispatches + if max_weekly_export_dispatches is not UNSET: + field_dict["max_weekly_export_dispatches"] = max_weekly_export_dispatches + if max_weekly_inventory_dispatches is not UNSET: + field_dict["max_weekly_inventory_dispatches"] = ( + max_weekly_inventory_dispatches + ) + if max_weekly_feature_dispatches is not UNSET: + field_dict["max_weekly_feature_dispatches"] = max_weekly_feature_dispatches + if max_weekly_pointcloud_dispatches is not UNSET: + field_dict["max_weekly_pointcloud_dispatches"] = ( + max_weekly_pointcloud_dispatches + ) + if resource_ttl_days is not UNSET: + field_dict["resource_ttl_days"] = resource_ttl_days + if failed_resource_ttl_days is not UNSET: + field_dict["failed_resource_ttl_days"] = failed_resource_ttl_days + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + max_active_grids = d.pop("max_active_grids", UNSET) + + max_active_exports = d.pop("max_active_exports", UNSET) + + max_active_inventories = d.pop("max_active_inventories", UNSET) + + max_active_features = d.pop("max_active_features", UNSET) + + max_active_pointclouds = d.pop("max_active_pointclouds", UNSET) + + max_domains = d.pop("max_domains", UNSET) + + max_grids = d.pop("max_grids", UNSET) + + max_exports = d.pop("max_exports", UNSET) + + max_inventories = d.pop("max_inventories", UNSET) + + max_features = d.pop("max_features", UNSET) + + max_pointclouds = d.pop("max_pointclouds", UNSET) + + max_api_keys = d.pop("max_api_keys", UNSET) + + max_applications = d.pop("max_applications", UNSET) + + max_grid_storage_bytes = d.pop("max_grid_storage_bytes", UNSET) + + max_export_storage_bytes = d.pop("max_export_storage_bytes", UNSET) + + max_inventory_storage_bytes = d.pop("max_inventory_storage_bytes", UNSET) + + max_feature_storage_bytes = d.pop("max_feature_storage_bytes", UNSET) + + max_pointcloud_storage_bytes = d.pop("max_pointcloud_storage_bytes", UNSET) + + max_weekly_grid_dispatches = d.pop("max_weekly_grid_dispatches", UNSET) + + max_weekly_export_dispatches = d.pop("max_weekly_export_dispatches", UNSET) + + max_weekly_inventory_dispatches = d.pop( + "max_weekly_inventory_dispatches", UNSET + ) + + max_weekly_feature_dispatches = d.pop("max_weekly_feature_dispatches", UNSET) + + max_weekly_pointcloud_dispatches = d.pop( + "max_weekly_pointcloud_dispatches", UNSET + ) + + def _parse_resource_ttl_days(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + resource_ttl_days = _parse_resource_ttl_days(d.pop("resource_ttl_days", UNSET)) + + def _parse_failed_resource_ttl_days(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + failed_resource_ttl_days = _parse_failed_resource_ttl_days( + d.pop("failed_resource_ttl_days", UNSET) + ) + + quotas = cls( + max_active_grids=max_active_grids, + max_active_exports=max_active_exports, + max_active_inventories=max_active_inventories, + max_active_features=max_active_features, + max_active_pointclouds=max_active_pointclouds, + max_domains=max_domains, + max_grids=max_grids, + max_exports=max_exports, + max_inventories=max_inventories, + max_features=max_features, + max_pointclouds=max_pointclouds, + max_api_keys=max_api_keys, + max_applications=max_applications, + max_grid_storage_bytes=max_grid_storage_bytes, + max_export_storage_bytes=max_export_storage_bytes, + max_inventory_storage_bytes=max_inventory_storage_bytes, + max_feature_storage_bytes=max_feature_storage_bytes, + max_pointcloud_storage_bytes=max_pointcloud_storage_bytes, + max_weekly_grid_dispatches=max_weekly_grid_dispatches, + max_weekly_export_dispatches=max_weekly_export_dispatches, + max_weekly_inventory_dispatches=max_weekly_inventory_dispatches, + max_weekly_feature_dispatches=max_weekly_feature_dispatches, + max_weekly_pointcloud_dispatches=max_weekly_pointcloud_dispatches, + resource_ttl_days=resource_ttl_days, + failed_resource_ttl_days=failed_resource_ttl_days, + ) + + quotas.additional_properties = d + return quotas + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/remove_action.py b/fastfuels_sdk/v2/client_library/models/remove_action.py new file mode 100644 index 0000000..31d19ed --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/remove_action.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="RemoveAction") + + +@_attrs_define +class RemoveAction: + """Action that removes matching trees from the inventory. + + Attributes: + modifier (Literal['remove'] | Unset): Default: 'remove'. + """ + + modifier: Literal["remove"] | Unset = "remove" + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + modifier = self.modifier + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if modifier is not UNSET: + field_dict["modifier"] = modifier + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + modifier = cast(Literal["remove"] | Unset, d.pop("modifier", UNSET)) + if modifier != "remove" and not isinstance(modifier, Unset): + raise ValueError(f"modifier must match const 'remove', got '{modifier}'") + + remove_action = cls( + modifier=modifier, + ) + + remove_action.additional_properties = d + return remove_action + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/resampling_method.py b/fastfuels_sdk/v2/client_library/models/resampling_method.py new file mode 100644 index 0000000..e287911 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/resampling_method.py @@ -0,0 +1,21 @@ +from enum import Enum + + +class ResamplingMethod(str, Enum): + AVERAGE = "average" + BILINEAR = "bilinear" + CUBIC = "cubic" + CUBIC_SPLINE = "cubic_spline" + FIRST_QUARTILE = "first_quartile" + LANCZOS = "lanczos" + MAX = "max" + MEDIAN = "median" + MIN = "min" + MODE = "mode" + NEAREST = "nearest" + ROOT_MEAN_SQUARE = "root_mean_square" + SUM = "sum" + THIRD_QUARTILE = "third_quartile" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/resolution_3d.py b/fastfuels_sdk/v2/client_library/models/resolution_3d.py new file mode 100644 index 0000000..ab84440 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/resolution_3d.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="Resolution3D") + + +@_attrs_define +class Resolution3D: + """Voxel resolution for a 3D grid. + + `horizontal` applies to both x and y (fastfuels-core requires isotropic + horizontal resolution). `vertical` is independent. + + Attributes: + horizontal (float): Cell size in x and y, meters. + vertical (float): Cell size in z, meters. + """ + + horizontal: float + vertical: float + + def to_dict(self) -> dict[str, Any]: + horizontal = self.horizontal + + vertical = self.vertical + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "horizontal": horizontal, + "vertical": vertical, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + horizontal = d.pop("horizontal") + + vertical = d.pop("vertical") + + resolution_3d = cls( + horizontal=horizontal, + vertical=vertical, + ) + + return resolution_3d diff --git a/fastfuels_sdk/v2/client_library/models/scope.py b/fastfuels_sdk/v2/client_library/models/scope.py new file mode 100644 index 0000000..06c3e11 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/scope.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class Scope(str, Enum): + READ = "read" + WRITE = "write" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/sort_order.py b/fastfuels_sdk/v2/client_library/models/sort_order.py new file mode 100644 index 0000000..87e7de1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/sort_order.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class SortOrder(str, Enum): + ASCENDING = "ascending" + DESCENDING = "descending" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/sparse_grid_data.py b/fastfuels_sdk/v2/client_library/models/sparse_grid_data.py new file mode 100644 index 0000000..00ad40e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/sparse_grid_data.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SparseGridData") + + +@_attrs_define +class SparseGridData: + """ + Attributes: + format_ (Literal['sparse']): + fill_value (float | int | None): + indices (list[int]): + values (list[float | int]): + """ + + format_: Literal["sparse"] + fill_value: float | int | None + indices: list[int] + values: list[float | int] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + format_ = self.format_ + + fill_value: float | int | None + fill_value = self.fill_value + + indices = self.indices + + values = [] + for values_item_data in self.values: + values_item: float | int + values_item = values_item_data + values.append(values_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "format": format_, + "fill_value": fill_value, + "indices": indices, + "values": values, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + format_ = cast(Literal["sparse"], d.pop("format")) + if format_ != "sparse": + raise ValueError(f"format must match const 'sparse', got '{format_}'") + + def _parse_fill_value(data: object) -> float | int | None: + if data is None: + return data + return cast(float | int | None, data) + + fill_value = _parse_fill_value(d.pop("fill_value")) + + indices = cast(list[int], d.pop("indices")) + + values = [] + _values = d.pop("values") + for values_item_data in _values: + + def _parse_values_item(data: object) -> float | int: + return cast(float | int, data) + + values_item = _parse_values_item(values_item_data) + + values.append(values_item) + + sparse_grid_data = cls( + format_=format_, + fill_value=fill_value, + indices=indices, + values=values, + ) + + sparse_grid_data.additional_properties = d + return sparse_grid_data + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/spatial_operator.py b/fastfuels_sdk/v2/client_library/models/spatial_operator.py new file mode 100644 index 0000000..67ba0be --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/spatial_operator.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class SpatialOperator(str, Enum): + INTERSECTS = "intersects" + OUTSIDE = "outside" + WITHIN = "within" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/stem_isolation_lmf.py b/fastfuels_sdk/v2/client_library/models/stem_isolation_lmf.py new file mode 100644 index 0000000..6da4bcc --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/stem_isolation_lmf.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StemIsolationLmf") + + +@_attrs_define +class StemIsolationLmf: + """Parameters for Local Maximum Filter (LMF) stem isolation. + + When set, ``max_height`` must be greater than ``min_height``. + + Attributes: + name (Literal['lmf'] | Unset): Default: 'lmf'. + min_height (float | Unset): Minimum height threshold (in meters) for a treetop. Default: 2.0. + max_height (float | None | Unset): Maximum height threshold (in meters) for a treetop. CHM returns taller than + this are treated as artifacts (e.g. LiDAR noise spikes) and excluded before detection. Defaults to 120, above + the tallest known tree; set to null to disable the ceiling. Default: 120.0. + footprint_size (int | Unset): Diameter of the circular footprint in pixels. Must be an odd integer. Default: 3. + """ + + name: Literal["lmf"] | Unset = "lmf" + min_height: float | Unset = 2.0 + max_height: float | None | Unset = 120.0 + footprint_size: int | Unset = 3 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + min_height = self.min_height + + max_height: float | None | Unset + if isinstance(self.max_height, Unset): + max_height = UNSET + else: + max_height = self.max_height + + footprint_size = self.footprint_size + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if min_height is not UNSET: + field_dict["min_height"] = min_height + if max_height is not UNSET: + field_dict["max_height"] = max_height + if footprint_size is not UNSET: + field_dict["footprint_size"] = footprint_size + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = cast(Literal["lmf"] | Unset, d.pop("name", UNSET)) + if name != "lmf" and not isinstance(name, Unset): + raise ValueError(f"name must match const 'lmf', got '{name}'") + + min_height = d.pop("min_height", UNSET) + + def _parse_max_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + max_height = _parse_max_height(d.pop("max_height", UNSET)) + + footprint_size = d.pop("footprint_size", UNSET) + + stem_isolation_lmf = cls( + name=name, + min_height=min_height, + max_height=max_height, + footprint_size=footprint_size, + ) + + stem_isolation_lmf.additional_properties = d + return stem_isolation_lmf + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/stem_isolation_vwf.py b/fastfuels_sdk/v2/client_library/models/stem_isolation_vwf.py new file mode 100644 index 0000000..183ff9b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/stem_isolation_vwf.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StemIsolationVwf") + + +@_attrs_define +class StemIsolationVwf: + """Parameters for Variable Window Filter (VWF) stem isolation. + + When set, ``max_height`` must be greater than ``min_height``. + + Attributes: + name (Literal['vwf'] | Unset): Default: 'vwf'. + min_height (float | Unset): Minimum height threshold (in meters) for a treetop. Default: 2.0. + max_height (float | None | Unset): Maximum height threshold (in meters) for a treetop. CHM returns taller than + this are treated as artifacts (e.g. LiDAR noise spikes) and excluded before detection. Defaults to 120, above + the tallest known tree; set to null to disable the ceiling. Default: 120.0. + spatial_resolution (float | None | Unset): Spatial resolution of the CHM. If omitted, it will be automatically + inferred from the source grid metadata. + crown_ratio (float | Unset): Multiplier used to dynamically scale the search window based on pixel height. + Default: 0.1. + crown_offset (float | Unset): Constant offset (in meters) added to the dynamic search window. Default: 1.0. + """ + + name: Literal["vwf"] | Unset = "vwf" + min_height: float | Unset = 2.0 + max_height: float | None | Unset = 120.0 + spatial_resolution: float | None | Unset = UNSET + crown_ratio: float | Unset = 0.1 + crown_offset: float | Unset = 1.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + min_height = self.min_height + + max_height: float | None | Unset + if isinstance(self.max_height, Unset): + max_height = UNSET + else: + max_height = self.max_height + + spatial_resolution: float | None | Unset + if isinstance(self.spatial_resolution, Unset): + spatial_resolution = UNSET + else: + spatial_resolution = self.spatial_resolution + + crown_ratio = self.crown_ratio + + crown_offset = self.crown_offset + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if min_height is not UNSET: + field_dict["min_height"] = min_height + if max_height is not UNSET: + field_dict["max_height"] = max_height + if spatial_resolution is not UNSET: + field_dict["spatial_resolution"] = spatial_resolution + if crown_ratio is not UNSET: + field_dict["crown_ratio"] = crown_ratio + if crown_offset is not UNSET: + field_dict["crown_offset"] = crown_offset + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = cast(Literal["vwf"] | Unset, d.pop("name", UNSET)) + if name != "vwf" and not isinstance(name, Unset): + raise ValueError(f"name must match const 'vwf', got '{name}'") + + min_height = d.pop("min_height", UNSET) + + def _parse_max_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + max_height = _parse_max_height(d.pop("max_height", UNSET)) + + def _parse_spatial_resolution(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + spatial_resolution = _parse_spatial_resolution( + d.pop("spatial_resolution", UNSET) + ) + + crown_ratio = d.pop("crown_ratio", UNSET) + + crown_offset = d.pop("crown_offset", UNSET) + + stem_isolation_vwf = cls( + name=name, + min_height=min_height, + max_height=max_height, + spatial_resolution=spatial_resolution, + crown_ratio=crown_ratio, + crown_offset=crown_offset, + ) + + stem_isolation_vwf.additional_properties = d + return stem_isolation_vwf + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/three_dep_dataset_coverage.py b/fastfuels_sdk/v2/client_library/models/three_dep_dataset_coverage.py new file mode 100644 index 0000000..32997f8 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/three_dep_dataset_coverage.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ThreeDepDatasetCoverage") + + +@_attrs_define +class ThreeDepDatasetCoverage: + """One 3DEP acquisition available over a domain, and what it would supply. + + Attributes: + name (str): USGS acquisition name. Pass it in `datasets` on a create request to pin the fetch to this + acquisition. + url (str): Location of the acquisition's Entwine Point Tile index. + contribution_fraction (float): Fraction of the domain this acquisition would supply, from `0.0` to `1.0`. + Acquisitions overlap each other freely, so this is the share left over after the acquisitions listed before it + have taken their part — not the raw overlap. The values are therefore disjoint and sum to `coverage_fraction`. + estimated_density (float): Average point density of the acquisition, in points per square metre, computed over + its full published extent. + estimated_points (int): Approximate number of points this acquisition would contribute. Derived from its density + and the area it covers, so treat it as an order-of-magnitude figure rather than an exact count. + """ + + name: str + url: str + contribution_fraction: float + estimated_density: float + estimated_points: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + url = self.url + + contribution_fraction = self.contribution_fraction + + estimated_density = self.estimated_density + + estimated_points = self.estimated_points + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "url": url, + "contribution_fraction": contribution_fraction, + "estimated_density": estimated_density, + "estimated_points": estimated_points, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + name = d.pop("name") + + url = d.pop("url") + + contribution_fraction = d.pop("contribution_fraction") + + estimated_density = d.pop("estimated_density") + + estimated_points = d.pop("estimated_points") + + three_dep_dataset_coverage = cls( + name=name, + url=url, + contribution_fraction=contribution_fraction, + estimated_density=estimated_density, + estimated_points=estimated_points, + ) + + three_dep_dataset_coverage.additional_properties = d + return three_dep_dataset_coverage + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/three_dep_resolution.py b/fastfuels_sdk/v2/client_library/models/three_dep_resolution.py new file mode 100644 index 0000000..adfcd21 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/three_dep_resolution.py @@ -0,0 +1,10 @@ +from enum import IntEnum + + +class ThreeDepResolution(IntEnum): + VALUE_1 = 1 + VALUE_10 = 10 + VALUE_30 = 30 + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/topography_band.py b/fastfuels_sdk/v2/client_library/models/topography_band.py new file mode 100644 index 0000000..359226c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/topography_band.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TopographyBand(str, Enum): + ASPECT = "aspect" + ELEVATION = "elevation" + SLOPE = "slope" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/topography_three_dep_coverage_response.py b/fastfuels_sdk/v2/client_library/models/topography_three_dep_coverage_response.py new file mode 100644 index 0000000..0ab82e1 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/topography_three_dep_coverage_response.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.three_dep_resolution import ThreeDepResolution +from ..types import UNSET, Unset + +T = TypeVar("T", bound="TopographyThreeDepCoverageResponse") + + +@_attrs_define +class TopographyThreeDepCoverageResponse: + """Response model for 3DEP tile coverage pre-flight check. + + Attributes: + resolution (ThreeDepResolution): Available resolutions for 3DEP data (meters). + available (bool): + tile_count (int): + tiles (list[str]): + acquisition_dates (list[str] | None | Unset): + """ + + resolution: ThreeDepResolution + available: bool + tile_count: int + tiles: list[str] + acquisition_dates: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + resolution = self.resolution.value + + available = self.available + + tile_count = self.tile_count + + tiles = self.tiles + + acquisition_dates: list[str] | None | Unset + if isinstance(self.acquisition_dates, Unset): + acquisition_dates = UNSET + elif isinstance(self.acquisition_dates, list): + acquisition_dates = self.acquisition_dates + + else: + acquisition_dates = self.acquisition_dates + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "resolution": resolution, + "available": available, + "tile_count": tile_count, + "tiles": tiles, + } + ) + if acquisition_dates is not UNSET: + field_dict["acquisition_dates"] = acquisition_dates + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + resolution = ThreeDepResolution(d.pop("resolution")) + + available = d.pop("available") + + tile_count = d.pop("tile_count") + + tiles = cast(list[str], d.pop("tiles")) + + def _parse_acquisition_dates(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + acquisition_dates_type_0 = cast(list[str], data) + + return acquisition_dates_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + acquisition_dates = _parse_acquisition_dates(d.pop("acquisition_dates", UNSET)) + + topography_three_dep_coverage_response = cls( + resolution=resolution, + available=available, + tile_count=tile_count, + tiles=tiles, + acquisition_dates=acquisition_dates, + ) + + topography_three_dep_coverage_response.additional_properties = d + return topography_three_dep_coverage_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/tree_band.py b/fastfuels_sdk/v2/client_library/models/tree_band.py new file mode 100644 index 0000000..af1cc95 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/tree_band.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class TreeBand(str, Enum): + BULK_DENSITY_BRANCHWOOD_DEAD = "bulk_density.branchwood.dead" + BULK_DENSITY_BRANCHWOOD_LIVE = "bulk_density.branchwood.live" + BULK_DENSITY_FINE_DEAD = "bulk_density.fine.dead" + BULK_DENSITY_FINE_LIVE = "bulk_density.fine.live" + BULK_DENSITY_FOLIAGE_DEAD = "bulk_density.foliage.dead" + BULK_DENSITY_FOLIAGE_LIVE = "bulk_density.foliage.live" + FUEL_MOISTURE_DEAD = "fuel_moisture.dead" + FUEL_MOISTURE_LIVE = "fuel_moisture.live" + LEAF_AREA_DENSITY = "leaf_area_density" + SAVR_FOLIAGE = "savr.foliage" + SPCD = "spcd" + TREE_ID = "tree_id" + VOLUME_FRACTION = "volume_fraction" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/tree_forestry_metrics.py b/fastfuels_sdk/v2/client_library/models/tree_forestry_metrics.py new file mode 100644 index 0000000..4290a87 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/tree_forestry_metrics.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.fia_species_group_share import FIASpeciesGroupShare + + +T = TypeVar("T", bound="TreeForestryMetrics") + + +@_attrs_define +class TreeForestryMetrics: + """Stand-level forestry scalars for a tree inventory. + + Attributes: + type_ (Literal['tree']): + tree_count (int): Total trees in the inventory. + basal_area_per_area (float | None): Stand basal area divided by domain area. Unit: ft**2/acre. + tree_density (float | None): Trees per unit domain area (TPA). Unit: 1/acre. + quadratic_mean_diameter (float | None): Quadratic mean DBH. Unit: in. + dominant_species_groups (list[FIASpeciesGroupShare] | Unset): The N FIA species groups with the largest basal + area share, sorted descending (N defaults to 5). Only the top N are returned; any remaining groups are omitted, + so the listed shares may sum to less than 1. + """ + + type_: Literal["tree"] + tree_count: int + basal_area_per_area: float | None + tree_density: float | None + quadratic_mean_diameter: float | None + dominant_species_groups: list[FIASpeciesGroupShare] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + tree_count = self.tree_count + + basal_area_per_area: float | None + basal_area_per_area = self.basal_area_per_area + + tree_density: float | None + tree_density = self.tree_density + + quadratic_mean_diameter: float | None + quadratic_mean_diameter = self.quadratic_mean_diameter + + dominant_species_groups: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.dominant_species_groups, Unset): + dominant_species_groups = [] + for dominant_species_groups_item_data in self.dominant_species_groups: + dominant_species_groups_item = ( + dominant_species_groups_item_data.to_dict() + ) + dominant_species_groups.append(dominant_species_groups_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "tree_count": tree_count, + "basal_area_per_area": basal_area_per_area, + "tree_density": tree_density, + "quadratic_mean_diameter": quadratic_mean_diameter, + } + ) + if dominant_species_groups is not UNSET: + field_dict["dominant_species_groups"] = dominant_species_groups + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.fia_species_group_share import FIASpeciesGroupShare + + d = dict(src_dict) + type_ = cast(Literal["tree"], d.pop("type")) + if type_ != "tree": + raise ValueError(f"type must match const 'tree', got '{type_}'") + + tree_count = d.pop("tree_count") + + def _parse_basal_area_per_area(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + basal_area_per_area = _parse_basal_area_per_area(d.pop("basal_area_per_area")) + + def _parse_tree_density(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + tree_density = _parse_tree_density(d.pop("tree_density")) + + def _parse_quadratic_mean_diameter(data: object) -> float | None: + if data is None: + return data + return cast(float | None, data) + + quadratic_mean_diameter = _parse_quadratic_mean_diameter( + d.pop("quadratic_mean_diameter") + ) + + _dominant_species_groups = d.pop("dominant_species_groups", UNSET) + dominant_species_groups: list[FIASpeciesGroupShare] | Unset = UNSET + if _dominant_species_groups is not UNSET: + dominant_species_groups = [] + for dominant_species_groups_item_data in _dominant_species_groups: + dominant_species_groups_item = FIASpeciesGroupShare.from_dict( + dominant_species_groups_item_data + ) + + dominant_species_groups.append(dominant_species_groups_item) + + tree_forestry_metrics = cls( + type_=type_, + tree_count=tree_count, + basal_area_per_area=basal_area_per_area, + tree_density=tree_density, + quadratic_mean_diameter=quadratic_mean_diameter, + dominant_species_groups=dominant_species_groups, + ) + + tree_forestry_metrics.additional_properties = d + return tree_forestry_metrics + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/tree_map_band.py b/fastfuels_sdk/v2/client_library/models/tree_map_band.py new file mode 100644 index 0000000..656b64c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/tree_map_band.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class TreeMapBand(str, Enum): + PLT_CN = "plt_cn" + TM_ID = "tm_id" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/tree_map_version.py b/fastfuels_sdk/v2/client_library/models/tree_map_version.py new file mode 100644 index 0000000..295708a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/tree_map_version.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class TreeMapVersion(str, Enum): + VALUE_0 = "2014" + VALUE_1 = "2016" + VALUE_2 = "2020" + VALUE_3 = "2022" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/uniform_band.py b/fastfuels_sdk/v2/client_library/models/uniform_band.py new file mode 100644 index 0000000..d6e5894 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/uniform_band.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class UniformBand(str, Enum): + CURING = "curing" + FUEL_DEPTH = "fuel_depth" + FUEL_LOAD_100HR = "fuel_load.100hr" + FUEL_LOAD_10HR = "fuel_load.10hr" + FUEL_LOAD_1HR = "fuel_load.1hr" + FUEL_LOAD_LIVE_HERB = "fuel_load.live_herb" + FUEL_LOAD_LIVE_WOODY = "fuel_load.live_woody" + FUEL_MOISTURE_100HR = "fuel_moisture.100hr" + FUEL_MOISTURE_10HR = "fuel_moisture.10hr" + FUEL_MOISTURE_1HR = "fuel_moisture.1hr" + FUEL_MOISTURE_LIVE_HERB = "fuel_moisture.live_herb" + FUEL_MOISTURE_LIVE_WOODY = "fuel_moisture.live_woody" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/uniform_band_input.py b/fastfuels_sdk/v2/client_library/models/uniform_band_input.py new file mode 100644 index 0000000..90313b3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/uniform_band_input.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.uniform_band import UniformBand + +T = TypeVar("T", bound="UniformBandInput") + + +@_attrs_define +class UniformBandInput: + """A single band specification for a uniform grid. + + Users provide a band key (from the predefined list) and a constant value. + The API resolves the key to unit and type. + + Attributes: + key (UniformBand): Predefined bands available for uniform grids. + value (float | int): + """ + + key: UniformBand + value: float | int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + key = self.key.value + + value: float | int + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "key": key, + "value": value, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + key = UniformBand(d.pop("key")) + + def _parse_value(data: object) -> float | int: + return cast(float | int, data) + + value = _parse_value(d.pop("value")) + + uniform_band_input = cls( + key=key, + value=value, + ) + + uniform_band_input.additional_properties = d + return uniform_band_input + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/uniform_moisture_value.py b/fastfuels_sdk/v2/client_library/models/uniform_moisture_value.py new file mode 100644 index 0000000..cc1ca31 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/uniform_moisture_value.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + Any, + Literal, + Self, + TypeVar, + cast, +) + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UniformMoistureValue") + + +@_attrs_define +class UniformMoistureValue: + """Uniform fuel moisture for one fuel state. + + Attributes: + method (Literal['uniform'] | Unset): Default: 'uniform'. + value (float | Unset): Fuel moisture content (%), applied uniformly. Default: 100.0. + """ + + method: Literal["uniform"] | Unset = "uniform" + value: float | Unset = 100.0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + method = self.method + + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if method is not UNSET: + field_dict["method"] = method + if value is not UNSET: + field_dict["value"] = value + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + method = cast(Literal["uniform"] | Unset, d.pop("method", UNSET)) + if method != "uniform" and not isinstance(method, Unset): + raise ValueError(f"method must match const 'uniform', got '{method}'") + + value = d.pop("value", UNSET) + + uniform_moisture_value = cls( + method=method, + value=value, + ) + + uniform_moisture_value.additional_properties = d + return uniform_moisture_value + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_application_request.py b/fastfuels_sdk/v2/client_library/models/update_application_request.py new file mode 100644 index 0000000..fd59f6c --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_application_request.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateApplicationRequest") + + +@_attrs_define +class UpdateApplicationRequest: + """Request body for updating an application. + + Attributes: + name (None | str | Unset): New name for the application. + description (None | str | Unset): New description for the application. + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + update_application_request = cls( + name=name, + description=description, + ) + + return update_application_request diff --git a/fastfuels_sdk/v2/client_library/models/update_domain_request_body.py b/fastfuels_sdk/v2/client_library/models/update_domain_request_body.py new file mode 100644 index 0000000..64867f9 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_domain_request_body.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.domain_style import DomainStyle + + +T = TypeVar("T", bound="UpdateDomainRequestBody") + + +@_attrs_define +class UpdateDomainRequestBody: + """Request body for updating a domain's metadata. + + All fields are optional. Only provided fields will be updated. + Geometry (features) and CRS cannot be modified after creation. + + Attributes: + name (None | str | Unset): The name of the domain. + description (None | str | Unset): A description of the domain. + tags (list[str] | None | Unset): A list of tags associated with the domain. + style (DomainStyle | None | Unset): Update visual style fields. Only provided sub-fields are merged into the + existing style; unspecified sub-fields preserve their current values. + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + style: DomainStyle | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.domain_style import DomainStyle + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + style: dict[str, Any] | None | Unset + if isinstance(self.style, Unset): + style = UNSET + elif isinstance(self.style, DomainStyle): + style = self.style.to_dict() + else: + style = self.style + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + if style is not UNSET: + field_dict["style"] = style + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.domain_style import DomainStyle + + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + def _parse_style(data: object) -> DomainStyle | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + style_type_0 = DomainStyle.from_dict(data) + + return style_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DomainStyle | None | Unset, data) + + style = _parse_style(d.pop("style", UNSET)) + + update_domain_request_body = cls( + name=name, + description=description, + tags=tags, + style=style, + ) + + update_domain_request_body.additional_properties = d + return update_domain_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_export_request_body.py b/fastfuels_sdk/v2/client_library/models/update_export_request_body.py new file mode 100644 index 0000000..b11952e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_export_request_body.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateExportRequestBody") + + +@_attrs_define +class UpdateExportRequestBody: + """Request body for updating export metadata. + + Attributes: + name (None | str | Unset): + description (None | str | Unset): + tags (list[str] | None | Unset): + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + update_export_request_body = cls( + name=name, + description=description, + tags=tags, + ) + + update_export_request_body.additional_properties = d + return update_export_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_feature_request_body.py b/fastfuels_sdk/v2/client_library/models/update_feature_request_body.py new file mode 100644 index 0000000..042acde --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_feature_request_body.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateFeatureRequestBody") + + +@_attrs_define +class UpdateFeatureRequestBody: + """Request body for updating feature metadata. + + Attributes: + name (None | str | Unset): + description (None | str | Unset): + tags (list[str] | None | Unset): + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + update_feature_request_body = cls( + name=name, + description=description, + tags=tags, + ) + + update_feature_request_body.additional_properties = d + return update_feature_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_grid_request_body.py b/fastfuels_sdk/v2/client_library/models/update_grid_request_body.py new file mode 100644 index 0000000..219573f --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_grid_request_body.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateGridRequestBody") + + +@_attrs_define +class UpdateGridRequestBody: + """Request body for updating grid metadata. + + Attributes: + name (None | str | Unset): + description (None | str | Unset): + tags (list[str] | None | Unset): + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + update_grid_request_body = cls( + name=name, + description=description, + tags=tags, + ) + + update_grid_request_body.additional_properties = d + return update_grid_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_inventory_request_body.py b/fastfuels_sdk/v2/client_library/models/update_inventory_request_body.py new file mode 100644 index 0000000..f0ef04b --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_inventory_request_body.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateInventoryRequestBody") + + +@_attrs_define +class UpdateInventoryRequestBody: + """Request body for updating inventory metadata. + + Attributes: + name (None | str | Unset): + description (None | str | Unset): + tags (list[str] | None | Unset): + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + update_inventory_request_body = cls( + name=name, + description=description, + tags=tags, + ) + + update_inventory_request_body.additional_properties = d + return update_inventory_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/update_point_cloud_request_body.py b/fastfuels_sdk/v2/client_library/models/update_point_cloud_request_body.py new file mode 100644 index 0000000..43723a4 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/update_point_cloud_request_body.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdatePointCloudRequestBody") + + +@_attrs_define +class UpdatePointCloudRequestBody: + """Request body for updating point cloud metadata. + + Only metadata is mutable. The point cloud's content, source, and derived + fields cannot be changed through this endpoint, so updates never alter the + `checksum`. + + Attributes: + name (None | str | Unset): New name. Omit to leave unchanged. + description (None | str | Unset): New description. Omit to leave unchanged. + tags (list[str] | None | Unset): New tags (replaces the existing list). Omit to leave unchanged. + """ + + name: None | str | Unset = UNSET + description: None | str | Unset = UNSET + tags: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + tags: list[str] | None | Unset + if isinstance(self.tags, Unset): + tags = UNSET + elif isinstance(self.tags, list): + tags = self.tags + + else: + tags = self.tags + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + if description is not UNSET: + field_dict["description"] = description + if tags is not UNSET: + field_dict["tags"] = tags + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + def _parse_tags(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + tags_type_0 = cast(list[str], data) + + return tags_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + tags = _parse_tags(d.pop("tags", UNSET)) + + update_point_cloud_request_body = cls( + name=name, + description=description, + tags=tags, + ) + + update_point_cloud_request_body.additional_properties = d + return update_point_cloud_request_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/upload_band_definition.py b/fastfuels_sdk/v2/client_library/models/upload_band_definition.py new file mode 100644 index 0000000..9c0455e --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/upload_band_definition.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.band_type import BandType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UploadBandDefinition") + + +@_attrs_define +class UploadBandDefinition: + """ + Attributes: + key (str): Dot-notation variable name, e.g. 'bulk_density.foliage' + type_ (BandType): Type of band data. + unit (None | str | Unset): Physical unit of the band's pixel values, in UDUNITS-2-conformant ASCII form with + `**` for exponents (e.g. `kg/m**3`, `1/m`, `%`). Optional for categorical/identifier bands. Non-canonical forms + (`kg/m³`, `kg/m^3`, `kg/m3`) are rejected. See docs/units.md. + """ + + key: str + type_: BandType + unit: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + key = self.key + + type_ = self.type_.value + + unit: None | str | Unset + if isinstance(self.unit, Unset): + unit = UNSET + else: + unit = self.unit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "key": key, + "type": type_, + } + ) + if unit is not UNSET: + field_dict["unit"] = unit + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + key = d.pop("key") + + type_ = BandType(d.pop("type")) + + def _parse_unit(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + unit = _parse_unit(d.pop("unit", UNSET)) + + upload_band_definition = cls( + key=key, + type_=type_, + unit=unit, + ) + + upload_band_definition.additional_properties = d + return upload_band_definition + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/usage.py b/fastfuels_sdk/v2/client_library/models/usage.py new file mode 100644 index 0000000..6f0b0ec --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/usage.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.count_usage import CountUsage + from ..models.job_resource_usage import JobResourceUsage + from ..models.usage_lifecycle import UsageLifecycle + + +T = TypeVar("T", bound="Usage") + + +@_attrs_define +class Usage: + """An owner's current usage against their resolved limits. + + Attributes: + grids (JobResourceUsage): Usage for a resource type that produces jobs and stores artifacts. + exports (JobResourceUsage): Usage for a resource type that produces jobs and stores artifacts. + inventories (JobResourceUsage): Usage for a resource type that produces jobs and stores artifacts. + features (JobResourceUsage): Usage for a resource type that produces jobs and stores artifacts. + pointclouds (JobResourceUsage): Usage for a resource type that produces jobs and stores artifacts. + domains (CountUsage): Usage for a count-only resource type (domains, applications, API keys). + applications (CountUsage): Usage for a count-only resource type (domains, applications, API keys). + api_keys (CountUsage): Usage for a count-only resource type (domains, applications, API keys). + lifecycle (UsageLifecycle): Retention policy in effect for the owner's resources. + """ + + grids: JobResourceUsage + exports: JobResourceUsage + inventories: JobResourceUsage + features: JobResourceUsage + pointclouds: JobResourceUsage + domains: CountUsage + applications: CountUsage + api_keys: CountUsage + lifecycle: UsageLifecycle + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + grids = self.grids.to_dict() + + exports = self.exports.to_dict() + + inventories = self.inventories.to_dict() + + features = self.features.to_dict() + + pointclouds = self.pointclouds.to_dict() + + domains = self.domains.to_dict() + + applications = self.applications.to_dict() + + api_keys = self.api_keys.to_dict() + + lifecycle = self.lifecycle.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "grids": grids, + "exports": exports, + "inventories": inventories, + "features": features, + "pointclouds": pointclouds, + "domains": domains, + "applications": applications, + "api_keys": api_keys, + "lifecycle": lifecycle, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.count_usage import CountUsage + from ..models.job_resource_usage import JobResourceUsage + from ..models.usage_lifecycle import UsageLifecycle + + d = dict(src_dict) + grids = JobResourceUsage.from_dict(d.pop("grids")) + + exports = JobResourceUsage.from_dict(d.pop("exports")) + + inventories = JobResourceUsage.from_dict(d.pop("inventories")) + + features = JobResourceUsage.from_dict(d.pop("features")) + + pointclouds = JobResourceUsage.from_dict(d.pop("pointclouds")) + + domains = CountUsage.from_dict(d.pop("domains")) + + applications = CountUsage.from_dict(d.pop("applications")) + + api_keys = CountUsage.from_dict(d.pop("api_keys")) + + lifecycle = UsageLifecycle.from_dict(d.pop("lifecycle")) + + usage = cls( + grids=grids, + exports=exports, + inventories=inventories, + features=features, + pointclouds=pointclouds, + domains=domains, + applications=applications, + api_keys=api_keys, + lifecycle=lifecycle, + ) + + usage.additional_properties = d + return usage + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/usage_count.py b/fastfuels_sdk/v2/client_library/models/usage_count.py new file mode 100644 index 0000000..f36336a --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/usage_count.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UsageCount") + + +@_attrs_define +class UsageCount: + """A count-based usage/limit pair (resources or concurrent jobs). + + Attributes: + usage (int): Current count. + limit (int): The limit this count is measured against. + """ + + usage: int + limit: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + usage = self.usage + + limit = self.limit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "usage": usage, + "limit": limit, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + usage = d.pop("usage") + + limit = d.pop("limit") + + usage_count = cls( + usage=usage, + limit=limit, + ) + + usage_count.additional_properties = d + return usage_count + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/usage_lifecycle.py b/fastfuels_sdk/v2/client_library/models/usage_lifecycle.py new file mode 100644 index 0000000..10c08ee --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/usage_lifecycle.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UsageLifecycle") + + +@_attrs_define +class UsageLifecycle: + """Retention policy in effect for the owner's resources. + + Attributes: + resource_ttl_days (int | None): Days a resource is retained after last modification; null never expires. + failed_resource_ttl_days (int | None): Shorter retention for failed resources; null never expires. + next_expiry_on (datetime.datetime | None | Unset): When the owner's next resource is scheduled to expire. + Populated once retention enforcement ships. + """ + + resource_ttl_days: int | None + failed_resource_ttl_days: int | None + next_expiry_on: datetime.datetime | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + resource_ttl_days: int | None + resource_ttl_days = self.resource_ttl_days + + failed_resource_ttl_days: int | None + failed_resource_ttl_days = self.failed_resource_ttl_days + + next_expiry_on: None | str | Unset + if isinstance(self.next_expiry_on, Unset): + next_expiry_on = UNSET + elif isinstance(self.next_expiry_on, datetime.datetime): + next_expiry_on = self.next_expiry_on.isoformat() + else: + next_expiry_on = self.next_expiry_on + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "resource_ttl_days": resource_ttl_days, + "failed_resource_ttl_days": failed_resource_ttl_days, + } + ) + if next_expiry_on is not UNSET: + field_dict["next_expiry_on"] = next_expiry_on + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + + def _parse_resource_ttl_days(data: object) -> int | None: + if data is None: + return data + return cast(int | None, data) + + resource_ttl_days = _parse_resource_ttl_days(d.pop("resource_ttl_days")) + + def _parse_failed_resource_ttl_days(data: object) -> int | None: + if data is None: + return data + return cast(int | None, data) + + failed_resource_ttl_days = _parse_failed_resource_ttl_days( + d.pop("failed_resource_ttl_days") + ) + + def _parse_next_expiry_on(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + next_expiry_on_type_0 = datetime.datetime.fromisoformat(data) + + return next_expiry_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + next_expiry_on = _parse_next_expiry_on(d.pop("next_expiry_on", UNSET)) + + usage_lifecycle = cls( + resource_ttl_days=resource_ttl_days, + failed_resource_ttl_days=failed_resource_ttl_days, + next_expiry_on=next_expiry_on, + ) + + usage_lifecycle.additional_properties = d + return usage_lifecycle + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/usage_storage.py b/fastfuels_sdk/v2/client_library/models/usage_storage.py new file mode 100644 index 0000000..b7bbc79 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/usage_storage.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UsageStorage") + + +@_attrs_define +class UsageStorage: + """A storage usage/limit pair, in bytes. + + Attributes: + usage_bytes (int): Summed GCS artifact bytes in use. + limit_bytes (int): The storage limit, in bytes. + """ + + usage_bytes: int + limit_bytes: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + usage_bytes = self.usage_bytes + + limit_bytes = self.limit_bytes + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "usage_bytes": usage_bytes, + "limit_bytes": limit_bytes, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + usage_bytes = d.pop("usage_bytes") + + limit_bytes = d.pop("limit_bytes") + + usage_storage = cls( + usage_bytes=usage_bytes, + limit_bytes=limit_bytes, + ) + + usage_storage.additional_properties = d + return usage_storage + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/user_me_response.py b/fastfuels_sdk/v2/client_library/models/user_me_response.py new file mode 100644 index 0000000..e879167 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/user_me_response.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.user_me_response_kind import UserMeResponseKind + +if TYPE_CHECKING: + from ..models.quotas import Quotas + + +T = TypeVar("T", bound="UserMeResponse") + + +@_attrs_define +class UserMeResponse: + """The authenticated owner's identity and resolved quota configuration. + + Attributes: + id (str): The authenticated owner's unique ID. + kind (UserMeResponseKind): Whether the credential authenticated a user or an application. + tier (str): The quota tier in effect for this owner. + quotas (Quotas): Usage limits for an owner. Field defaults are the standard tier. + """ + + id: str + kind: UserMeResponseKind + tier: str + quotas: Quotas + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + kind = self.kind.value + + tier = self.tier + + quotas = self.quotas.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "kind": kind, + "tier": tier, + "quotas": quotas, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.quotas import Quotas + + d = dict(src_dict) + id = d.pop("id") + + kind = UserMeResponseKind(d.pop("kind")) + + tier = d.pop("tier") + + quotas = Quotas.from_dict(d.pop("quotas")) + + user_me_response = cls( + id=id, + kind=kind, + tier=tier, + quotas=quotas, + ) + + user_me_response.additional_properties = d + return user_me_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/models/user_me_response_kind.py b/fastfuels_sdk/v2/client_library/models/user_me_response_kind.py new file mode 100644 index 0000000..a2824d3 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/user_me_response_kind.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class UserMeResponseKind(str, Enum): + APPLICATION = "application" + USER = "user" + + def __str__(self) -> str: + return str(self.value) diff --git a/fastfuels_sdk/v2/client_library/models/validation_error.py b/fastfuels_sdk/v2/client_library/models/validation_error.py new file mode 100644 index 0000000..7e62fb2 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/models/validation_error.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ValidationError") + + +@_attrs_define +class ValidationError: + """ + Attributes: + loc (list[int | str]): + msg (str): + type_ (str): + """ + + loc: list[int | str] + msg: str + type_: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + loc = [] + for loc_item_data in self.loc: + loc_item: int | str + loc_item = loc_item_data + loc.append(loc_item) + + msg = self.msg + + type_ = self.type_ + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "loc": loc, + "msg": msg, + "type": type_, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + loc = [] + _loc = d.pop("loc") + for loc_item_data in _loc: + + def _parse_loc_item(data: object) -> int | str: + return cast(int | str, data) + + loc_item = _parse_loc_item(loc_item_data) + + loc.append(loc_item) + + msg = d.pop("msg") + + type_ = d.pop("type") + + validation_error = cls( + loc=loc, + msg=msg, + type_=type_, + ) + + validation_error.additional_properties = d + return validation_error + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fastfuels_sdk/v2/client_library/types.py b/fastfuels_sdk/v2/client_library/types.py new file mode 100644 index 0000000..b64af09 --- /dev/null +++ b/fastfuels_sdk/v2/client_library/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/fastfuels_sdk/v2/compose.py b/fastfuels_sdk/v2/compose.py new file mode 100644 index 0000000..8973c9a --- /dev/null +++ b/fastfuels_sdk/v2/compose.py @@ -0,0 +1,326 @@ +"""Builders for grid-compose operations.""" + +from collections.abc import Iterable + +from fastfuels_sdk.v2.client_library.models import ( + ComposeAttributeCondition, + ComposeComparisonOperator, + ComposeCompute, + ComposeLiteral, + ComposeOperator, + ComposeSelect, + GridFeatureSpatialCondition, + GridGeometrySpatialCondition, + InlineCompute, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +__all__ = ["select", "compute", "condition", "literal", "inline_compute"] + +_VARIADIC_OPERATORS = { + ComposeOperator.ADD, + ComposeOperator.AVERAGE, + ComposeOperator.MAX, + ComposeOperator.MIN, + ComposeOperator.MULTIPLY, +} +_ORDERING_OPERATORS = { + ComposeComparisonOperator.GE, + ComposeComparisonOperator.GT, + ComposeComparisonOperator.LE, + ComposeComparisonOperator.LT, +} +_CONDITION_TYPES = ( + ComposeAttributeCondition, + GridFeatureSpatialCondition, + GridGeometrySpatialCondition, +) +_ELSE_TYPES = (ComposeLiteral, InlineCompute, int, float, str) + + +def select( + output: str, + from_: str, + *, + conditions: Iterable | None = None, + else_=None, + name: str | None = None, + description: str | None = None, +) -> ComposeSelect: + """Build an operation that copies an input band into the output grid. + + Parameters + ---------- + output : str + Key for the output band. + from_ : str + Alias-qualified source band, such as ``"base.fbfm"``. + conditions : iterable, optional + Attribute or spatial conditions, all of which must match. Attribute + conditions can be built with :func:`condition`. + else_ : optional + Fallback band reference, number, typed :func:`literal`, fuel-model + label, or :func:`inline_compute`. Required when conditions are given. + name, description : str, optional + Display metadata for the output band. + + Returns + ------- + ComposeSelect + A select operation for + :func:`fastfuels_sdk.v2.grids.create_grid_from_compose`. + """ + _require_text(output, "output") + _require_band_reference(from_, "from_") + condition_list = _condition_list(conditions) + _validate_conditional_fallback(condition_list, else_) + return ComposeSelect( + output=output, + from_=from_, + conditions=condition_list, + else_=_else_value(else_), + name=_optional(name), + description=_optional(description), + ) + + +def compute( + output: str, + operator, + operands: Iterable, + *, + conditions: Iterable | None = None, + else_=None, + unit: str | None = None, + name: str | None = None, + description: str | None = None, +) -> ComposeCompute: + """Build an operation that computes an output band. + + Parameters + ---------- + output : str + Key for the output band. + operator : str or ComposeOperator + ``"add"``, ``"subtract"``, ``"multiply"``, ``"divide"``, + ``"min"``, ``"max"``, or ``"average"``. + operands : iterable + Alias-qualified band references, bare numbers, or typed literals. + At least one operand must be a band reference. + conditions : iterable, optional + Attribute or spatial conditions, all of which must match. + else_ : optional + Fallback value. Required when conditions are given. + unit : str, optional + Canonical unit in which to express the computed output. + name, description : str, optional + Display metadata for the output band. + + Returns + ------- + ComposeCompute + A compute operation for + :func:`fastfuels_sdk.v2.grids.create_grid_from_compose`. + """ + _require_text(output, "output") + operator = _operator(operator) + operand_list = _operands(operator, operands) + condition_list = _condition_list(conditions) + _validate_conditional_fallback(condition_list, else_) + return ComposeCompute( + output=output, + operator=operator, + operands=operand_list, + conditions=condition_list, + else_=_else_value(else_), + unit=_optional(unit), + name=_optional(name), + description=_optional(description), + ) + + +def condition(band: str, operator, value) -> ComposeAttributeCondition: + """Build an attribute condition for a compose operation. + + Parameters + ---------- + band : str + Alias-qualified input band to test. + operator : str or ComposeComparisonOperator + ``"eq"``, ``"ne"``, ``"gt"``, ``"lt"``, ``"ge"``, ``"le"``, + or ``"in"``. + value : int, float, str, or list + Comparison value. ``"in"`` requires a list; ordering comparisons + require a scalar. + + Returns + ------- + ComposeAttributeCondition + A condition for :func:`select` or :func:`compute`. + """ + _require_band_reference(band, "band") + try: + operator = ComposeComparisonOperator(operator) + except (TypeError, ValueError): + choices = [item.value for item in ComposeComparisonOperator] + raise ValueError( + f"Unknown compose comparison operator {operator!r}. Use one of {choices}." + ) from None + + is_list = isinstance(value, list) + values = value if is_list else [value] + if not all(_is_scalar(item) for item in values): + raise TypeError("Compose condition values must be numbers or strings.") + if operator == ComposeComparisonOperator.IN and not is_list: + raise ValueError("The 'in' compose condition requires a list value.") + if operator in _ORDERING_OPERATORS and is_list: + raise ValueError(f"The {operator.value!r} condition requires a scalar value.") + return ComposeAttributeCondition(band=band, operator=operator, value=value) + + +def literal(value, unit: str | None = None) -> ComposeLiteral: + """Build a typed literal for a compose operand or fallback. + + Parameters + ---------- + value : int, float, or str + Literal value. + unit : str, optional + Canonical unit for a numeric literal. String literals are unitless. + + Returns + ------- + ComposeLiteral + A typed compose literal. + """ + if not _is_scalar(value): + raise TypeError("Compose literal values must be numbers or strings.") + if isinstance(value, str) and unit is not None: + raise ValueError("String compose literals cannot carry a unit.") + if unit is not None and not isinstance(unit, str): + raise TypeError("unit must be a string or None.") + return ComposeLiteral(value=value, unit=_optional(unit)) + + +def inline_compute(operator, operands: Iterable) -> InlineCompute: + """Build a computation for the fallback branch of a compose operation. + + Parameters + ---------- + operator : str or ComposeOperator + Arithmetic operator. + operands : iterable + Alias-qualified band references, bare numbers, or typed literals. + + Returns + ------- + InlineCompute + A computed fallback for ``else_=``. + """ + operator = _operator(operator) + return InlineCompute(operator=operator, operands=_operands(operator, operands)) + + +def _optional(value): + return UNSET if value is None else value + + +def _require_text(value, name: str) -> None: + if not isinstance(value, str) or not value: + raise TypeError(f"{name} must be a nonempty string.") + + +def _require_band_reference(value, name: str) -> None: + _require_text(value, name) + alias, separator, band = value.partition(".") + if not separator or not alias or not band: + raise ValueError( + f"{name} must be an alias-qualified band reference such as " + "'base.fuel_load.1hr'." + ) + + +def _operator(value) -> ComposeOperator: + try: + return ComposeOperator(value) + except (TypeError, ValueError): + choices = [item.value for item in ComposeOperator] + raise ValueError( + f"Unknown compose operator {value!r}. Use one of {choices}." + ) from None + + +def _operands(operator: ComposeOperator, values: Iterable) -> list: + if isinstance(values, (str, bytes)): + raise TypeError("operands must be an iterable of compose operands.") + try: + operands = list(values) + except TypeError: + raise TypeError("operands must be an iterable of compose operands.") from None + + expected = "at least two" if operator in _VARIADIC_OPERATORS else "exactly two" + valid_arity = ( + len(operands) >= 2 if operator in _VARIADIC_OPERATORS else len(operands) == 2 + ) + if not valid_arity: + raise ValueError( + f"The {operator.value!r} operator requires {expected} operands." + ) + for operand in operands: + if isinstance(operand, str): + _require_band_reference(operand, "operand") + elif isinstance(operand, ComposeLiteral): + if isinstance(operand.value, str): + raise ValueError("String literals are not valid compute operands.") + elif not _is_number(operand): + raise TypeError( + "Compose operands must be band references, numbers, or typed literals." + ) + if not any(isinstance(operand, str) for operand in operands): + raise ValueError("A compute operation must include at least one band operand.") + return operands + + +def _condition_list(values: Iterable | None): + if values is None: + return UNSET + if isinstance(values, _CONDITION_TYPES): + raise TypeError("conditions must be an iterable of compose conditions.") + try: + conditions = list(values) + except TypeError: + raise TypeError( + "conditions must be an iterable of compose conditions." + ) from None + if not all(isinstance(item, _CONDITION_TYPES) for item in conditions): + raise TypeError( + "conditions must contain compose attribute or grid spatial conditions." + ) + return conditions + + +def _validate_conditional_fallback(conditions, else_) -> None: + has_conditions = conditions is not UNSET and bool(conditions) + if has_conditions and else_ is None: + raise ValueError("else_ is required when compose conditions are provided.") + if not has_conditions and else_ is not None: + raise ValueError("else_ requires at least one compose condition.") + + +def _else_value(value): + if value is None: + return UNSET + if isinstance(value, bool) or not isinstance(value, _ELSE_TYPES): + raise TypeError( + "else_ must be a band reference, number, fuel-model label, typed " + "literal, or inline compute." + ) + return value + + +def _is_number(value) -> bool: + return not isinstance(value, bool) and isinstance(value, (int, float)) + + +def _is_scalar(value) -> bool: + return _is_number(value) or isinstance(value, str) diff --git a/fastfuels_sdk/v2/domains.py b/fastfuels_sdk/v2/domains.py new file mode 100644 index 0000000..c1ec1c4 --- /dev/null +++ b/fastfuels_sdk/v2/domains.py @@ -0,0 +1,707 @@ +""" +fastfuels_sdk/v2/domains.py +""" + +# Core imports +import json +from http import HTTPStatus +from pathlib import Path +from typing import Any, List, Optional, Union + +# Internal imports +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.exceptions import expect +from fastfuels_sdk.v2.client_library.api.domains import ( + create_domain, + delete_domain, + get_domain, + get_domain_lattice, + list_domains as list_domains_endpoint, + preview_domain, + reproject_domain, + update_domain, +) +from fastfuels_sdk.v2.client_library.models import ( + Domain as DomainModel, + DomainLattice, + DomainSortField, + DomainSortOrder, + GeoJsonFeatureCollection, + ListDomainsResponse, + UpdateDomainRequestBody, +) +from fastfuels_sdk.v2.client_library.types import UNSET, Unset + +# External imports +import attrs +import geopandas as gpd + + +def _build_create_request_body( + geojson: dict, + name: str, + description: str, + tags: Optional[List[str]], + pad_to_resolution: Optional[float], +) -> GeoJsonFeatureCollection: + """Build a domain creation request body from GeoJSON input. + + The v2 API accepts FeatureCollection input only; a single Feature is + wrapped in a FeatureCollection here (carrying along a feature-level + ``crs`` member if present) so v1-style Feature input keeps working. + """ + geojson_type = geojson.get("type") + if geojson_type == "Feature": + feature_collection = {"type": "FeatureCollection", "features": [geojson]} + if "crs" in geojson: + feature_collection["crs"] = geojson["crs"] + geojson = feature_collection + elif geojson_type != "FeatureCollection": + raise ValueError( + "GeoJSON type must be 'Feature' or 'FeatureCollection', " + f"got {geojson_type!r}" + ) + + return GeoJsonFeatureCollection.from_dict( + { + **geojson, + "name": name, + "description": description, + "tags": tags, + "pad_to_resolution": pad_to_resolution, + } + ) + + +class Domain(DomainModel): + """Domain resource for the FastFuels v2 API. + + Represents a spatial container that defines geographic boundaries for + fire behavior modeling and analysis. A Domain includes metadata like + name and description along with geometric data defining its spatial + extent. Domains must specify a valid area between 0 and 16 square + kilometers, located within CONUS. + + The Domain handles coordinate system transformations automatically: + + 1. Geographic coordinates (e.g. EPSG:4326) are projected to the + appropriate UTM zone + 2. Projected coordinates are preserved in their original CRS + 3. Geometries are optionally padded to align with a grid resolution + (``pad_to_resolution``) + + Attributes + ---------- + id : str + Unique identifier for the domain. + name : str + Human-readable name for the domain. + description : str + Detailed description of the domain. + type_ : str + Always "FeatureCollection". + features : List[GeoJsonFeature] + One GeoJSON feature named "domain": the projected working extent / + bounding box. + bbox : List[float] + Bounding box of the "domain" feature. + crs : GeoJsonCRS + Coordinate reference system specification (always projected). + tags : List[str], optional + User-defined tags for organization. + pad_to_resolution : float, optional + Resolution in meters the domain bounding box was padded to align + with. + created_on : datetime + When the domain was created. + modified_on : datetime + When the domain was last modified. + + Examples + -------- + Create a domain from a file: + >>> domain = Domain.from_file("area.geojson", name="my domain") + + Get a domain by ID: + >>> domain = Domain.from_id("abc123") + >>> print(domain.name) + 'my domain' + + See Also + -------- + Domain.from_geojson : Create a domain from GeoJSON data. + Domain.from_geodataframe : Create a domain from a GeoPandas GeoDataFrame. + Domain.from_file : Create a domain from a geospatial file. + Domain.preview : Validate and project a domain without persisting it. + Domain.get_lattice : Get the pixel lattice for a domain at a resolution. + Domain.to_geodataframe : Convert a domain to a GeoPandas GeoDataFrame. + list_domains : List available domains. + reproject_geojson : Reproject a GeoJSON FeatureCollection (stateless). + """ + + @classmethod + def _from_model(cls, model: DomainModel) -> "Domain": + """Build a Domain from a generated DomainModel instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + return cls.from_dict(model.to_dict()) + + def _copy_fields_from(self, model: DomainModel) -> "Domain": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(DomainModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + self.additional_properties = dict(model.additional_properties) + return self + + def _require_id(self) -> str: + """Return the domain id, raising if this instance has none.""" + if isinstance(self.id, Unset): + raise ValueError( + "This Domain has no id (it was not created through the API)." + ) + return self.id + + @classmethod + def from_id(cls, domain_id: str) -> "Domain": + """Retrieve an existing Domain resource by its ID. + + Parameters + ---------- + domain_id : str + The unique identifier of the domain to retrieve. + + Returns + ------- + Domain + The requested Domain object. + + Raises + ------ + NotFoundException + If no domain exists with the given ID, or the user does not + have access to it. + + Examples + -------- + >>> domain = Domain.from_id("abc123") + >>> domain.id + 'abc123' + """ + response = get_domain.sync_detailed(client=ensure_client(), domain_id=domain_id) + return cls._from_model(expect(response)) + + @classmethod + def from_geojson( + cls, + geojson: dict, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + pad_to_resolution: Optional[float] = None, + ) -> "Domain": + """Create a new Domain from GeoJSON data. + + Parameters + ---------- + geojson : dict + A GeoJSON FeatureCollection or Feature. (The v2 API accepts + FeatureCollection input only; a single Feature is wrapped + automatically.) The source CRS is read from the ``crs`` member + if present; otherwise EPSG:4326 is assumed. + name : str, optional + Name for the domain. + description : str, optional + Description of the domain. + tags : List[str], optional + Tags for organizing the domain. + pad_to_resolution : float, optional + Pad the domain bounding box outward so its extent is a + multiple of this resolution (meters). + + Returns + ------- + Domain + The created Domain object. + + Raises + ------ + ValueError + If the GeoJSON is not a Feature or FeatureCollection. + UnprocessableEntityException + If the geometry is invalid: zero area, larger than 16 square + kilometers, outside CONUS, or an invalid CRS. + + Examples + -------- + >>> with open("area.geojson") as f: + ... geojson = json.load(f) + >>> domain = Domain.from_geojson(geojson, name="my domain") + """ + request_body = _build_create_request_body( + geojson, name, description, tags, pad_to_resolution + ) + response = create_domain.sync_detailed( + client=ensure_client(), body=request_body + ) + return cls._from_model(expect(response, HTTPStatus.CREATED)) + + @classmethod + def from_geodataframe( + cls, + geodataframe: gpd.GeoDataFrame, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + pad_to_resolution: Optional[float] = None, + ) -> "Domain": + """Create a new Domain from a GeoPandas GeoDataFrame. + + The GeoDataFrame's CRS (when set) is forwarded to the API as the + FeatureCollection's ``crs`` member, so projected inputs (e.g. + EPSG:5070) are interpreted correctly. + + Parameters + ---------- + geodataframe : gpd.GeoDataFrame + GeoDataFrame containing the domain geometry. + name : str, optional + Name for the domain. + description : str, optional + Description of the domain. + tags : List[str], optional + Tags for organizing the domain. + pad_to_resolution : float, optional + Pad the domain bounding box outward so its extent is a + multiple of this resolution (meters). + + Returns + ------- + Domain + The created Domain object. + + Raises + ------ + ValueError + If the GeoDataFrame CRS has no authority code (e.g. a custom + CRS). + UnprocessableEntityException + If the geometry is invalid: zero area, larger than 16 square + kilometers, outside CONUS, or an invalid CRS. + + Examples + -------- + >>> gdf = gpd.read_file("area.shp") + >>> domain = Domain.from_geodataframe(gdf, name="my domain") + """ + geojson = json.loads(geodataframe.to_json()) + if geodataframe.crs is not None: + authority = geodataframe.crs.to_authority() + if authority is None: + raise ValueError( + "GeoDataFrame CRS has no authority code (e.g. a custom " + "CRS). Reproject to a known CRS such as EPSG:4326 with " + "geodataframe.to_crs(...) before creating a domain." + ) + geojson["crs"] = { + "type": "name", + "properties": {"name": ":".join(authority)}, + } + return cls.from_geojson( + geojson=geojson, + name=name, + description=description, + tags=tags, + pad_to_resolution=pad_to_resolution, + ) + + @classmethod + def from_file( + cls, + path: Union[str, Path], + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + pad_to_resolution: Optional[float] = None, + ) -> "Domain": + """Create a new Domain from a geospatial file. + + Reads any format geopandas supports (GeoJSON, Shapefile, KML, + GeoPackage, ...) and creates a domain from its geometry. + + Parameters + ---------- + path : str or Path + Path to the geospatial file. + name : str, optional + Name for the domain. + description : str, optional + Description of the domain. + tags : List[str], optional + Tags for organizing the domain. + pad_to_resolution : float, optional + Pad the domain bounding box outward so its extent is a + multiple of this resolution (meters). + + Returns + ------- + Domain + The created Domain object. + + Examples + -------- + >>> domain = Domain.from_file("area.geojson", name="my domain") + """ + return cls.from_geodataframe( + gpd.read_file(path), + name=name, + description=description, + tags=tags, + pad_to_resolution=pad_to_resolution, + ) + + @classmethod + def preview( + cls, + geojson: dict, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + pad_to_resolution: Optional[float] = None, + ) -> "Domain": + """Preview a domain without persisting it. + + Runs the same validation and projection pipeline as domain + creation but writes nothing: use this to inspect the projected, + padded bounding box before committing to a create. Takes the same + arguments as :meth:`from_geojson`. + + Parameters + ---------- + geojson : dict + A GeoJSON FeatureCollection or Feature (a single Feature is + wrapped automatically). + name : str, optional + Name for the domain. + description : str, optional + Description of the domain. + tags : List[str], optional + Tags for organizing the domain. + pad_to_resolution : float, optional + Pad the domain bounding box outward so its extent is a + multiple of this resolution (meters). + + Returns + ------- + Domain + The previewed Domain object. Its ``id`` is always + ``"preview"`` — not a real domain identifier, so API-backed + instance methods (``refresh``, ``update``, ``delete``, + ``get_lattice``) will not work on it. + + Raises + ------ + UnprocessableEntityException + Same validation errors as :meth:`from_geojson`. + + Examples + -------- + >>> previewed = Domain.preview(geojson, pad_to_resolution=2.0) + >>> previewed.id + 'preview' + """ + request_body = _build_create_request_body( + geojson, name, description, tags, pad_to_resolution + ) + response = preview_domain.sync_detailed( + client=ensure_client(), body=request_body + ) + return cls._from_model(expect(response)) + + def refresh(self) -> "Domain": + """Update this Domain in place with the latest data from the API. + + Returns + ------- + Domain + ``self``, updated with the latest data (so calls chain). To fetch + a fresh, separate copy instead, use :meth:`Domain.from_id`. + + Raises + ------ + NotFoundException + If the domain no longer exists. + + Examples + -------- + >>> domain = Domain.from_id("abc123") + >>> domain.refresh() # refresh in place + """ + domain_id = self._require_id() + response = get_domain.sync_detailed(client=ensure_client(), domain_id=domain_id) + return self._copy_fields_from(expect(response)) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Domain": + """Update the domain's mutable properties (name, description, tags) in place. + + Only provided fields are sent in the PATCH body (the generated + UNSET sentinel keeps omitted fields out of the request entirely). + If no fields are provided, no API call is made. + + Parameters + ---------- + name : str, optional + New name for the domain. + description : str, optional + New description for the domain. + tags : List[str], optional + New tags for the domain (replaces existing tags). + + Returns + ------- + Domain + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the domain no longer exists. + + Examples + -------- + >>> domain.update(name="new name", tags=["test"]) + """ + if name is None and description is None and tags is None: + return self + + request_body = UpdateDomainRequestBody( + name=name if name is not None else UNSET, + description=description if description is not None else UNSET, + tags=tags if tags is not None else UNSET, + ) + domain_id = self._require_id() + response = update_domain.sync_detailed( + client=ensure_client(), domain_id=domain_id, body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self, force: bool = False) -> None: + """Delete this domain. + + Parameters + ---------- + force : bool, optional + If True, cascade-delete every child resource (features, grids, + inventories, exports) in the domain. If False (default), the API + rejects the request with 412 when the domain still has child + resources. + + Raises + ------ + NotFoundException + If the domain no longer exists. + ApiException + With ``status_code`` 412 if the domain has child resources and + ``force`` is False. + + Examples + -------- + >>> domain = Domain.from_id("abc123") + >>> domain.delete(force=True) # also deletes its grids and features + """ + domain_id = self._require_id() + response = delete_domain.sync_detailed( + client=ensure_client(), domain_id=domain_id, force=force + ) + expect(response, HTTPStatus.NO_CONTENT) + + def get_lattice( + self, resolution: float, num_buffer_cells: int = 0 + ) -> DomainLattice: + """Get the pixel lattice for this domain at a given resolution. + + Returns the pixel lattice (affine transform + raster shape) for + the domain at the requested resolution. Use this to align a + GeoTIFF before uploading it as a custom grid. + + Parameters + ---------- + resolution : float + Pixel size in meters (domain CRS units, always projected). + num_buffer_cells : int, optional + Expand the lattice by N cells on each side (default 0). + Mirrors the buffer semantics of the grid creation endpoints. + + Returns + ------- + DomainLattice + With attributes ``crs`` (authority string), ``resolution``, + ``num_buffer_cells``, ``transform`` (affine coefficients + ``[a, b, c, d, e, f]``, rasterio convention), and ``shape`` + (``[height, width]`` in pixels). + + Raises + ------ + NotFoundException + If the domain no longer exists. + UnprocessableEntityException + If ``resolution`` is non-positive or ``num_buffer_cells`` is + negative. + + Examples + -------- + >>> lattice = domain.get_lattice(resolution=2.0) + >>> lattice.shape + [400, 400] + """ + domain_id = self._require_id() + response = get_domain_lattice.sync_detailed( + domain_id=domain_id, + client=ensure_client(), + resolution=resolution, + num_buffer_cells=num_buffer_cells, + ) + return expect(response) + + def to_json(self) -> str: + """Serialize the complete Domain object to a JSON string. + + Returns + ------- + str + The Domain as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + def to_geodataframe(self) -> gpd.GeoDataFrame: + """Convert the Domain to a GeoPandas GeoDataFrame. + + The v2 API returns one named feature, "domain": the projected working + extent / bounding box. The GeoDataFrame therefore has one row. + + Returns + ------- + gpd.GeoDataFrame + GeoDataFrame in the domain CRS with the domain features as + rows and domain metadata as columns. + + Examples + -------- + >>> gdf = domain.to_geodataframe() + >>> gdf.loc[0, "name"] + 'domain' + """ + feature_collection: dict[str, Any] = { + "type": "FeatureCollection", + "features": ( + [feature.to_dict() for feature in self.features] + if self.features + else [] + ), + } + if self.crs and not isinstance(self.crs, Unset): + feature_collection["crs"] = self.crs.to_dict() + + gdf = gpd.read_file(json.dumps(feature_collection)) + + # Add domain metadata as columns (UNSET fields become None) + gdf["domain_id"] = None if isinstance(self.id, Unset) else self.id + gdf["domain_name"] = None if isinstance(self.name, Unset) else self.name + gdf["domain_description"] = ( + None if isinstance(self.description, Unset) else self.description + ) + if self.pad_to_resolution and not isinstance(self.pad_to_resolution, Unset): + gdf["pad_to_resolution"] = self.pad_to_resolution + if self.tags and not isinstance(self.tags, Unset): + gdf["tags"] = ", ".join(self.tags) + + return gdf + + +def list_domains( + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, +) -> List[Domain]: + """List domains belonging to the authenticated user (single page). + + Parameters + ---------- + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of domains per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + + Returns + ------- + List[Domain] + The requested page of Domain objects. + + Examples + -------- + >>> domains = list_domains(sort_by="created_on", sort_order="descending") + """ + response = list_domains_endpoint.sync_detailed( + client=ensure_client(), + page=page, + size=size, + sort_by=DomainSortField(sort_by) if sort_by else UNSET, + sort_order=DomainSortOrder(sort_order) if sort_order else UNSET, + ) + list_response: ListDomainsResponse = expect(response) + return [Domain._from_model(d) for d in list_response.domains] + + +def reproject_geojson(geojson: dict, target_epsg: int) -> dict: + """Reproject a GeoJSON FeatureCollection to a target CRS. + + Stateless utility — no resource is created; the reprojected + FeatureCollection is returned immediately. + + Parameters + ---------- + geojson : dict + A GeoJSON FeatureCollection or Feature (a single Feature is + wrapped automatically). The source CRS is read from the ``crs`` + member if present; otherwise EPSG:4326 is assumed. + target_epsg : int + EPSG code of the target CRS (e.g. 4326 for WGS84, 32611 for UTM + zone 11N). + + Returns + ------- + dict + The reprojected FeatureCollection, with original feature + properties preserved and ``crs`` set to the target EPSG code. + + Raises + ------ + UnprocessableEntityException + If the source CRS is invalid, the target EPSG is invalid, or a + geometry cannot be reprojected. + + Examples + -------- + >>> projected = reproject_geojson(geojson, target_epsg=5070) + >>> projected["crs"]["properties"]["name"] + 'EPSG:5070' + """ + request_body = _build_create_request_body( + geojson, name="", description="", tags=None, pad_to_resolution=None + ) + response = reproject_domain.sync_detailed( + client=ensure_client(), body=request_body, target_epsg=target_epsg + ) + return expect(response).to_dict() diff --git a/fastfuels_sdk/v2/exceptions.py b/fastfuels_sdk/v2/exceptions.py new file mode 100644 index 0000000..6084c83 --- /dev/null +++ b/fastfuels_sdk/v2/exceptions.py @@ -0,0 +1,282 @@ +""" +fastfuels_sdk/v2/exceptions.py + +Typed exceptions for the FastFuels v2 SDK. + +Every API error surfaces as a subclass of :class:`ApiException` carrying +the HTTP status code and the error detail reported by the API. +""" + +import json +from collections.abc import Mapping +from http import HTTPStatus +from typing import Any, NoReturn + +from fastfuels_sdk.v2.client_library.models import ( + HTTPValidationError, + QuotaExceededDetail, +) +from fastfuels_sdk.v2.client_library.types import UNSET, Response + +# Implementation note: the generated client (openapi-python-client) does +# not raise per-status exceptions — documented error responses are +# *returned* as parsed models and undocumented statuses are only surfaced +# through the raw ``Response`` object. The wrapper modules therefore call +# the generated ``sync_detailed()`` endpoints (the shared client is created +# with ``raise_on_unexpected_status=False``) and funnel every ``Response`` +# through ``expect()``, the single choke point where errors become +# exceptions. + +# The v2 API raises some 422s with a plain-string ``detail`` (e.g. invalid +# EPSG codes), while the OpenAPI spec types ``detail`` as a list of +# validation errors. The generated parser crashes on the string form +# *inside* sync_detailed(), before the wrapper can translate the response, +# so wrap it to tolerate undocumented body shapes; the actual detail is +# re-read from ``response.content`` in ``raise_for_response()``. +_generated_from_dict = HTTPValidationError.from_dict.__func__ +_generated_quota_from_dict = QuotaExceededDetail.from_dict.__func__ + + +def _tolerant_from_dict(cls, src_dict): + try: + return _generated_from_dict(cls, src_dict) + except (TypeError, ValueError): + model = cls() + if isinstance(src_dict, Mapping): + model.additional_properties = dict(src_dict) + return model + + +HTTPValidationError.from_dict = classmethod(_tolerant_from_dict) + + +def _quota_from_dict(cls, src_dict): + """Unwrap FastAPI's ``detail`` envelope before generated parsing.""" + if isinstance(src_dict, Mapping) and isinstance(src_dict.get("detail"), Mapping): + src_dict = src_dict["detail"] + return _generated_quota_from_dict(cls, src_dict) + + +QuotaExceededDetail.from_dict = classmethod(_quota_from_dict) + + +class ApiException(Exception): + """Base exception for FastFuels v2 API errors. + + Attributes + ---------- + status_code : int + The HTTP status code returned by the API. + detail : Any + The error detail extracted from the response body (FastAPI's + ``detail`` field when present, otherwise the raw response text). + """ + + def __init__(self, status_code: int, detail: Any = None): + self.status_code = status_code + self.detail = detail + super().__init__(f"({status_code}) {detail}") + + +class BadRequestException(ApiException): + """Raised when the API returns 400 Bad Request.""" + + +class UnauthorizedException(ApiException): + """Raised when the API returns 401 Unauthorized (invalid API key).""" + + +class ForbiddenException(ApiException): + """Raised when the API returns 403 Forbidden.""" + + +class NotFoundException(ApiException): + """Raised when the API returns 404 Not Found. + + The v2 API returns 404 both for missing resources and for ownership + mismatches, to avoid leaking information about resource existence. + """ + + +class UnprocessableEntityException(ApiException): + """Raised when the API returns 422 Unprocessable Entity. + + The ``detail`` attribute carries FastAPI's validation error detail: + either a human-readable message or a list of per-field errors. + """ + + +class QuotaExceededException(ApiException): + """Raised when the API returns 429 Too Many Requests. + + Attributes + ---------- + quota : str or None + The quota field that was exceeded. + current : int or None + The owner's current usage for the quota. + limit : int or None + The quota limit that was reached. + window_reset_on : datetime.datetime or None + When a windowed quota resets, if applicable. + message : str or None + A human-readable explanation and suggested next steps. + reason : str or None + The machine-readable error reason. + retry_after : int or None + Seconds the API recommends waiting before retrying. Present only for + active-job quota rejections. + """ + + def __init__( + self, + status_code: int, + detail: Any = None, + retry_after: int | None = None, + ): + self.quota = None + self.current = None + self.limit = None + self.window_reset_on = None + self.message = None + self.reason = None + self.retry_after = retry_after + + if isinstance(detail, QuotaExceededDetail): + self.quota = detail.quota + self.current = detail.current + self.limit = detail.limit + if detail.window_reset_on is not UNSET: + self.window_reset_on = detail.window_reset_on + self.message = detail.message + if detail.reason is not UNSET: + self.reason = detail.reason + + self.status_code = status_code + self.detail = detail + Exception.__init__(self, f"({status_code}) {self.message or detail}") + + +class ServiceException(ApiException): + """Raised when the API returns a 5xx server error.""" + + +_STATUS_TO_EXCEPTION = { + HTTPStatus.BAD_REQUEST: BadRequestException, + HTTPStatus.UNAUTHORIZED: UnauthorizedException, + HTTPStatus.FORBIDDEN: ForbiddenException, + HTTPStatus.NOT_FOUND: NotFoundException, + HTTPStatus.TOO_MANY_REQUESTS: QuotaExceededException, + HTTPStatus.UNPROCESSABLE_ENTITY: UnprocessableEntityException, +} + + +def _extract_detail(response: Response) -> Any: + """Pull FastAPI's ``detail`` field out of an error response body.""" + try: + return json.loads(response.content)["detail"] + except (json.JSONDecodeError, KeyError, UnicodeDecodeError): + return ( + response.content.decode(errors="ignore") + or HTTPStatus(response.status_code).phrase + ) + + +def _extract_quota_detail(response: Response) -> Any: + """Return a typed quota detail, falling back to the generic detail.""" + if isinstance(response.parsed, QuotaExceededDetail): + return response.parsed + + try: + payload = json.loads(response.content) + if isinstance(payload, Mapping) and isinstance(payload.get("detail"), Mapping): + payload = payload["detail"] + if isinstance(payload, Mapping): + return QuotaExceededDetail.from_dict(payload) + except (json.JSONDecodeError, KeyError, TypeError, ValueError): + pass + + return _extract_detail(response) + + +def _extract_retry_after(response: Response) -> int | None: + """Return the delta-seconds value from ``Retry-After``, when present.""" + value = response.headers.get("Retry-After") + try: + return int(value) if value is not None else None + except ValueError: + return None + + +def raise_for_response(response: Response) -> NoReturn: + """Translate an error ``Response`` into a typed SDK exception. + + Parameters + ---------- + response : Response + A response from a generated ``sync_detailed()`` endpoint call. + + Raises + ------ + BadRequestException + If the response status is 400. + UnauthorizedException + If the response status is 401. + ForbiddenException + If the response status is 403. + NotFoundException + If the response status is 404. + QuotaExceededException + If the response status is 429. + UnprocessableEntityException + If the response status is 422. + ServiceException + If the response status is 5xx. + ApiException + For any other unexpected status. + """ + status_code = int(response.status_code) + exception_class = _STATUS_TO_EXCEPTION.get(response.status_code) + if exception_class is None: + exception_class = ServiceException if status_code >= 500 else ApiException + detail = ( + _extract_quota_detail(response) + if response.status_code == HTTPStatus.TOO_MANY_REQUESTS + else _extract_detail(response) + ) + if exception_class is QuotaExceededException: + raise exception_class( + status_code, + detail, + retry_after=_extract_retry_after(response), + ) + raise exception_class(status_code, detail) + + +def expect(response: Response, expected_status: HTTPStatus = HTTPStatus.OK) -> Any: + """Return a response's parsed body, raising typed exceptions on error. + + Parameters + ---------- + response : Response + A response from a generated ``sync_detailed()`` endpoint call. + expected_status : HTTPStatus, optional + The success status documented for the endpoint (default 200 OK; + pass ``HTTPStatus.NO_CONTENT`` for deletes). + + Returns + ------- + Any + ``response.parsed`` — the generated success model (``None`` for + 204 No Content). + + Raises + ------ + ApiException + A subclass matching the response status (see + :func:`raise_for_response`) when it differs from + ``expected_status``. + """ + if response.status_code == expected_status: + return response.parsed + raise_for_response(response) diff --git a/fastfuels_sdk/v2/exports.py b/fastfuels_sdk/v2/exports.py new file mode 100644 index 0000000..a86d850 --- /dev/null +++ b/fastfuels_sdk/v2/exports.py @@ -0,0 +1,700 @@ +""" +fastfuels_sdk/v2/exports.py +""" + +# Core imports +import json +from http import HTTPStatus +from pathlib import Path +from typing import List, Optional, Tuple, Union +from urllib.parse import urlparse + +# Internal imports +from fastfuels_sdk.v2._jobs import wait as _wait +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.exceptions import expect +from fastfuels_sdk.v2.client_library.api.exports import ( + delete_export, + get_export as get_export_endpoint, + list_exports as list_exports_endpoint, + update_export, +) +from fastfuels_sdk.v2.client_library.api.grids import ( + create_landscape_export as create_landscape_export_endpoint, + create_quicfire_export as create_quicfire_export_endpoint, +) +from fastfuels_sdk.v2.client_library.models import ( + Export as ExportModel, + ExportSortField, + FieldSource, + JobStatus, + LandscapeExportAlignmentDomainTarget, + LandscapeExportAlignmentGridTarget, + LandscapeExportRequest, + LandscapeExportRequestFireBehaviorFuelModel, + LandscapeFieldSource, + ListExportsResponse, + QuicfireExportRequest, + QuicfireExportRequestMoistMerge, + QUICFireExportAlignmentDomainTarget, + QUICFireExportAlignmentGridTarget, + SortOrder, + UpdateExportRequestBody, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +# External imports +import attrs +import requests + +__all__ = [ + "Export", + "create_landscape_export", + "create_quicfire_export", + "list_exports", + "get_export", +] + +_DOWNLOAD_CHUNK_BYTES = 1024 * 1024 + + +def _domain_id(domain) -> str: + """Resolve a Domain object or a domain-id string to the id string.""" + return getattr(domain, "id", domain) + + +def _opt(value): + """Map ``None`` to the generated UNSET sentinel, else pass through.""" + return value if value is not None else UNSET + + +def _field_source(value, role: str) -> FieldSource: + """Build a FieldSource from a ``(grid, band)`` tuple or pass one through.""" + if isinstance(value, FieldSource): + return value + if isinstance(value, (tuple, list)) and len(value) == 2: + grid, band = value + return FieldSource(grid_id=_domain_id(grid), band=band) + raise ValueError( + f"{role} must be a (grid, band) tuple or a FieldSource, got {value!r}" + ) + + +def _landscape_field_source(value, role: str) -> LandscapeFieldSource: + """Build a landscape field source from a ``(grid, band)`` pair.""" + if isinstance(value, LandscapeFieldSource): + return value + if isinstance(value, (tuple, list)) and len(value) == 2: + grid, band = value + return LandscapeFieldSource(grid_id=_domain_id(grid), band=band) + raise ValueError( + f"{role} must be a (grid, band) tuple or a LandscapeFieldSource, " + f"got {value!r}" + ) + + +class Export(ExportModel): + """Export resource for the FastFuels v2 API. + + An export packages a resource's data — a grid, an inventory, or a + multi-grid QUIC-Fire bundle — into a downloadable file. Exports are + asynchronous job resources: creation returns a *pending* record, and + the signed download URL is populated when the job completes. Call + :meth:`wait` and then :meth:`to_file` to download. + + Attributes + ---------- + id : str + Unique identifier for the export. + domain_id : str + Identifier of the domain the exported resource belongs to. + status : JobStatus + Job status: "pending", "running", "completed", or "failed". + source : ExportSource + What was exported and in which format. + name : str + Human-readable name for the export. + description : str + Detailed description of the export. + progress : JobProgress, optional + Progress info while the job is running. + signed_url : str, optional + Download URL; populated when the job completes. + expires_on : datetime, optional + When the signed URL stops working. + error : JobError, optional + Error details if the job failed. + tags : List[str], optional + User-defined tags for organization. + created_on : datetime + When the export was created. + modified_on : datetime + When the export was last modified. + + Examples + -------- + Export a grid and download the result: + >>> export = grid.export(format="geotiff") + >>> export.wait().to_file("elevation.tif") + + Get an export by ID: + >>> export = ff.get_export("abc123") + + See Also + -------- + create_landscape_export : Assemble an 8-band fire-behavior landscape. + create_quicfire_export : Bundle fuel grids into a QUIC-Fire archive. + list_exports : List your exports. + """ + + @classmethod + def _from_model(cls, model: ExportModel) -> "Export": + """Build an Export from a generated Export model instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + return cls.from_dict(model.to_dict()) + + def _copy_fields_from(self, model: ExportModel) -> "Export": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(ExportModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + self.additional_properties = dict(model.additional_properties) + return self + + @classmethod + def from_id(cls, export_id: str) -> "Export": + """Retrieve an existing Export resource by its ID. + + Exports are addressed by their ID alone (no domain in the path). + + Parameters + ---------- + export_id : str + The unique identifier of the export to retrieve. + + Returns + ------- + Export + The requested Export object. + + Raises + ------ + NotFoundException + If no export exists with the given ID, or the user does not + have access to it. + """ + response = get_export_endpoint.sync_detailed(export_id, client=ensure_client()) + return cls._from_model(expect(response)) + + def refresh(self) -> "Export": + """Update this Export in place with the latest data from the API. + + Returns + ------- + Export + ``self``, updated with the latest data (so calls chain). + + Raises + ------ + NotFoundException + If the export no longer exists. + """ + response = get_export_endpoint.sync_detailed(self.id, client=ensure_client()) + return self._copy_fields_from(expect(response)) + + def wait(self, timeout: Optional[float] = None, verbose: bool = False) -> "Export": + """Poll the export job until it reaches a terminal status. + + Parameters + ---------- + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + If True, print the job status at each poll. + + Returns + ------- + Export + ``self``, updated to its terminal state (so calls chain). + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapses before a terminal status. + JobFailedError + If the job finished with status "failed". + """ + return _wait(self, timeout=timeout, verbose=verbose) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Export": + """Update the export's mutable metadata (name, description, tags) in place. + + Only provided fields are sent. If no fields are provided, no API call + is made. + + Parameters + ---------- + name : str, optional + New name for the export. + description : str, optional + New description for the export. + tags : List[str], optional + New tags for the export (replaces existing tags). + + Returns + ------- + Export + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the export no longer exists. + """ + if name is None and description is None and tags is None: + return self + request_body = UpdateExportRequestBody( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = update_export.sync_detailed( + self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self) -> None: + """Delete this export and its packaged file. + + Raises + ------ + NotFoundException + If the export no longer exists. + """ + response = delete_export.sync_detailed(self.id, client=ensure_client()) + expect(response, HTTPStatus.NO_CONTENT) + + def to_file(self, path: Union[str, Path]) -> Path: + """Download the export's packaged file. + + Streams the signed URL to disk. The export must be completed — + call :meth:`wait` first. + + Parameters + ---------- + path : str or Path + Destination file path. If ``path`` is an existing directory, + the file is saved inside it under the export's default + filename. + + Returns + ------- + Path + The path of the written file. + + Raises + ------ + ValueError + If the export is not completed or carries no download URL. + + Examples + -------- + >>> export = grid.export(format="geotiff") + >>> export.wait().to_file("elevation.tif") + """ + if self.status != JobStatus.COMPLETED: + raise ValueError( + f"Cannot download an export with status '{self.status}'. " + "Call .wait() until it completes first." + ) + if not self.signed_url: + raise ValueError( + "Export carries no download URL. The signed URL may have " + "expired — re-create the export." + ) + destination = Path(path) + if destination.is_dir(): + filename = Path(urlparse(self.signed_url).path).name or f"{self.id}.zip" + destination = destination / filename + with requests.get(self.signed_url, stream=True) as response: + response.raise_for_status() + with open(destination, "wb") as file_obj: + for chunk in response.iter_content(chunk_size=_DOWNLOAD_CHUNK_BYTES): + file_obj.write(chunk) + return destination + + def to_json(self) -> str: + """Serialize the complete Export object to a JSON string. + + Returns + ------- + str + The Export as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + +# --------------------------------------------------------------------------- +# Create exports assembled from many resources (module-level functions) +# --------------------------------------------------------------------------- + + +def create_landscape_export( + domain, + *, + fire_behavior_fuel_model: str, + elevation: Tuple, + slope: Tuple, + aspect: Tuple, + fuel_model: Tuple, + canopy_cover: Tuple, + canopy_height: Tuple, + canopy_base_height: Tuple, + canopy_bulk_density: Tuple, + resolution_m: Optional[float] = None, + align_to=None, + expiration_days: int = 7, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Export: + """Assemble terrain, fuel-model, and canopy grids into a landscape export. + + Produces an 8-band LANDFIRE-style GeoTIFF for FlamMap, IFTDSS, and WFDSS. + Every role is a ``(grid, band)`` pair. Role grids must already share the + selected 2D lattice and cover its full extent; the exporter never + resamples or reprojects them. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the source grids belong to. + fire_behavior_fuel_model : {"fbfm40", "fbfm13"} + How codes in ``fuel_model`` should be interpreted. + elevation, slope, aspect : tuple + ``(grid, band)`` roles for terrain elevation (m), slope (degrees), and + aspect (degrees). + fuel_model : tuple + ``(grid, band)`` for categorical FBFM40 or FBFM13 codes. + canopy_cover : tuple + ``(grid, band)`` for canopy cover (%). + canopy_height : tuple + ``(grid, band)`` for canopy height (m). + canopy_base_height : tuple + ``(grid, band)`` for canopy base height (m). + canopy_bulk_density : tuple + ``(grid, band)`` for canopy bulk density (kg/m³). + resolution_m : float, optional + Domain-anchored landscape cell size. Defaults to 30 m. Mutually + exclusive with ``align_to``. + align_to : Grid or str, optional + Match an existing grid's CRS, transform, and shape. Mutually exclusive + with ``resolution_m``. + expiration_days : int, optional + Days until the signed download URL expires (1-7, default 7). + name, description : str, optional + Metadata for the export. + tags : List[str], optional + Tags for the export. + + Returns + ------- + Export + The new pending Export. Call :meth:`Export.wait` and then + :meth:`Export.to_file` to download ``landscape.tif``. + + Raises + ------ + ValueError + If ``align_to`` and ``resolution_m`` are combined, a role is not a + field-source pair, or the fuel-model declaration is invalid. + UnprocessableEntityException + If a source band has the wrong unit or dimensionality, or a role grid + is not aligned with or does not cover the landscape lattice. + + Examples + -------- + >>> export = ff.exports.create_landscape_export( + ... domain, + ... fire_behavior_fuel_model="fbfm40", + ... elevation=(topography, "elevation"), + ... slope=(topography, "slope"), + ... aspect=(topography, "aspect"), + ... fuel_model=(fuel_models, "fbfm"), + ... canopy_cover=(canopy, "cc"), + ... canopy_height=(canopy, "chm"), + ... canopy_base_height=(canopy, "cbh"), + ... canopy_bulk_density=(canopy, "cbd"), + ... ) + >>> export.wait().to_file("landscape.tif") + """ + if align_to is not None and resolution_m is not None: + raise ValueError("Specify either align_to or resolution_m, not both.") + if align_to is not None: + alignment = LandscapeExportAlignmentGridTarget( + target="grid", grid_id=_domain_id(align_to) + ) + elif resolution_m is not None: + alignment = LandscapeExportAlignmentDomainTarget( + target="domain", resolution=resolution_m + ) + else: + alignment = UNSET + + request_body = LandscapeExportRequest( + fire_behavior_fuel_model=LandscapeExportRequestFireBehaviorFuelModel( + fire_behavior_fuel_model + ), + elevation=_landscape_field_source(elevation, "elevation"), + slope=_landscape_field_source(slope, "slope"), + aspect=_landscape_field_source(aspect, "aspect"), + fuel_model=_landscape_field_source(fuel_model, "fuel_model"), + canopy_cover=_landscape_field_source(canopy_cover, "canopy_cover"), + canopy_height=_landscape_field_source(canopy_height, "canopy_height"), + canopy_base_height=_landscape_field_source( + canopy_base_height, "canopy_base_height" + ), + canopy_bulk_density=_landscape_field_source( + canopy_bulk_density, "canopy_bulk_density" + ), + alignment=alignment, + expiration_days=expiration_days, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_landscape_export_endpoint.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Export._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_quicfire_export( + domain, + canopy_bulk_density: Tuple, + canopy_moisture: Tuple, + surface_fuel_load: Tuple, + surface_fuel_depth: Tuple, + surface_moisture: Tuple, + topography: Optional[Tuple] = None, + canopy_savr: Optional[Tuple] = None, + surface_savr: Optional[Tuple] = None, + horizontal_resolution_m: Optional[float] = None, + vertical_resolution_m: Optional[float] = None, + align_to=None, + moist_merge: Optional[str] = None, + expiration_days: int = 7, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Export: + """Bundle fuel and topography grids into a QUIC-Fire export. + + Packages the grids into a QUIC-Fire-loadable zip archive containing + ``treesrhof.dat``, ``treesmoist.dat``, ``treesfueldepth.dat``, + ``metadata.json``, and ``domain.geojson`` — plus ``topo.dat`` when + ``topography`` is given, and ``treesss.dat`` when both SAVR roles are + given. + + Each grid role is a ``(grid, band)`` pair naming the grid (or its id) + and the band to read. Every role grid must be lattice-aligned with the + fire grid and cover its full extent; the exporter crops oversized + grids but never resamples. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the grids belong to. + canopy_bulk_density : tuple + ``(grid, band)`` for 3D canopy bulk density (kg/m³). + canopy_moisture : tuple + ``(grid, band)`` for 3D canopy fuel moisture. + surface_fuel_load : tuple + ``(grid, band)`` for 2D surface fuel load (kg/m²). + surface_fuel_depth : tuple + ``(grid, band)`` for 2D surface fuel depth (m). + surface_moisture : tuple + ``(grid, band)`` for 2D surface fuel moisture. + topography : tuple, optional + ``(grid, band)`` for elevation; produces ``topo.dat``. + canopy_savr, surface_savr : tuple, optional + ``(grid, band)`` for surface-area-to-volume ratio. Give both or + neither; together they produce ``treesss.dat``. + horizontal_resolution_m : float, optional + Fire-grid cell size (x and y) in meters when anchoring to the + domain bounding box (API default 2.0). Mutually exclusive with + ``align_to``. + vertical_resolution_m : float, optional + Fire-grid vertical cell size in meters (API default 1.0). + Mutually exclusive with ``align_to``. + align_to : Grid or str, optional + Define the fire grid by an existing grid's lattice instead. + moist_merge : str, optional + How surface and canopy moisture merge in shared cells: "max" + (API default) or "weighted_avg". + expiration_days : int, optional + Days until the signed download URL expires (max 7, default 7). + name, description : str, optional + Metadata for the export. + tags : List[str], optional + Tags for the export. + + Returns + ------- + Export + The created Export object (job status "pending"). Call + :meth:`Export.wait` and then :meth:`Export.to_file` to download + the archive. + + Raises + ------ + ValueError + If ``align_to`` is combined with a resolution argument. + + Examples + -------- + >>> export = ff.exports.create_quicfire_export( + ... domain, + ... canopy_bulk_density=(voxels, "bulk_density.foliage.live"), + ... canopy_moisture=(canopy_moist, "fuel_moisture"), + ... surface_fuel_load=(surface, "fuel_load.1hr"), + ... surface_fuel_depth=(surface, "fuel_depth"), + ... surface_moisture=(surface_moist, "fuel_moisture"), + ... ) + >>> export.wait().to_file("quicfire.zip") + """ + if align_to is not None: + if horizontal_resolution_m is not None or vertical_resolution_m is not None: + raise ValueError( + "Specify either align_to or the resolution arguments, not both." + ) + alignment = QUICFireExportAlignmentGridTarget( + target="grid", grid_id=_domain_id(align_to) + ) + elif horizontal_resolution_m is not None or vertical_resolution_m is not None: + alignment = QUICFireExportAlignmentDomainTarget( + dx=_opt(horizontal_resolution_m), + dy=_opt(horizontal_resolution_m), + dz=_opt(vertical_resolution_m), + ) + else: + alignment = UNSET + + request_body = QuicfireExportRequest( + canopy_bulk_density=_field_source(canopy_bulk_density, "canopy_bulk_density"), + canopy_moisture=_field_source(canopy_moisture, "canopy_moisture"), + surface_fuel_load=_field_source(surface_fuel_load, "surface_fuel_load"), + surface_fuel_depth=_field_source(surface_fuel_depth, "surface_fuel_depth"), + surface_moisture=_field_source(surface_moisture, "surface_moisture"), + topography=( + _field_source(topography, "topography") if topography is not None else UNSET + ), + canopy_savr=( + _field_source(canopy_savr, "canopy_savr") + if canopy_savr is not None + else UNSET + ), + surface_savr=( + _field_source(surface_savr, "surface_savr") + if surface_savr is not None + else UNSET + ), + alignment=alignment, + moist_merge=( + QuicfireExportRequestMoistMerge(moist_merge) + if moist_merge is not None + else UNSET + ), + expiration_days=expiration_days, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_quicfire_export_endpoint.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Export._from_model(expect(response, HTTPStatus.CREATED)) + + +# --------------------------------------------------------------------------- +# Top-level fetch / list helpers +# --------------------------------------------------------------------------- + + +def list_exports( + domain=None, + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + source: Optional[str] = None, + tag: Optional[str] = None, +) -> List[Export]: + """List your exports (single page), most filters optional. + + Parameters + ---------- + domain : Domain or str, optional + Only return exports of resources in this domain (or its id). If + omitted, exports from all your domains are listed. + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of exports per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + source : str, optional + Only return exports with this source name — the format for + single-resource exports (e.g. "geotiff", "csv"), "landscape" for + landscape GeoTIFFs, or "quicfire" for bundles. + tag : str, optional + Only return exports carrying this tag. + + Returns + ------- + List[Export] + The requested page of Export objects. + """ + response = list_exports_endpoint.sync_detailed( + client=ensure_client(), + page=page, + size=size, + sort_by=ExportSortField(sort_by) if sort_by else UNSET, + sort_order=SortOrder(sort_order) if sort_order else UNSET, + domain_id=_opt(_domain_id(domain) if domain is not None else None), + source_name=_opt(source), + tag=_opt(tag), + ) + list_response: ListExportsResponse = expect(response) + return [Export._from_model(e) for e in list_response.exports] + + +def get_export(export_id: str) -> Export: + """Retrieve a single export by its ID. + + Parameters + ---------- + export_id : str + The unique identifier of the export. + + Returns + ------- + Export + The requested Export object. + + Raises + ------ + NotFoundException + If no export exists with the given ID, or the user does not have + access. + """ + return Export.from_id(export_id) diff --git a/fastfuels_sdk/v2/features.py b/fastfuels_sdk/v2/features.py new file mode 100644 index 0000000..59e268f --- /dev/null +++ b/fastfuels_sdk/v2/features.py @@ -0,0 +1,879 @@ +""" +fastfuels_sdk/v2/features.py +""" + +# Core imports +import json +from http import HTTPStatus +from typing import Any, Dict, List, Optional + +# Internal imports +from fastfuels_sdk.v2._jobs import wait as _wait +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.exceptions import expect +from fastfuels_sdk.v2.client_library.api.features import ( + create_layerset, + create_osm_road_feature, + create_osm_water_feature, + delete_feature, + get_feature as get_feature_endpoint, + get_feature_data_metadata, + get_feature_data_partition, + list_features as list_features_endpoint, + list_features_cross_domain, + update_feature, +) +from fastfuels_sdk.v2.client_library.models import ( + Feature as FeatureModel, + CreateLayersetRequestBody, + CreateOsmRoadFeatureRequest, + CreateOsmWaterFeatureRequest, + FeatureDataMetadata, + FeatureSortField, + FeatureType, + JobStatus, + ListFeaturesResponse, + SortOrder, + UpdateFeatureRequestBody, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +# External imports +import attrs +import geopandas as gpd + + +def _domain_id(domain) -> str: + """Resolve a Domain object or a domain-id string to the id string.""" + return getattr(domain, "id", domain) + + +def _opt(value): + """Map ``None`` to the generated UNSET sentinel, else pass through.""" + return value if value is not None else UNSET + + +def _as_feature_collection(geojson: dict) -> dict: + """Return ``geojson`` as a FeatureCollection dict. + + A single Feature is wrapped in a FeatureCollection (carrying along a + feature-level ``crs`` member if present) so Feature input keeps + working. + """ + geojson_type = geojson.get("type") + if geojson_type == "Feature": + feature_collection = {"type": "FeatureCollection", "features": [geojson]} + if "crs" in geojson: + feature_collection["crs"] = geojson["crs"] + return feature_collection + if geojson_type != "FeatureCollection": + raise ValueError( + "GeoJSON type must be 'Feature' or 'FeatureCollection', " + f"got {geojson_type!r}" + ) + return geojson + + +class Feature(FeatureModel): + """Feature resource for the FastFuels v2 API. + + Represents geographic features within a domain: roads and water + bodies sourced from OpenStreetMap, or user-uploaded layersets of + fuelbed polygons. Features are asynchronous job resources — OSM + features are generated in the background and expose a + ``status``/``progress`` lifecycle, while layerset uploads complete + synchronously. + + The Feature resource itself is geometry-free; the generated geodata + is retrieved through the data methods (:meth:`get_data_metadata`, + :meth:`get_data_partition`, :meth:`get_data`, + :meth:`to_geodataframe`) once the feature is completed. + + Attributes + ---------- + id : str + Unique identifier for the feature. + domain_id : str + Identifier of the domain the feature belongs to. + type_ : FeatureType + Type of geographic feature: "road", "water", or "layerset". + status : JobStatus + Job status: "pending", "running", "completed", or "failed". + source : FeatureSource + Where the feature data comes from (e.g. "osm"). + name : str + Human-readable name for the feature. + description : str + Detailed description of the feature. + progress : JobProgress, optional + Progress info while the job is running. + georeference : FeatureGeoreference, optional + Spatial reference of the generated data; populated when the job + completes. + error : JobError, optional + Error details if the job failed. + tags : List[str], optional + User-defined tags for organization. + created_on : datetime + When the feature was created. + modified_on : datetime + When the feature was last modified. + + Examples + -------- + Create a road feature and wait for it to complete: + >>> import fastfuels_sdk as ff + >>> feature = ff.features.create_road_feature_from_osm(domain) + >>> feature.wait() + >>> roads = feature.to_geodataframe() + + Get a feature by ID: + >>> feature = ff.get_feature(domain, "def456") + + See Also + -------- + create_road_feature_from_osm : Create a road feature from OpenStreetMap. + create_water_feature_from_osm : Create a water feature from OpenStreetMap. + create_layerset_feature_from_geojson : Upload a custom layerset of fuelbed polygons. + Feature.to_geodataframe : Retrieve the feature data as a GeoDataFrame. + Feature.rasterize : Rasterize a layerset feature into a grid. + list_features : List features in a domain or across all domains. + """ + + @classmethod + def _from_model(cls, model: FeatureModel) -> "Feature": + """Build a Feature from a generated FeatureModel instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + return cls.from_dict(model.to_dict()) + + def _copy_fields_from(self, model: FeatureModel) -> "Feature": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(FeatureModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + self.additional_properties = dict(model.additional_properties) + return self + + def _require_completed(self, action: str) -> None: + """Raise if the feature is not completed, before deriving from it.""" + if self.status != JobStatus.COMPLETED: + raise ValueError( + f"Cannot {action} a feature with status '{self.status}'. Call " + ".wait() until it completes first." + ) + + @classmethod + def from_id(cls, domain_id: str, feature_id: str) -> "Feature": + """Retrieve an existing Feature resource by its ID. + + Parameters + ---------- + domain_id : str + The unique identifier of the domain the feature belongs to. + feature_id : str + The unique identifier of the feature to retrieve. + + Returns + ------- + Feature + The requested Feature object. + + Raises + ------ + NotFoundException + If no feature exists with the given IDs, or the user does not + have access to it. + + Examples + -------- + >>> feature = Feature.from_id("abc123", "def456") + >>> feature.id + 'def456' + """ + response = get_feature_endpoint.sync_detailed( + domain_id, feature_id, client=ensure_client() + ) + return cls._from_model(expect(response)) + + def refresh(self) -> "Feature": + """Update this Feature in place with the latest data from the API. + + Returns + ------- + Feature + ``self``, updated with the latest data (so calls chain). + + Raises + ------ + NotFoundException + If the feature no longer exists. + + Examples + -------- + >>> feature.refresh() + """ + response = get_feature_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return self._copy_fields_from(expect(response)) + + def wait(self, timeout: Optional[float] = None, verbose: bool = False) -> "Feature": + """Poll the feature job until it reaches a terminal status. + + Parameters + ---------- + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + If True, print the job status at each poll. + + Returns + ------- + Feature + ``self``, updated to its terminal state (so calls chain). + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapses before a terminal status. + JobFailedError + If the job finished with status "failed". + + Examples + -------- + >>> feature = create_road_feature_from_osm(domain) + >>> feature.wait(verbose=True) + Feature def456: JobStatus.COMPLETED (5s) + """ + return _wait(self, timeout=timeout, verbose=verbose) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Feature": + """Update the feature's mutable metadata (name, description, tags) in place. + + Only provided fields are sent. If no fields are provided, no API call + is made. + + Parameters + ---------- + name : str, optional + New name for the feature. + description : str, optional + New description for the feature. + tags : List[str], optional + New tags for the feature (replaces existing tags). + + Returns + ------- + Feature + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the feature no longer exists. + + Examples + -------- + >>> feature.update(name="new name", tags=["test"]) + """ + if name is None and description is None and tags is None: + return self + + request_body = UpdateFeatureRequestBody( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = update_feature.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self) -> None: + """Delete this feature and its generated data. + + Raises + ------ + NotFoundException + If the feature no longer exists. + + Examples + -------- + >>> feature = Feature.from_id("abc123", "def456") + >>> feature.delete() + """ + response = delete_feature.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + expect(response, HTTPStatus.NO_CONTENT) + + def rasterize( + self, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + overlap_method: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, + ): + """Rasterize this layerset feature into a grid. + + Only valid for layerset features (uploaded fuelbed polygons). + + Parameters + ---------- + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "nearest"). + overlap_method : str, optional + Per-cell reduction when polygons of the same fuel type overlap: + "max", "mean", or "min". + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10). + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The new (pending) rasterized Grid. + """ + # Imported here rather than at module scope so the feature/grid + # modules stay decoupled (grids never imports features). + from fastfuels_sdk.v2.grids import Grid, _build_alignment + from fastfuels_sdk.v2.client_library.api.grids import create_layerset_rasterize + from fastfuels_sdk.v2.client_library.models import ( + CreateLayersetRasterizeRequest, + OverlapMethod, + ) + + self._require_completed("rasterize") + request_body = CreateLayersetRasterizeRequest( + layerset_id=self.id, + alignment=_build_alignment( + output_resolution_m, align_to, align, resampling + ), + overlap_method=( + OverlapMethod(overlap_method) if overlap_method is not None else UNSET + ), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_layerset_rasterize.sync_detailed( + self.domain_id, client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + def get_data_metadata(self) -> FeatureDataMetadata: + """Get the partition layout of the feature's generated data. + + The generated geodata is served in fixed-size partitions. Use + this to discover how many partitions exist before retrieving them + with :meth:`get_data_partition` (or call :meth:`get_data` / + :meth:`to_geodataframe` to retrieve everything at once). + + Returns + ------- + FeatureDataMetadata + With attributes ``total_features`` (count across all + partitions), ``partition_count`` (number of valid partition + indices), and ``partitions`` (per-partition feature counts). + A feature with no data has ``partition_count`` 0. + + Raises + ------ + NotFoundException + If the feature no longer exists. + UnprocessableEntityException + If the feature is not in "completed" status. + + Examples + -------- + >>> metadata = feature.get_data_metadata() + >>> metadata.partition_count + 1 + """ + response = get_feature_data_metadata.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return expect(response) + + def get_data_partition(self, partition_index: int) -> Dict[str, Any]: + """Get one partition of the feature's generated data. + + Parameters + ---------- + partition_index : int + Zero-indexed partition number. Must be less than the + ``partition_count`` reported by :meth:`get_data_metadata`. + + Returns + ------- + dict + A self-contained GeoJSON FeatureCollection with this + partition's features. + + Raises + ------ + NotFoundException + If the feature no longer exists. + UnprocessableEntityException + If ``partition_index`` is past the last partition, or the + feature is not in "completed" status. + ApiException + With ``status_code`` 413 if the serialized partition exceeds + the 30 MB response cap. + + Examples + -------- + >>> partition = feature.get_data_partition(0) + >>> partition["type"] + 'FeatureCollection' + """ + response = get_feature_data_partition.sync_detailed( + self.domain_id, self.id, partition_index, client=ensure_client() + ) + return expect(response) + + def get_data(self) -> Dict[str, Any]: + """Get the feature's complete generated data as GeoJSON. + + Retrieves every partition and concatenates them into a single + FeatureCollection, preserving source order. + + Returns + ------- + dict + A GeoJSON FeatureCollection with all of the feature's data. + Empty (no features) if the source data contained nothing + within the domain extent. + + Raises + ------ + NotFoundException + If the feature no longer exists. + UnprocessableEntityException + If the feature is not in "completed" status. + + Examples + -------- + >>> data = feature.get_data() + >>> len(data["features"]) + 42 + """ + metadata = self.get_data_metadata() + feature_collection: Dict[str, Any] = { + "type": "FeatureCollection", + "features": [], + } + for partition_index in range(metadata.partition_count): + partition = self.get_data_partition(partition_index) + if "crs" in partition and "crs" not in feature_collection: + feature_collection["crs"] = partition["crs"] + feature_collection["features"].extend(partition.get("features", [])) + return feature_collection + + def to_geodataframe(self) -> gpd.GeoDataFrame: + """Retrieve the feature's generated data as a GeoPandas GeoDataFrame. + + Returns + ------- + gpd.GeoDataFrame + GeoDataFrame with one row per generated feature. Empty if the + source data contained nothing within the domain extent. + + Raises + ------ + NotFoundException + If the feature no longer exists. + UnprocessableEntityException + If the feature is not in "completed" status. + + Examples + -------- + >>> feature.wait() + >>> gdf = feature.to_geodataframe() + """ + data = self.get_data() + if not data["features"]: + return gpd.GeoDataFrame() + return gpd.read_file(json.dumps(data)) + + def to_json(self) -> str: + """Serialize the complete Feature object to a JSON string. + + Returns + ------- + str + The Feature as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + +# --------------------------------------------------------------------------- +# Create features (module-level functions) +# --------------------------------------------------------------------------- + + +def create_road_feature_from_osm( + domain, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + extent_buffer_m: float = 0.0, +) -> Feature: + """Create a road feature from OpenStreetMap data. + + Starts a background job that extracts road geometries from + OpenStreetMap within the domain extent. Use :meth:`Feature.wait` to + block until the data is ready. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the feature in. + name : str, optional + Name for the feature. + description : str, optional + Description of the feature. + tags : List[str], optional + Tags for organizing the feature. + extent_buffer_m : float, optional + Distance in meters (0-100, default 0) to buffer the domain extent + outward when querying OpenStreetMap. + + Returns + ------- + Feature + The created Feature object (job status "pending" or "running"). + + Raises + ------ + NotFoundException + If the domain does not exist. + UnprocessableEntityException + If ``extent_buffer_m`` is outside the 0-100 meter range. + + Examples + -------- + >>> feature = create_road_feature_from_osm(domain, name="roads") + >>> feature.type_ + + """ + request_body = CreateOsmRoadFeatureRequest( + name=name, + description=description, + tags=_opt(tags), + extent_buffer_m=extent_buffer_m, + ) + response = create_osm_road_feature.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Feature._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_water_feature_from_osm( + domain, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + extent_buffer_m: float = 0.0, +) -> Feature: + """Create a water feature from OpenStreetMap data. + + Starts a background job that extracts water-body geometries from + OpenStreetMap within the domain extent. Use :meth:`Feature.wait` to + block until the data is ready. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the feature in. + name : str, optional + Name for the feature. + description : str, optional + Description of the feature. + tags : List[str], optional + Tags for organizing the feature. + extent_buffer_m : float, optional + Distance in meters (0-100, default 0) to buffer the domain extent + outward when querying OpenStreetMap. + + Returns + ------- + Feature + The created Feature object (job status "pending" or "running"). + + Raises + ------ + NotFoundException + If the domain does not exist. + UnprocessableEntityException + If ``extent_buffer_m`` is outside the 0-100 meter range. + + Examples + -------- + >>> feature = create_water_feature_from_osm(domain, name="water") + >>> feature.type_ + + """ + request_body = CreateOsmWaterFeatureRequest( + name=name, + description=description, + tags=_opt(tags), + extent_buffer_m=extent_buffer_m, + ) + response = create_osm_water_feature.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Feature._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_layerset_feature_from_geojson( + domain, + geojson: dict, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Feature: + """Upload a custom layerset of fuelbed polygons from GeoJSON. + + Uploads a GeoJSON FeatureCollection where each feature is one fuelbed + polygon. The upload is validated and stored synchronously: the returned + Feature has status "completed" and its data is immediately available + through the data methods. + + Each feature's ``properties`` must carry the fuelbed input columns: + + - ``fuel_type`` (str) + - ``fuel_loading`` (float) + - ``fuel_height`` (float) + - ``percent_cover`` (float) + - ``distribution`` (str): "homogeneous", "random_clusters", or + "uniform_random" + + Optional columns: ``strata_fb``, ``patch_size``, + ``live_fuel_moisture``, ``dead_fuel_moisture``, ``heat_of_combustion``, + ``patch_std_dev``. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the feature in. + geojson : dict + A GeoJSON FeatureCollection or Feature (a single Feature is wrapped + automatically) with Polygon or MultiPolygon geometries. The ``crs`` + member must declare a **projected** CRS (e.g. EPSG:5070); geographic + coordinates are rejected. + name : str, optional + Name for the feature. + description : str, optional + Description of the feature. + tags : List[str], optional + Tags for organizing the feature. + + Returns + ------- + Feature + The created Feature object with status "completed". + + Raises + ------ + ValueError + If the GeoJSON is not a Feature or FeatureCollection. + NotFoundException + If the domain does not exist. + UnprocessableEntityException + If the CRS is missing or geographic, a geometry is not a + Polygon/MultiPolygon, or a feature is missing required fuelbed + properties. + + Examples + -------- + >>> feature = create_layerset_feature_from_geojson(domain, geojson) + >>> feature.status + + """ + feature_collection = _as_feature_collection(geojson) + body = {**feature_collection, "name": name, "description": description} + if tags is not None: + body["tags"] = tags + request_body = CreateLayersetRequestBody.from_dict(body) + response = create_layerset.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Feature._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_layerset_feature_from_geodataframe( + domain, + geodataframe: gpd.GeoDataFrame, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Feature: + """Upload a custom layerset from a GeoPandas GeoDataFrame. + + The GeoDataFrame's CRS is forwarded to the API as the FeatureCollection's + ``crs`` member. The CRS must be **projected** (e.g. EPSG:5070); the + GeoDataFrame is not reprojected — use ``geodataframe.to_crs(...)`` first if + needed. Each row's columns must carry the fuelbed properties described in + :func:`create_layerset_feature_from_geojson`. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the feature in. + geodataframe : gpd.GeoDataFrame + GeoDataFrame with one fuelbed polygon per row. + name : str, optional + Name for the feature. + description : str, optional + Description of the feature. + tags : List[str], optional + Tags for organizing the feature. + + Returns + ------- + Feature + The created Feature object with status "completed". + + Raises + ------ + ValueError + If the GeoDataFrame CRS has no authority code (e.g. a custom CRS). + UnprocessableEntityException + Same validation errors as :func:`create_layerset_feature_from_geojson`. + + Examples + -------- + >>> gdf = gpd.read_file("fuelbeds.shp").to_crs(epsg=5070) + >>> feature = create_layerset_feature_from_geodataframe(domain, gdf) + """ + geojson = json.loads(geodataframe.to_json()) + if geodataframe.crs is not None: + authority = geodataframe.crs.to_authority() + if authority is None: + raise ValueError( + "GeoDataFrame CRS has no authority code (e.g. a custom " + "CRS). Reproject to a known projected CRS with " + "geodataframe.to_crs(...) before creating a layerset." + ) + geojson["crs"] = { + "type": "name", + "properties": {"name": ":".join(authority)}, + } + return create_layerset_feature_from_geojson( + domain, + geojson, + name=name, + description=description, + tags=tags, + ) + + +def get_feature(domain, feature_id: str) -> Feature: + """Retrieve a single feature by its ID. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the feature belongs to. + feature_id : str + The unique identifier of the feature. + + Returns + ------- + Feature + The requested Feature object. + + Raises + ------ + NotFoundException + If no feature exists with the given IDs, or the user does not have + access to it. + """ + return Feature.from_id(_domain_id(domain), feature_id) + + +def list_features( + domain=None, + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + feature_type: Optional[str] = None, + product: Optional[str] = None, + tag: Optional[str] = None, +) -> List[Feature]: + """List features in a domain, or across all domains (single page). + + Parameters + ---------- + domain : Domain or str, optional + The domain (or its id) to list features in. If omitted, features from + all the user's domains are listed. + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of features per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + feature_type : str, optional + Only return features of this type: "road", "water", or "layerset". + product : str, optional + Only return features from this data product (e.g. "osm"). + tag : str, optional + Only return features carrying this tag. + + Returns + ------- + List[Feature] + The requested page of Feature objects. + + Examples + -------- + >>> features = list_features(domain, feature_type="road") + >>> all_features = list_features() # across all domains + """ + kwargs = dict( + client=ensure_client(), + page=page, + size=size, + sort_by=FeatureSortField(sort_by) if sort_by else UNSET, + sort_order=SortOrder(sort_order) if sort_order else UNSET, + type_=FeatureType(feature_type) if feature_type else UNSET, + product=_opt(product), + tag=_opt(tag), + ) + if domain is None: + response = list_features_cross_domain.sync_detailed(**kwargs) + else: + response = list_features_endpoint.sync_detailed(_domain_id(domain), **kwargs) + list_response: ListFeaturesResponse = expect(response) + return [Feature._from_model(f) for f in list_response.features] diff --git a/fastfuels_sdk/v2/generate_client.sh b/fastfuels_sdk/v2/generate_client.sh new file mode 100644 index 0000000..cf8552d --- /dev/null +++ b/fastfuels_sdk/v2/generate_client.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Regenerate the v2 client library (fastfuels_sdk/v2/client_library/) from +# the live API spec. Run from this directory: bash generate_client.sh +# +# Generated code is committed — rerun this when the v2 API spec changes +# and commit the diff so API-surface changes are reviewable in PRs. +# +# Requirements: uv (openapi-python-client is run via uvx, no install +# needed). The generator version is pinned so regen diffs reflect API +# changes only; bump the pin deliberately. +set -euo pipefail + +OPC_VERSION="0.29.0" + +URL="https://api-v2-prod-782971006568.us-west1.run.app" +SPEC=$(mktemp /tmp/v2_openapi.XXXXXX.json) + +curl -fsS "$URL/openapi.json" > "$SPEC" + +uvx "openapi-python-client@${OPC_VERSION}" generate \ + --path "$SPEC" \ + --meta none \ + --output-path client_library \ + --overwrite + +# openapi-python-client embeds no server URL (the spec has no `servers` +# entry, and the generated client takes base_url at construction time), so +# record the deployment URL this client was generated against. api.py +# imports it as the SDK default — this script is the single source of +# truth for the URL. +cat > client_library/base_url.py < str: + """Resolve a Domain object or a domain-id string to the id string.""" + return getattr(domain, "id", domain) + + +def _enum_list(values, enum_cls): + """Coerce a list of strings/enum members to enum members, or UNSET.""" + if values is None: + return UNSET + return [v if isinstance(v, enum_cls) else enum_cls(v) for v in values] + + +def _opt(value): + """Map ``None`` to the generated UNSET sentinel, else pass through.""" + return value if value is not None else UNSET + + +def _build_alignment( + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, +): + """Translate friendly alignment keywords into a grid alignment target. + + Returns a ``GridAlignment{Domain,Grid,Native}Target`` or ``UNSET`` (let the + API choose its default, which anchors to the domain origin at the source's + native cell size). + + - ``output_resolution_m=N`` anchors output cells to the domain origin at N + meters (the default target). + - ``align_to=`` matches an existing grid's lattice exactly. + - ``align="native"`` keeps the source raster's pixel anchor. + - ``resampling=`` selects the resampling method for any of the above. + + ``output_resolution_m``, ``align_to``, and ``align`` are mutually exclusive. + """ + if align is not None and align != "native": + raise ValueError( + f'align must be "native" if given, got {align!r}. Use ' + "output_resolution_m to set a resolution or align_to to match a grid." + ) + if sum(x is not None for x in (output_resolution_m, align_to, align)) > 1: + raise ValueError( + "Specify at most one of output_resolution_m, align_to, or align." + ) + + method = ResamplingMethod(resampling) if resampling is not None else UNSET + + if align_to is not None: + return GridAlignmentGridTarget( + target="grid", grid_id=_domain_id(align_to), method=method + ) + if align is not None: # align == "native" + return GridAlignmentNativeTarget(target="native", method=method) + if output_resolution_m is not None or resampling is not None: + return GridAlignmentDomainTarget( + resolution=_opt(output_resolution_m), method=method + ) + return UNSET + + +def _fill_for(dtype, nodata=None): + """Pick the fill value for cells a chunk does not cover. + + Uses the band's ``nodata`` when defined, else NaN for floating dtypes and + 0 for integers (NaN is not representable in an integer array). + """ + if nodata is not None and nodata is not UNSET: + return nodata + return np.nan if np.issubdtype(dtype, np.floating) else 0 + + +def _decode_grid_chunk(content: bytes, headers): + """Decode one binary grid chunk into ``(offset, block)``. + + The chunk endpoint returns ``application/octet-stream`` and describes the + payload entirely in headers (see ``get_grid_data_binary``): ``X-Data-Shape``, + ``X-Data-Offset``, ``X-Data-Order``, ``X-Data-Format`` and the dtype headers. + ``offset`` is where the block lands in the full grid; ``block`` is an + ``np.ndarray`` of the chunk's own shape. + """ + shape = [int(s) for s in headers["X-Data-Shape"].split(",")] + offset = tuple(int(o) for o in headers["X-Data-Offset"].split(",")) + order = headers.get("X-Data-Order", "C") + + if headers["X-Data-Format"] == "dense": + dtype = np.dtype(headers["X-Data-Dtype"]) + block = np.frombuffer(content, dtype=dtype).reshape(shape, order=order) + return offset, block + + # Sparse: the body is the index array bytes immediately followed by the + # value array bytes; split at NNZ * sizeof(index_dtype). + nnz = int(headers["X-Data-NNZ"]) + index_dtype = np.dtype(headers["X-Data-Index-Dtype"]) + value_dtype = np.dtype(headers["X-Data-Value-Dtype"]) + split = nnz * index_dtype.itemsize + indices = np.frombuffer(content[:split], dtype=index_dtype) + values = np.frombuffer(content[split:], dtype=value_dtype) + + raw_fill = headers.get("X-Data-Fill-Value") + fill = _fill_for(value_dtype, float(raw_fill) if raw_fill else None) + flat = np.full(int(np.prod(shape)), fill, dtype=value_dtype) + flat[indices] = values + return offset, flat.reshape(shape, order=order) + + +class Grid(GridModel): + """Grid resource for the FastFuels v2 API. + + A grid is a raster (2D) or voxel (3D) dataset within a domain: topography, + canopy, surface fuel models, uploaded rasters, or grids derived from other + grids. Grids are asynchronous job resources — creation starts a background + job and returns a *pending* record; call :meth:`wait` to block until it is + ready. + + Attributes + ---------- + id : str + Unique identifier for the grid. + domain_id : str + Identifier of the domain the grid belongs to. + status : JobStatus + Job status: "pending", "running", "completed", or "failed". + source : GridSource + Where the grid data comes from (e.g. "landfire", "3dep"). + bands : List[Band] + The grid's data bands (keys, types, units). + name : str + Human-readable name for the grid. + description : str + Detailed description of the grid. + progress : JobProgress, optional + Progress info while the job is running. + georeference : Georeference or Georeference3D, optional + Spatial reference of the data; populated when the job completes. + error : JobError, optional + Error details if the job failed. + chunks : Chunks, optional + Chunk layout; populated when processing completes. + tags : List[str], optional + User-defined tags for organization. + created_on : datetime + When the grid was created. + modified_on : datetime + When the grid was last modified. + + Examples + -------- + Create a topography grid and wait for it to complete: + >>> import fastfuels_sdk as ff + >>> grid = ff.grids.create_topography_grid_from_3dep(domain, output_resolution_m=10) + >>> grid.wait() + + Get a grid by ID: + >>> grid = ff.get_grid(domain, "def456") + + See Also + -------- + create_topography_grid_from_3dep : Create a topography grid from USGS 3DEP. + create_fuel_model_grid_from_landfire_fbfm40 : Create an FBFM40 fuel model grid. + create_uniform_grid : Create a constant-value grid. + list_grids : List grids in a domain or across all domains. + """ + + @classmethod + def _from_model(cls, model: GridModel) -> "Grid": + """Build a Grid from a generated Grid model instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + return cls.from_dict(model.to_dict()) + + def _copy_fields_from(self, model: GridModel) -> "Grid": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(GridModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + self.additional_properties = dict(model.additional_properties) + return self + + def _require_completed(self, action: str) -> None: + """Raise if the grid is not completed, before deriving from it.""" + if self.status != JobStatus.COMPLETED: + raise ValueError( + f"Cannot {action} a grid with status '{self.status}'. Call " + ".wait() until it completes first." + ) + + def _band(self, band: str): + """Return the :class:`Band` with key ``band``, or raise if absent.""" + for grid_band in self.bands: + if grid_band.key == band: + return grid_band + keys = [b.key for b in self.bands] + raise ValueError(f"Grid has no band {band!r}. Available bands: {keys}.") + + @classmethod + def from_id(cls, domain_id: str, grid_id: str) -> "Grid": + """Retrieve an existing Grid resource by its ID. + + Parameters + ---------- + domain_id : str + The unique identifier of the domain the grid belongs to. + grid_id : str + The unique identifier of the grid to retrieve. + + Returns + ------- + Grid + The requested Grid object. + + Raises + ------ + NotFoundException + If no grid exists with the given IDs, or the user does not have + access to it. + """ + response = get_grid_endpoint.sync_detailed( + domain_id, grid_id, client=ensure_client() + ) + return cls._from_model(expect(response)) + + def refresh(self) -> "Grid": + """Update this Grid in place with the latest data from the API. + + Returns + ------- + Grid + ``self``, updated with the latest data (so calls chain). + + Raises + ------ + NotFoundException + If the grid no longer exists. + """ + response = get_grid_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return self._copy_fields_from(expect(response)) + + def wait(self, timeout: Optional[float] = None, verbose: bool = False) -> "Grid": + """Poll the grid job until it reaches a terminal status. + + Parameters + ---------- + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + If True, print the job status at each poll. + + Returns + ------- + Grid + ``self``, updated to its terminal state (so calls chain). + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapses before a terminal status. + JobFailedError + If the job finished with status "failed". + """ + return _wait(self, timeout=timeout, verbose=verbose) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Grid": + """Update the grid's mutable metadata (name, description, tags) in place. + + Only provided fields are sent. If no fields are provided, no API call + is made. + + Parameters + ---------- + name : str, optional + New name for the grid. + description : str, optional + New description for the grid. + tags : List[str], optional + New tags for the grid (replaces existing tags). + + Returns + ------- + Grid + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the grid no longer exists. + """ + if name is None and description is None and tags is None: + return self + request_body = UpdateGridRequestBody( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = update_grid.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self) -> None: + """Delete this grid and its data. + + Raises + ------ + NotFoundException + If the grid no longer exists. + """ + response = delete_grid.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + expect(response, HTTPStatus.NO_CONTENT) + + def duplicate( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Grid": + """Create an independent copy of this grid under a new ID. + + The copy is a true clone — the finished data is byte-copied, not + re-derived — carrying over the source's ``source``, ``modifications``, + ``georeference``, and ``checksum`` verbatim; only its ``id`` and + timestamps differ. Use this to branch from a grid before modifying the + copy while the original stays untouched. + + Parameters + ---------- + name : str, optional + Name for the copy. Defaults to the source's name. + description : str, optional + Description for the copy. Defaults to the source's description. + tags : List[str], optional + Tags for the copy. Defaults to the source's tags. + + Returns + ------- + Grid + The new Grid object (job status "pending" while the data is + copied; call :meth:`wait` before using it). + + Raises + ------ + NotFoundException + If the grid no longer exists. + """ + request_body = DuplicateGridRequest( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = duplicate_grid_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + def resample( + self, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + method_overrides=None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, + ) -> "Grid": + """Create a new grid by resampling this grid onto a different lattice. + + Parameters + ---------- + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep this grid's pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "nearest", "average"). + method_overrides : CreateResampleRequestMethodOverrides, optional + Per-band resampling method overrides. + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The new (pending) resampled Grid. + """ + self._require_completed("resample") + request_body = CreateResampleRequest( + source_grid_id=self.id, + alignment=_build_alignment( + output_resolution_m, align_to, align, resampling + ), + method_overrides=_opt(method_overrides), + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_resample.sync_detailed( + self.domain_id, client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + def apply_modifications(self, modifications: list) -> "Grid": + """Apply modification rules to this grid in place. + + The grid keeps its ID; the submitted rules are appended to its + cumulative ``modifications`` list and the data is re-derived as a + background job — the grid returns to "pending" status, so call + :meth:`wait` before using its data. Unlike a creator's + ``modifications=`` argument (applied while the grid is first built), + this modifies a grid you already hold. + + Parameters + ---------- + modifications : list + Modification rules (``GridModification``). Build feature masks with + :func:`fastfuels_sdk.v2.modifications.mask` (``ff.mask``); each rule + pairs conditions (spatial or band-value tests, ANDed) with actions + that rewrite band values for the matching cells. + + Returns + ------- + Grid + ``self``, updated (so calls chain). + + Raises + ------ + ValueError + If the grid is not completed. + NotFoundException + If the grid no longer exists. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> grid.apply_modifications([ff.mask(roads, "fbfm", 91, buffer_m=5)]) + >>> grid.wait() + """ + self._require_completed("apply modifications to") + request_body = ApplyGridModificationsRequest(modifications=list(modifications)) + response = apply_grid_modifications_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def export( + self, + format: str = "geotiff", + bands: Optional[List[str]] = None, + expiration_days: int = 7, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + ): + """Export this grid to a downloadable file format. + + Parameters + ---------- + format : str, optional + Export format: "geotiff" (2D only), "netcdf", or "zarr" + (default "geotiff"). + bands : List[str], optional + Band keys to include (default: all bands). + expiration_days : int, optional + Days until the signed download URL expires (max 7, default 7). + name, description : str, optional + Metadata for the export. + tags : List[str], optional + Tags for the export. + + Returns + ------- + Export + The created Export object (job status "pending"). Call + :meth:`Export.wait` and then :meth:`Export.to_file` to download + the file. + """ + # Lazy import so the modules stay decoupled (exports never imports + # grids). + from fastfuels_sdk.v2.exports import Export + + request_body = ExportGridRequest( + bands=_opt(bands), + expiration_days=expiration_days, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_grid_export.sync_detailed( + self.domain_id, + self.id, + GridExportFormat(format), + client=ensure_client(), + body=request_body, + ) + return Export._from_model(expect(response, HTTPStatus.CREATED)) + + def _chunk_count(self) -> int: + """Number of chunks the completed grid's data is split into.""" + count = self.chunks.count if self.chunks is not None else None + if count is UNSET or count is None: + raise ValueError( + "Grid chunk layout is unavailable. Call .refresh() once the " + "job has completed before reading data." + ) + return count + + def _request_chunk(self, band: str, chunk_index: int, array_format: str): + """GET one (band, chunk) as a raw octet-stream ``httpx.Response``. + + Bypasses ``get_grid_data_binary.sync_detailed()``, whose generated + parser JSON-decodes the binary body and raises (tracked in #184). We + reuse its ``_get_kwargs()`` for correct URL/param construction, then + issue the request through the shared client and read the bytes/headers + off the response directly. + """ + kwargs = get_grid_data_binary._get_kwargs( + self.domain_id, + self.id, + band, + chunk_index, + array_format=GridDataArrayFormat(array_format), + order=GridDataOrder.C, + ) + return ensure_client().get_httpx_client().request(**kwargs) + + def _read_chunk(self, band: str, chunk_index: int, array_format: str): + """Fetch and decode one chunk, retrying oversized dense chunks as sparse.""" + response = self._request_chunk(band, chunk_index, array_format) + if ( + response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE + and array_format == "dense" + ): + # The API returns 413 for an oversized dense chunk and suggests + # the sparse encoding; honor that hint transparently. + response = self._request_chunk(band, chunk_index, "sparse") + if response.status_code != HTTPStatus.OK: + raise_for_response( + Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=None, + ) + ) + return _decode_grid_chunk(response.content, response.headers) + + def band_summary(self, band: str): + """Return summary statistics for one band, without downloading the data. + + The server computes a per-band summary when the grid completes, so this + is a cheap overview that does not fetch the grid's cells (unlike + :meth:`to_numpy`). + + Parameters + ---------- + band : str + The band key to summarize (see :attr:`bands` for available keys). + + Returns + ------- + ContinuousBandSummary or CategoricalBandSummary or None + The band's summary, discriminated by its ``type_``: + ``"continuous"`` carries ``count``, ``nodata_count``, ``min_``, + ``max_``, ``mean``, and ``std``; ``"categorical"`` carries + ``count``, ``nodata_count``, and ``unique_count``. ``None`` until + the grid completes (call :meth:`wait` first). + + Raises + ------ + ValueError + If ``band`` is not one of the grid's bands. + + Examples + -------- + >>> grid = ff.get_grid(domain, "def456").wait() + >>> grid.band_summary("elevation").mean + 2143.7 + """ + summary = self._band(band).summary + return None if summary is UNSET else summary + + def to_numpy(self, band: str) -> "np.ndarray": + """Read one band of this grid into an in-memory NumPy array. + + Fetches every chunk of ``band`` and reassembles them into a single + array shaped like the full grid: 2D ``(y, x)`` for rasters, 3D + ``(z, y, x)`` for voxel grids. Chunks are requested in the dense + encoding for 2D grids and the sparse encoding for 3D grids, matching + how the data is stored. + + Parameters + ---------- + band : str + The band key to read (see :attr:`bands` for available keys). + + Returns + ------- + numpy.ndarray + The band's values. Cells with no data carry the band's ``nodata`` + value, or NaN (floating dtypes) / 0 (integer dtypes) when the band + defines none. + + Raises + ------ + ValueError + If the grid is not completed, or ``band`` is not one of its bands. + + Examples + -------- + >>> grid = ff.get_grid(domain, "def456").wait() + >>> elevation = grid.to_numpy("elevation") + >>> elevation.shape + (1200, 1600) + """ + self._require_completed("read data from") + nodata = self._band(band).nodata + # 3D grids are stored sparsely; 2D rasters densely (413 falls back). + array_format = "sparse" if len(self.georeference.shape) == 3 else "dense" + + full = None + for chunk_index in range(self._chunk_count()): + offset, block = self._read_chunk(band, chunk_index, array_format) + if full is None: + full = np.full( + tuple(self.georeference.shape), + _fill_for(block.dtype, nodata), + dtype=block.dtype, + ) + slices = tuple(slice(o, o + s) for o, s in zip(offset, block.shape)) + full[slices] = block + return full + + def to_xarray(self): + """Read this grid's bands into an in-memory :class:`xarray.Dataset`. + + Each band becomes a data variable over ``(y, x)`` (2D) or + ``(z, y, x)`` (3D), with ``x``/``y`` (and ``z``) coordinate vectors + derived from the grid's affine transform and the CRS recorded on the + dataset's ``crs`` attribute. + + Returns + ------- + xarray.Dataset + All bands of the grid as aligned data variables. + + Raises + ------ + ValueError + If the grid is not completed. + + Examples + -------- + >>> grid = ff.get_grid(domain, "def456").wait() + >>> ds = grid.to_xarray() + >>> ds["elevation"].mean().item() + 2143.7 + """ + import xarray as xr + + self._require_completed("read data from") + is_3d = len(self.georeference.shape) == 3 + dims = ("z", "y", "x") if is_3d else ("y", "x") + data_vars = {b.key: (dims, self.to_numpy(b.key)) for b in self.bands} + return xr.Dataset( + data_vars=data_vars, + coords=self._coords(is_3d), + attrs={"crs": self.georeference.crs}, + ) + + def _coords(self, is_3d: bool) -> dict: + """Build x/y(/z) cell-center coordinate vectors from the georeference. + + Assumes a north-up affine (no rotation), the convention for FastFuels + grids: ``transform`` is the six-element ``(a, b, c, d, e, f)`` mapping + ``x = a*col + c`` and ``y = e*row + f``. + """ + geo = self.georeference + a, _b, c, _d, e, f = geo.transform[:6] + shape = geo.shape + ny, nx = (shape[1], shape[2]) if is_3d else (shape[0], shape[1]) + coords = { + "x": c + a * (np.arange(nx) + 0.5), + "y": f + e * (np.arange(ny) + 0.5), + } + if is_3d: + coords["z"] = geo.z_origin + geo.z_resolution * (np.arange(shape[0]) + 0.5) + return coords + + def to_json(self) -> str: + """Serialize the complete Grid object to a JSON string. + + Returns + ------- + str + The Grid as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + +# --------------------------------------------------------------------------- +# Create grids from an external source (module-level functions) +# --------------------------------------------------------------------------- + + +def create_topography_grid_from_3dep( + domain, + source_resolution_m: int = 10, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + bands: Optional[list] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a topography grid from USGS 3DEP elevation data. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + source_resolution_m : int, optional + The 3DEP product family to draw from: 1, 10, or 30 meters + (default 10). Use :func:`check_3dep_coverage` first for 1 m data. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. By default + the source resolution is kept. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "cubic"). + bands : list, optional + Topography bands to produce (``TopographyBand`` members or their string + keys, e.g. "elevation", "slope", "aspect"). Defaults to all. + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateThreeDepTopographyRequest( + source_resolution=ThreeDepResolution(source_resolution_m), + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + bands=_enum_list(bands, TopographyBand), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_3dep_topography.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_topography_grid_from_landfire( + domain, + version: Optional[str] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + bands: Optional[list] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a topography grid from LANDFIRE elevation data. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + LANDFIRE version (see ``LandfireTopographyVersion``). Defaults to the + API's current version. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "cubic"). + bands : list, optional + Topography bands to produce (``TopographyBand`` members or their string + keys). Defaults to all. + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateLandfireTopographyRequest( + version=(LandfireTopographyVersion(version) if version is not None else UNSET), + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + bands=_enum_list(bands, TopographyBand), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_landfire_topography.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_canopy_fuel_grid_from_landfire( + domain, + version: Optional[str] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + bands: Optional[list] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a canopy fuel grid from LANDFIRE data. + + Produces canopy fuel bands (e.g. canopy bulk density, canopy base height, + canopy cover, canopy height) within the domain. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + LANDFIRE version (see ``LandfireCanopyVersion``). Defaults to the API's + current version. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "cubic"). + bands : list, optional + Canopy fuel bands to produce (``LandfireCanopyFuelBand`` members or + their string keys). Defaults to all. + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateLandfireCanopyRequest( + version=LandfireCanopyVersion(version) if version is not None else UNSET, + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + bands=_enum_list(bands, LandfireCanopyFuelBand), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_landfire_canopy.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_canopy_height_grid_from_meta( + domain, + version: Optional[str] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a canopy height grid from the Meta Canopy Height Model. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + Meta CHM version (see ``MetaCHMVersion``). Defaults to the API's + current version. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "bilinear", "cubic"). + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateMetaChmRequest( + version=MetaCHMVersion(version) if version is not None else UNSET, + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_meta_chm.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_canopy_height_grid_from_naip_chm( + domain, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a canopy height grid from the NAIP-CHM model. + + NAIP-CHM is a 0.6 m canopy height and structure model covering the + contiguous US (CONUS). The grid carries a single continuous ``chm`` band + (above-ground height in meters). + + NAIP-CHM is a surface model (nDSM): it captures the top of *all* + above-ground structure — vegetation **and** buildings/infrastructure — not + vegetation alone. To model vegetative fuels only, subtract built structures + with ``modifications=`` (e.g. a building-footprint mask). Coverage is + CONUS-only; domains outside it return no data. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. The source + is 0.6 m; coarser outputs are resampled. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method for the continuous height band (e.g. "bilinear", + "cubic"). + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + + References + ---------- + Morford, S. L., Allred, B. W., Coons, S. P., Marcozzi, A. A., McCord, S. E., + Smith, J. T., & Naugle, D. E. (2025). A 0.6-meter resolution canopy height + and structure model for the contiguous United States. bioRxiv. + https://doi.org/10.64898/2025.12.12.694075 + + Dataset and model code: https://github.com/smorf-ntsg/naip-chm + """ + request_body = CreateNaipChmRequest( + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_naip_chm.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_canopy_height_grid_from_point_cloud( + point_cloud: "PointCloud", + output_resolution_m: Optional[float] = None, + align_to=None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a canopy height grid from a completed airborne point cloud. + + Parameters + ---------- + point_cloud : PointCloud + The completed airborne point cloud to rasterize. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. Defaults to + 1 meter when no alignment target is provided. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + resampling : str, optional + Resampling method for the continuous canopy-height band. + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created canopy-height Grid (job status "pending" or "running"). + + Raises + ------ + ValueError + If the point cloud is not completed or is not airborne. + """ + if point_cloud.status != JobStatus.COMPLETED: + raise ValueError( + f"Point cloud {point_cloud.id} must be completed before creating " + f"a canopy height grid (current status: {point_cloud.status.value})." + ) + if point_cloud.type_.value != "als": + raise ValueError( + f"Point cloud {point_cloud.id} must be airborne (ALS) to create a " + f"canopy height grid (got {point_cloud.type_.value!r})." + ) + + request_body = CreatePointCloudChmRequest( + source_point_cloud_id=point_cloud.id, + alignment=_build_alignment( + output_resolution_m, + align_to, + resampling=resampling, + ), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_point_cloud_chm.sync_detailed( + point_cloud.domain_id, + client=ensure_client(), + body=request_body, + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_fuel_model_grid_from_landfire_fbfm13( + domain, + version: Optional[str] = None, + remove_non_burnable: Optional[list] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel model grid from LANDFIRE Anderson 13 fuel models. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + LANDFIRE version (``"2023"`` or ``"2024"``). Defaults to the API's + current version. + remove_non_burnable : list, optional + Non-burnable fuel models to drop (``NonBurnableFuelModel`` members or + their string keys, e.g. ``"NB1"``, ``"NB2"``). + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. ``"nearest"`` for categorical fuel models). + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status ``"pending"`` or ``"running"``). + """ + request_body = CreateLandfireFbfm13Request( + version=LandfireFbfm13Version(version) if version is not None else UNSET, + remove_non_burnable=_enum_list(remove_non_burnable, NonBurnableFuelModel), + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_landfire_fbfm13.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_fuel_model_grid_from_landfire_fbfm40( + domain, + version: Optional[str] = None, + remove_non_burnable: Optional[list] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel model grid from LANDFIRE 40 Scott & Burgan fuel models. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + LANDFIRE version (see ``LandfireFbfm40Version``). Defaults to the API's + current version. + remove_non_burnable : list, optional + Non-burnable fuel models to drop (``NonBurnableFuelModel`` members or + their string keys, e.g. "NB1", "NB2"). + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "nearest" for categorical fuel models). + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateLandfireFbfm40Request( + version=LandfireFbfm40Version(version) if version is not None else UNSET, + remove_non_burnable=_enum_list(remove_non_burnable, NonBurnableFuelModel), + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_landfire_fbfm40.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_fuel_model_grid_from_landfire_fccs( + domain, + version: Optional[str] = None, + remove_bare_ground: bool = False, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel model grid from LANDFIRE FCCS fuelbeds. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + LANDFIRE version (see ``LandfireFccsVersion``). Defaults to the API's + current version. + remove_bare_ground : bool, optional + Drop bare-ground fuelbeds (default False). + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method (e.g. "nearest" for categorical fuelbeds). + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateLandfireFccsRequest( + version=LandfireFccsVersion(version) if version is not None else UNSET, + remove_bare_ground=remove_bare_ground, + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_landfire_fccs.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_pim_grid_from_treemap( + domain, + version: Optional[str] = None, + output_resolution_m: Optional[float] = None, + align_to=None, + align: Optional[str] = None, + resampling: Optional[str] = None, + bands: Optional[list] = None, + extent_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a Plot Imputation Map (PIM) grid from TreeMap. + + TreeMap imputes an FIA plot to every forested 30 m pixel across the + conterminous US. The resulting grid carries up to two categorical bands: + + - ``tm_id``: the TreeMap raster pixel value (always present) + - ``plt_cn``: the FIA plot condition number (request via ``bands``) + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + version : str, optional + TreeMap version (see ``TreeMapVersion``). Defaults to the API's + current version. + output_resolution_m : float, optional + Output cell size in meters, anchored to the domain origin. + align_to : Grid or str, optional + Match the lattice of an existing grid (or its id). + align : str, optional + Pass ``"native"`` to keep the source raster's pixel anchor. + resampling : str, optional + Resampling method. TreeMap bands are categorical, so use "nearest" + (interpolating plot ids is meaningless). + bands : list, optional + TreeMap bands to produce (``TreeMapBand`` members or their string + keys: "tm_id", "plt_cn"). Defaults to ``tm_id`` only. + extent_buffer_cells : int, optional + Result-grid cells to buffer around the domain extent (0-10, default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + request_body = CreateTreeMapRequest( + version=TreeMapVersion(version) if version is not None else UNSET, + alignment=_build_alignment(output_resolution_m, align_to, align, resampling), + bands=_enum_list(bands, TreeMapBand), + extent_buffer_cells=extent_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_treemap.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +# --------------------------------------------------------------------------- +# Create grids from your own file / supplied values +# --------------------------------------------------------------------------- + + +def create_grid_from_geotiff( + domain, + path: str, + bands: list, + num_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Grid: + """Create a grid by uploading a local GeoTIFF. + + Creates the grid resource, uploads the file to the returned signed URL, and + returns the (pending) Grid. The GeoTIFF's CRS must match the domain CRS. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + path : str + Path to the local ``.tif``/``.tiff`` file (max 1 GB). + bands : list + Band definitions (``UploadBandDefinition``) mapping 1:1 to the GeoTIFF + raster bands in order. + num_buffer_cells : int, optional + Cells kept around the domain extent in the stored grid (default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + + Returns + ------- + Grid + The created Grid object (job status "pending"). Call :meth:`Grid.wait` + to block until the uploaded file is processed. + """ + request_body = CreateGeoTIFFUploadRequest( + bands=bands, + num_buffer_cells=num_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_geotiff_upload.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + created = expect(response, HTTPStatus.CREATED) + put_upload(created.upload, path) + return Grid._from_model(created.grid) + + +def create_grid_from_netcdf( + domain, + path: str, + num_buffer_cells: int = 0, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Grid: + """Create a grid by uploading a local NetCDF file. + + Creates the grid resource, uploads the file to the returned signed URL, and + returns the (pending) Grid. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + path : str + Path to the local NetCDF file. + num_buffer_cells : int, optional + Cells kept around the domain extent in the stored grid (default 0). + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + + Returns + ------- + Grid + The created Grid object (job status "pending"). Call :meth:`Grid.wait` + to block until the uploaded file is processed. + """ + request_body = CreateNetcdfUploadRequest( + num_buffer_cells=num_buffer_cells, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_netcdf_upload.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + created = expect(response, HTTPStatus.CREATED) + put_upload(created.upload, path) + return Grid._from_model(created.grid) + + +def create_uniform_grid( + domain, + resolution_m: float, + bands: Dict[str, Union[float, int]], + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a uniform (constant-value) grid. + + Each band fills the entire domain with a single value at the given + resolution. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the grid in. + resolution_m : float + Grid cell size in meters (required — uniform grids have no native + resolution). + bands : dict + Mapping of band key to constant value, e.g. + ``{"fuel_load": 0.5, "fuel_moisture": 15.0}``. Keys must be valid + ``UniformBand`` values. + name, description : str, optional + Metadata for the grid. + tags : List[str], optional + Tags for the grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The created Grid object (job status "pending" or "running"). + """ + band_inputs = [ + UniformBandInput(key=UniformBand(key), value=value) + for key, value in bands.items() + ] + request_body = CreateUniformRequest( + resolution=resolution_m, + bands=band_inputs, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_uniform_grid_endpoint.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_grid_from_compose( + inputs: Mapping[str, "Grid"], + *, + select: Optional[list] = None, + compute: Optional[list] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a grid by selecting and computing bands from aligned grids. + + Parameters + ---------- + inputs : mapping of str to Grid + Alias-to-grid mapping for completed, aligned 2D grids in one domain. + Operations refer to their bands as ``"alias.band_key"``. + select : list of ComposeSelect, optional + Bands to copy, built with :func:`fastfuels_sdk.v2.compose.select`. + compute : list of ComposeCompute, optional + Bands to calculate, built with + :func:`fastfuels_sdk.v2.compose.compute`. + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is composed. + + Returns + ------- + Grid + The new pending composed Grid. + + Raises + ------ + TypeError + If inputs or operations have the wrong shape. + ValueError + If inputs are empty, incomplete, duplicated, or from different + domains; if aliases or band references are invalid; or if operation + outputs are empty or duplicated. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> composed = ff.grids.create_grid_from_compose( + ... {"fuels": fuel_grid}, + ... select=[ff.compose.select("fuel_depth", "fuels.fuel_depth")], + ... compute=[ + ... ff.compose.compute( + ... "fuel_load.1hr", + ... "multiply", + ... ["fuels.fuel_load.1hr", 0.5], + ... ) + ... ], + ... ) + >>> composed.wait() + """ + if not isinstance(inputs, Mapping): + raise TypeError("inputs must be an alias-to-Grid mapping.") + if not inputs: + raise ValueError("inputs must contain at least one aliased Grid.") + + compose_inputs = [] + grids_by_alias = {} + seen_grid_ids = set() + domain_id = None + for alias, grid in inputs.items(): + if ( + not isinstance(alias, str) + or re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", alias) is None + ): + raise ValueError( + f"Invalid compose alias {alias!r}; aliases must start with a " + "letter and contain only letters, numbers, and underscores." + ) + if not isinstance(grid, Grid): + raise TypeError(f"Compose input {alias!r} must be a Grid object.") + grid._require_completed("compose") + if grid.id in seen_grid_ids: + raise ValueError(f"Grid {grid.id!r} is assigned more than one alias.") + if domain_id is None: + domain_id = grid.domain_id + elif grid.domain_id != domain_id: + raise ValueError("All compose input grids must belong to the same domain.") + seen_grid_ids.add(grid.id) + grids_by_alias[alias] = grid + compose_inputs.append(ComposeInput(grid_id=grid.id, alias=alias)) + + select_operations = _compose_operations(select, ComposeSelect, "select") + compute_operations = _compose_operations(compute, ComposeCompute, "compute") + operations = [*select_operations, *compute_operations] + if not operations: + raise ValueError("At least one select or compute operation is required.") + outputs = [operation.output for operation in operations] + if any(not isinstance(output, str) or not output for output in outputs): + raise ValueError("Every compose operation requires a nonempty output band.") + duplicates = sorted({output for output in outputs if outputs.count(output) > 1}) + if duplicates: + raise ValueError(f"Compose output bands must be unique: {duplicates}.") + for operation in operations: + _validate_compose_references(operation, grids_by_alias) + + request_body = CreateComposeRequest( + inputs=compose_inputs, + select=select_operations if select_operations else UNSET, + compute=compute_operations if compute_operations else UNSET, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_compose_grid.sync_detailed( + domain_id, + client=ensure_client(), + body=request_body, + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def _compose_operations(value, model_type, name: str) -> list: + """Normalize and type-check one compose operation collection.""" + if value is None: + return [] + if isinstance(value, model_type): + raise TypeError(f"{name} must be a list of {model_type.__name__} objects.") + try: + operations = list(value) + except TypeError: + raise TypeError( + f"{name} must be a list of {model_type.__name__} objects." + ) from None + if not all(isinstance(operation, model_type) for operation in operations): + raise TypeError(f"{name} must contain only {model_type.__name__} objects.") + return operations + + +def _validate_compose_references(operation, grids_by_alias) -> None: + """Catch alias and band typos before dispatching a compose request.""" + if isinstance(operation, ComposeSelect): + _validate_compose_reference(operation.from_, grids_by_alias) + else: + _validate_compose_operands(operation.operands, grids_by_alias) + + conditions = operation.conditions + if conditions is not UNSET and conditions is not None: + for condition in conditions: + if isinstance(condition, ComposeAttributeCondition): + _validate_compose_reference(condition.band, grids_by_alias) + + else_value = operation.else_ + if ( + else_value is UNSET + or else_value is None + or isinstance(else_value, ComposeLiteral) + ): + return + if isinstance(else_value, InlineCompute): + _validate_compose_operands(else_value.operands, grids_by_alias) + elif isinstance(else_value, str) and "." in else_value: + _validate_compose_reference(else_value, grids_by_alias) + + +def _validate_compose_operands(operands, grids_by_alias) -> None: + for operand in operands: + if isinstance(operand, str): + _validate_compose_reference(operand, grids_by_alias) + + +def _validate_compose_reference(reference: str, grids_by_alias) -> None: + alias, separator, band_key = reference.partition(".") + if not separator or alias not in grids_by_alias or not band_key: + raise ValueError( + f"Unknown compose band reference {reference!r}; use " + "'alias.band_key' with an alias from inputs." + ) + available = [band.key for band in grids_by_alias[alias].bands] + if band_key not in available: + raise ValueError( + f"Grid {grids_by_alias[alias].id} has no {band_key!r} band for " + f"compose alias {alias!r}. Available bands: {available}." + ) + + +# --------------------------------------------------------------------------- +# Derive a grid from a grid you hold (source-specific transforms) +# --------------------------------------------------------------------------- +# +# Universal transforms that apply to *any* grid are methods on ``Grid`` +# (``resample``, ``export``). Transforms that only make sense for a particular +# kind of grid are functions here instead — keeping them off ``Grid`` so they +# never appear on a grid that cannot perform them. + + +def create_fuel_grid_from_fccs_lookup( + source_grid: "Grid", + bands: list, + source_band: str = "fccs", + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel-parameter grid by looking up FCCS codes in a grid. + + Parameters + ---------- + source_grid : Grid + A completed grid carrying FCCS codes (produced by + :func:`create_fuel_model_grid_from_landfire_fccs`). + bands : list + The FCCS lookup bands to produce (``FccsLookupBand`` members or + string keys such as ``"fuel_load.duff"``, ``"duff_depth"``, and + ``"fuel_load.live_shrub"``). + source_band : str, optional + The band in ``source_grid`` that holds FCCS codes (default ``"fccs"``). + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The new pending fuel-parameter Grid. + + Raises + ------ + ValueError + If ``source_grid`` is not completed or has no ``source_band`` band. + """ + source_grid._require_completed("look up fuel parameters from") + band_keys = [band.key for band in source_grid.bands] + if source_band not in band_keys: + raise ValueError( + f"Grid {source_grid.id} has no {source_band!r} band to look up; pass " + "an FCCS fuel model grid (see " + f"create_fuel_model_grid_from_landfire_fccs). Available bands: " + f"{band_keys}." + ) + request_body = CreateFccsLookupRequest( + source_grid_id=source_grid.id, + bands=_enum_list(bands, FccsLookupBand), + source_band=source_band, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_fccs_lookup.sync_detailed( + source_grid.domain_id, + client=ensure_client(), + body=request_body, + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_fuel_grid_from_fbfm13_lookup( + source_grid: "Grid", + bands: list, + source_band: str = "fbfm13", + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel-parameter grid by looking up FBFM13 codes in a grid. + + Parameters + ---------- + source_grid : Grid + A completed grid carrying FBFM13 codes (produced by + :func:`create_fuel_model_grid_from_landfire_fbfm13`). + bands : list + The FBFM13 lookup bands to produce (``Fbfm13LookupBand`` members or + string keys such as ``"fuel_load.1hr"`` and ``"fuel_depth"``). + source_band : str, optional + The band in ``source_grid`` that holds FBFM13 codes (default + ``"fbfm13"``). + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The new pending fuel-parameter Grid. + + Raises + ------ + ValueError + If ``source_grid`` is not completed or has no ``source_band`` band. + """ + source_grid._require_completed("look up fuel parameters from") + band_keys = [band.key for band in source_grid.bands] + if source_band not in band_keys: + raise ValueError( + f"Grid {source_grid.id} has no {source_band!r} band to look up; pass " + "an FBFM13 fuel model grid (see " + f"create_fuel_model_grid_from_landfire_fbfm13). Available bands: " + f"{band_keys}." + ) + request_body = CreateFbfm13LookupRequest( + source_grid_id=source_grid.id, + bands=_enum_list(bands, Fbfm13LookupBand), + source_band=source_band, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_fbfm13_lookup.sync_detailed( + source_grid.domain_id, + client=ensure_client(), + body=request_body, + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_fuel_grid_from_fbfm40_lookup( + source_grid: "Grid", + bands: list, + source_band: str = "fbfm", + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, +) -> Grid: + """Create a fuel-parameter grid by looking up FBFM40 codes in a grid. + + The `fbfm` band of an FBFM40 fuel model grid holds categorical fuel-model + codes, not the quantities a fire model consumes. This translates those + codes into fuel parameters (loadings by size class, fuel-bed depth, + surface-area-to-volume ratios), returning a new grid whose bands are the + requested parameters. + + Parameters + ---------- + source_grid : Grid + A completed grid carrying FBFM40 codes (produced by + :func:`create_fuel_model_grid_from_landfire_fbfm40`). + bands : list + The FBFM40 lookup bands to produce (``Fbfm40LookupBand`` members or + their string keys, e.g. "fuel_load.1hr", "fuel_depth"). + source_band : str, optional + The band in ``source_grid`` that holds FBFM40 codes (default "fbfm"). + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + modifications : list, optional + Modification rules applied after the grid is built. + + Returns + ------- + Grid + The new (pending) fuel-parameter Grid. + + Raises + ------ + ValueError + If ``source_grid`` is not completed, or carries no ``source_band`` band + to look up (i.e. it is not an FBFM40 fuel model grid). + """ + source_grid._require_completed("look up fuel parameters from") + band_keys = [b.key for b in source_grid.bands] + if source_band not in band_keys: + raise ValueError( + f"Grid {source_grid.id} has no {source_band!r} band to look up; pass " + "an FBFM40 fuel model grid (see " + f"create_fuel_model_grid_from_landfire_fbfm40). Available bands: " + f"{band_keys}." + ) + request_body = CreateFbfm40LookupRequest( + source_grid_id=source_grid.id, + bands=_enum_list(bands, Fbfm40LookupBand), + source_band=source_band, + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + ) + response = create_fbfm40_lookup.sync_detailed( + source_grid.domain_id, client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_surface_fuel_grid_from_duet( + source_grid: "Grid", + years_since_burn: int, + wind_direction: int = 270, + wind_variability: int = 30, + bands: Optional[list] = None, + calibration: Optional[DuetCalibration] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Grid: + """Create a 2D DUET surface-fuel grid from a 3D tree grid. + + Parameters + ---------- + source_grid : Grid + A completed 3D tree grid carrying ``bulk_density.foliage.live``, + ``spcd``, and ``fuel_moisture.live`` bands. + years_since_burn : int + Years of litter accumulation to simulate, from 1 through 100. + wind_direction : int, optional + Prevailing wind direction in whole degrees clockwise from north + (0-359, default 270). + wind_variability : int, optional + Angular spread of wind direction in whole degrees (0-180, default 30). + bands : list, optional + DUET output bands (``DuetBand`` members or string keys). Defaults to + ``fuel_load.grass`` and ``fuel_load.litter``. + calibration : DuetCalibration, optional + Per-parameter and per-fuel-type targets from + :func:`fastfuels_sdk.v2.calibrations.duet_calibration`. If omitted, + the grid stores raw DUET values. + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + + Returns + ------- + Grid + The new pending DUET surface-fuel Grid. + + Raises + ------ + TypeError + If a DUET time or wind parameter is not a whole number. + ValueError + If a parameter is out of range, the source is not completed, or the + source lacks a required tree band. + """ + source_grid._require_completed("create a DUET surface fuel grid from") + required_bands = { + "bulk_density.foliage.live", + "spcd", + "fuel_moisture.live", + } + band_keys = {band.key for band in source_grid.bands} + missing = sorted(required_bands - band_keys) + if missing: + raise ValueError( + f"Grid {source_grid.id} lacks DUET source bands: {missing}. " + f"Available bands: {sorted(band_keys)}." + ) + + years_since_burn = _duet_integer( + "years_since_burn", years_since_burn, minimum=1, maximum=100 + ) + wind_direction = _duet_integer( + "wind_direction", wind_direction, minimum=0, maximum=359 + ) + wind_variability = _duet_integer( + "wind_variability", wind_variability, minimum=0, maximum=180 + ) + requested_bands = _enum_list(bands, DuetBand) + if requested_bands is not UNSET: + if not requested_bands: + raise ValueError("bands must contain at least one DUET band.") + if len(set(requested_bands)) != len(requested_bands): + raise ValueError("bands contains duplicate DUET bands.") + + request_body = CreateDuetRequest( + source_grid_id=source_grid.id, + years_since_burn=years_since_burn, + wind_direction=wind_direction, + wind_variability=wind_variability, + bands=requested_bands, + calibration=_opt(calibration), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_duet_grid.sync_detailed( + source_grid.domain_id, + client=ensure_client(), + body=request_body, + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + +def _duet_integer(name: str, value, *, minimum: int, maximum: int) -> int: + """Validate a bounded whole-number DUET parameter.""" + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be a whole number.") + if not minimum <= value <= maximum: + raise ValueError(f"{name} must be between {minimum} and {maximum}.") + return value + + +# --------------------------------------------------------------------------- +# Listing, fetching, and utilities +# --------------------------------------------------------------------------- + + +def list_grids( + domain=None, + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + source: Optional[str] = None, + product: Optional[str] = None, + tag: Optional[str] = None, +) -> List[Grid]: + """List grids in a domain, or across all domains (single page). + + Parameters + ---------- + domain : Domain or str, optional + The domain (or its id) to list grids in. If omitted, grids from all the + user's domains are listed. + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of grids per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + source : str, optional + Only return grids from this source (e.g. "landfire", "3dep"). + product : str, optional + Only return grids from this source product (e.g. "fbfm40", + "topography"). Requires ``source``. + tag : str, optional + Only return grids carrying this tag. + + Returns + ------- + List[Grid] + The requested page of Grid objects. + """ + kwargs = dict( + client=ensure_client(), + page=page, + size=size, + sort_by=GridSortField(sort_by) if sort_by else UNSET, + sort_order=SortOrder(sort_order) if sort_order else UNSET, + source=_opt(source), + product=_opt(product), + tag=_opt(tag), + ) + if domain is None: + response = list_grids_cross_domain.sync_detailed(**kwargs) + else: + response = list_grids_endpoint.sync_detailed(_domain_id(domain), **kwargs) + list_response: ListGridsResponse = expect(response) + return [Grid._from_model(g) for g in list_response.grids] + + +def get_grid(domain, grid_id: str) -> Grid: + """Retrieve a single grid by its ID. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the grid belongs to. + grid_id : str + The unique identifier of the grid. + + Returns + ------- + Grid + The requested Grid object. + + Raises + ------ + NotFoundException + If no grid exists with the given IDs, or the user does not have access. + """ + return Grid.from_id(_domain_id(domain), grid_id) + + +def check_3dep_coverage( + domain, resolution_m: Optional[int] = None +) -> TopographyThreeDepCoverageResponse: + """Check USGS 3DEP tile coverage for a domain before creating a grid. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to check. + resolution_m : int, optional + Resolution to check: 1, 10, or 30 meters. Defaults to the API's default. + + Returns + ------- + TopographyThreeDepCoverageResponse + Tile availability, count, URLs, and (for 1 m) acquisition dates. + """ + response = check_3dep_coverage_endpoint.sync_detailed( + _domain_id(domain), + client=ensure_client(), + resolution=ThreeDepResolution(resolution_m) if resolution_m else UNSET, + ) + return expect(response) diff --git a/fastfuels_sdk/v2/inventories.py b/fastfuels_sdk/v2/inventories.py new file mode 100644 index 0000000..cc93434 --- /dev/null +++ b/fastfuels_sdk/v2/inventories.py @@ -0,0 +1,1191 @@ +""" +fastfuels_sdk/v2/inventories.py +""" + +# Core imports +import json +from http import HTTPStatus +from io import StringIO +from pathlib import Path +from typing import List, Optional + +# Internal imports +from fastfuels_sdk.v2._jobs import wait as _wait +from fastfuels_sdk.v2._uploads import put_upload +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.exceptions import expect +from fastfuels_sdk.v2.client_library.api.inventories import ( + apply_modifications as apply_modifications_endpoint, + apply_treatments as apply_treatments_endpoint, + create_chm_inventory, + create_gdam_inventory, + create_inventory_export, + create_inventory_upload, + create_pim_inventory, + delete_inventory, + duplicate_inventory as duplicate_inventory_endpoint, + get_inventory as get_inventory_endpoint, + get_inventory_data_csv, + get_inventory_data_json, + get_inventory_data_metadata, + list_inventories as list_inventories_endpoint, + list_inventories_cross_domain, + update_inventory, +) +from fastfuels_sdk.v2.client_library.models import ( + Inventory as InventoryModel, + ApplyModificationsRequest, + ApplyTreatmentsRequest, + CreateChmInventoryRequest, + CreateGdamInventoryRequest, + CreateGdamInventoryRequestImputeColumnsItem, + CreateInventoryUploadRequest, + CreatePimInventoryRequest, + CreateTreeInventoryRequest, + CrownProfileModel, + DuplicateInventoryRequest, + ExportInventoryRequest, + InventoryColumnMapping, + InventoryDataMetadata, + InventoryDataResponse, + InventoryExportFormat, + InventoryJsonOrientation, + InventorySortField, + InventoryUploadFormat, + JobStatus, + ListInventoriesResponse, + PointProcess, + Resolution3D, + SortOrder, + TreeBand, + UpdateInventoryRequestBody, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +# External imports +import attrs +import pandas as pd + +__all__ = [ + "Inventory", + "create_tree_inventory_from_pim_grid", + "create_tree_inventory_from_chm_grid", + "create_tree_inventory_from_file", + "create_tree_inventory_from_gdam", + "list_inventories", + "get_inventory", +] + +_UPLOAD_FORMATS = { + ".csv": InventoryUploadFormat.CSV, + ".geojson": InventoryUploadFormat.GEOJSON, + ".json": InventoryUploadFormat.GEOJSON, + ".gpkg": InventoryUploadFormat.GEOPACKAGE, +} + + +def _domain_id(domain) -> str: + """Resolve a Domain object or a domain-id string to the id string.""" + return getattr(domain, "id", domain) + + +def _enum_list(values, enum_cls): + """Coerce a list of strings/enum members to enum members, or UNSET.""" + if values is None: + return UNSET + return [v if isinstance(v, enum_cls) else enum_cls(v) for v in values] + + +def _opt(value): + """Map ``None`` to the generated UNSET sentinel, else pass through.""" + return value if value is not None else UNSET + + +class Inventory(InventoryModel): + """Inventory resource for the FastFuels v2 API. + + An inventory is a table of individual entities — trees — within a + domain, generated from a PIM (TreeMap) grid, derived from a canopy + height model, or uploaded from your own data. Inventories are + asynchronous job resources — creation starts a background job and + returns a *pending* record; call :meth:`wait` to block until it is + ready. + + The tree records themselves are retrieved through the data methods + (:meth:`get_data_metadata`, :meth:`get_data_partition`, + :meth:`to_dataframe`) once the inventory is completed. + + Attributes + ---------- + id : str + Unique identifier for the inventory. + domain_id : str + Identifier of the domain the inventory belongs to. + type_ : InventoryType + Type of entities in the inventory (currently always "tree"). + status : JobStatus + Job status: "pending", "running", "completed", or "failed". + source : InventorySource + Where the inventory data comes from (e.g. a PIM grid, a CHM + grid, or an upload). + name : str + Human-readable name for the inventory. + description : str + Detailed description of the inventory. + progress : JobProgress, optional + Progress info while the job is running. + checksum : str, optional + Version marker for the inventory's content; changes each time + the data is rebuilt, unaffected by metadata-only edits. + modifications : List[InventoryModification], optional + Cumulative modification rules applied to the inventory. + treatments : list, optional + Silvicultural treatments applied to the inventory. + columns : List[Column], optional + The columns of the inventory's tabular data. + forestry_metrics : TreeForestryMetrics, optional + Stand-level tree count, basal area per acre, trees per acre, + quadratic mean diameter, and dominant FIA species groups. Populated + when a tree inventory completes; ``None`` when unavailable. + georeference : InventoryGeoreference, optional + Spatial reference of the generated data; populated when the job + completes. + error : JobError, optional + Error details if the job failed. + tags : List[str], optional + User-defined tags for organization. + created_on : datetime + When the inventory was created. + modified_on : datetime + When the inventory was last modified. + + Examples + -------- + Create a tree inventory from a PIM grid and load it as a DataFrame: + >>> import fastfuels_sdk as ff + >>> pim = ff.grids.create_pim_grid_from_treemap(domain) + >>> inventory = ff.inventories.create_tree_inventory_from_pim_grid( + ... domain, pim.wait() + ... ) + >>> trees = inventory.wait().to_dataframe() + + Get an inventory by ID: + >>> inventory = ff.get_inventory(domain, "abc123") + + See Also + -------- + create_tree_inventory_from_pim_grid : Expand a PIM grid into trees. + create_tree_inventory_from_chm_grid : Derive trees from a canopy height model. + create_tree_inventory_from_file : Upload your own tree records. + list_inventories : List inventories in a domain or across all domains. + """ + + @classmethod + def _from_model(cls, model: InventoryModel) -> "Inventory": + """Build an Inventory from a generated Inventory model instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + inventory = cls.from_dict(model.to_dict()) + if inventory.forestry_metrics is UNSET: + inventory.forestry_metrics = None + return inventory + + def _copy_fields_from(self, model: InventoryModel) -> "Inventory": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(InventoryModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + if self.forestry_metrics is UNSET: + self.forestry_metrics = None + self.additional_properties = dict(model.additional_properties) + return self + + def _require_completed(self, action: str) -> None: + """Raise if the inventory is not completed, before deriving from it.""" + if self.status != JobStatus.COMPLETED: + raise ValueError( + f"Cannot {action} an inventory with status '{self.status}'. " + "Call .wait() until it completes first." + ) + + def _column(self, column: str): + """Return the :class:`Column` with key ``column``, or raise if absent.""" + columns = [] if self.columns is UNSET or self.columns is None else self.columns + for inventory_column in columns: + if inventory_column.key == column: + return inventory_column + keys = [inventory_column.key for inventory_column in columns] + raise ValueError( + f"Inventory has no column {column!r}. Available columns: {keys}." + ) + + @classmethod + def from_id(cls, domain_id: str, inventory_id: str) -> "Inventory": + """Retrieve an existing Inventory resource by its ID. + + Parameters + ---------- + domain_id : str + The unique identifier of the domain the inventory belongs to. + inventory_id : str + The unique identifier of the inventory to retrieve. + + Returns + ------- + Inventory + The requested Inventory object. + + Raises + ------ + NotFoundException + If no inventory exists with the given IDs, or the user does not + have access to it. + """ + response = get_inventory_endpoint.sync_detailed( + domain_id, inventory_id, client=ensure_client() + ) + return cls._from_model(expect(response)) + + def refresh(self) -> "Inventory": + """Update this Inventory in place with the latest data from the API. + + Returns + ------- + Inventory + ``self``, updated with the latest data (so calls chain). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + """ + response = get_inventory_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return self._copy_fields_from(expect(response)) + + def wait( + self, timeout: Optional[float] = None, verbose: bool = False + ) -> "Inventory": + """Poll the inventory job until it reaches a terminal status. + + Parameters + ---------- + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + If True, print the job status at each poll. + + Returns + ------- + Inventory + ``self``, updated to its terminal state (so calls chain). + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapses before a terminal status. + JobFailedError + If the job finished with status "failed". + """ + return _wait(self, timeout=timeout, verbose=verbose) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Inventory": + """Update the inventory's mutable metadata (name, description, tags) in place. + + Only provided fields are sent. If no fields are provided, no API call + is made. + + Parameters + ---------- + name : str, optional + New name for the inventory. + description : str, optional + New description for the inventory. + tags : List[str], optional + New tags for the inventory (replaces existing tags). + + Returns + ------- + Inventory + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + """ + if name is None and description is None and tags is None: + return self + request_body = UpdateInventoryRequestBody( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = update_inventory.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self) -> None: + """Delete this inventory and its data. + + Raises + ------ + NotFoundException + If the inventory no longer exists. + """ + response = delete_inventory.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + expect(response, HTTPStatus.NO_CONTENT) + + def duplicate( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "Inventory": + """Create an independent copy of this inventory under a new ID. + + The copy is a true clone — the finished data is byte-copied, not + regenerated — carrying over the source's ``source``, + ``modifications``, ``treatments``, ``columns``, ``georeference``, + and ``checksum`` verbatim. Use this to branch a scenario: duplicate, + then modify the copy while the original stays untouched. + + Parameters + ---------- + name : str, optional + Name for the copy. Defaults to the source's name. + description : str, optional + Description for the copy. Defaults to the source's description. + tags : List[str], optional + Tags for the copy. Defaults to the source's tags. + + Returns + ------- + Inventory + The new Inventory object (job status "pending" while the data + is copied; call :meth:`wait` before using it). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + UnprocessableEntityException + If the inventory is not in "completed" status. + """ + request_body = DuplicateInventoryRequest( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = duplicate_inventory_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return Inventory._from_model(expect(response, HTTPStatus.CREATED)) + + def apply_modifications(self, modifications: list) -> "Inventory": + """Apply modification rules to this inventory in place. + + The inventory keeps its ID; the submitted rules are queued while the + tree data is re-derived as a background job. The inventory returns to + "pending" status, and the rules are appended to its cumulative + ``modifications`` list once processing completes. Call :meth:`wait` + before using its data. To keep the original data, :meth:`duplicate` + first and modify the copy. + + Parameters + ---------- + modifications : list + Modification rules (``InventoryModification``). Each rule + filters trees by conditions (ANDed together) and applies + actions — remove, multiply, divide, add, subtract, or + replace — to the matching rows. + + Returns + ------- + Inventory + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + """ + self._require_completed("apply modifications to") + request_body = ApplyModificationsRequest(modifications=list(modifications)) + response = apply_modifications_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def apply_treatments(self, treatments: list) -> "Inventory": + """Apply silvicultural treatments to this inventory in place. + + The inventory keeps its ID; the submitted treatments are queued while + the tree data is re-derived as a background job. The inventory returns + to "pending" status, and the treatments are appended to its cumulative + ``treatments`` list once processing completes. Call :meth:`wait` before + using its data. To keep the original data, :meth:`duplicate` first and + treat the copy. + + Parameters + ---------- + treatments : list + Treatments thinning the stand to a target. Build them with + :func:`fastfuels_sdk.v2.treatments.basal_area_treatment` (residual + basal area) or + :func:`fastfuels_sdk.v2.treatments.diameter_treatment` (diameter + limit), available as ``ff.basal_area_treatment`` / + ``ff.diameter_treatment``. + + Returns + ------- + Inventory + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> inventory.apply_treatments([ff.basal_area_treatment("from_below", 25.0)]) + >>> inventory.wait() + """ + self._require_completed("apply treatments to") + request_body = ApplyTreatmentsRequest(treatments=list(treatments)) + response = apply_treatments_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def voxelize( + self, + horizontal_resolution_m: Optional[float] = None, + vertical_resolution_m: Optional[float] = None, + bands: Optional[list] = None, + crown_profile_model: Optional[str] = None, + biomass_source=None, + max_crown_radius_source=None, + moisture_model=None, + seed: Optional[int] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + ): + """Create a 3D voxelized tree fuel grid from this inventory. + + Distributes each tree's crown biomass into 3D cells, producing a + voxel grid of canopy fuel properties. + + Parameters + ---------- + horizontal_resolution_m : float, optional + Horizontal (x and y) cell size in meters. Must be given + together with ``vertical_resolution_m``; if both are omitted + the API default resolution is used. + vertical_resolution_m : float, optional + Vertical (z) cell size in meters. + bands : list, optional + Voxel bands to produce (``TreeBand`` members or their string + keys, e.g. "bulk_density.foliage.live", "fuel_moisture.dead", + "savr.foliage", "spcd", "tree_id", "volume_fraction"). + Defaults to the API's standard band set. + crown_profile_model : str, optional + Crown shape model used to distribute biomass: "beta" or + "purves". + biomass_source : AllometryBiomassSource or InventoryColumnsBiomassSource, optional + Where crown biomass values come from: allometry equations or + columns of this inventory. + max_crown_radius_source : optional + Where each tree's maximum crown radius comes from. + moisture_model : MoistureModel, optional + Fuel moisture values assigned to the voxelized fuels. + seed : int, optional + Random seed for reproducibility. Generated randomly if omitted. + name, description : str, optional + Metadata for the new grid. + tags : List[str], optional + Tags for the new grid. + + Returns + ------- + Grid + The new (pending) 3D voxel Grid. + + Raises + ------ + ValueError + If only one of ``horizontal_resolution_m`` / + ``vertical_resolution_m`` is given, or the inventory is not + completed. + + Examples + -------- + >>> inventory.wait() + >>> voxels = inventory.voxelize( + ... horizontal_resolution_m=2.0, vertical_resolution_m=1.0 + ... ) + >>> voxels.wait() + """ + # Lazy import so the modules stay decoupled (grids never imports + # inventories). + from fastfuels_sdk.v2.client_library.api.grids import ( + create_tree_inventory_grid, + ) + from fastfuels_sdk.v2.grids import Grid + + self._require_completed("voxelize") + if (horizontal_resolution_m is None) != (vertical_resolution_m is None): + raise ValueError( + "horizontal_resolution_m and vertical_resolution_m must be " + "given together." + ) + if horizontal_resolution_m is not None: + resolution = Resolution3D( + horizontal=horizontal_resolution_m, vertical=vertical_resolution_m + ) + else: + resolution = UNSET + request_body = CreateTreeInventoryRequest( + source_inventory_id=self.id, + resolution=resolution, + bands=_enum_list(bands, TreeBand), + crown_profile_model=( + CrownProfileModel(crown_profile_model) + if crown_profile_model is not None + else UNSET + ), + biomass_source=_opt(biomass_source), + max_crown_radius_source=_opt(max_crown_radius_source), + moisture_model=_opt(moisture_model), + seed=_opt(seed), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_tree_inventory_grid.sync_detailed( + self.domain_id, client=ensure_client(), body=request_body + ) + return Grid._from_model(expect(response, HTTPStatus.CREATED)) + + def export( + self, + format: str = "parquet", + columns: Optional[List[str]] = None, + expiration_days: int = 7, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + ): + """Export this inventory to a downloadable file format. + + Parameters + ---------- + format : str, optional + Export format: "parquet" (zipped, default), "csv", "geojson", + or "geopackage". + columns : List[str], optional + Columns to include (default: all columns). + expiration_days : int, optional + Days until the signed download URL expires (max 7, default 7). + name, description : str, optional + Metadata for the export. + tags : List[str], optional + Tags for the export. + + Returns + ------- + Export + The created Export object (job status "pending"). Call + :meth:`Export.wait` and then :meth:`Export.to_file` to download + the file. + + Raises + ------ + NotFoundException + If the inventory no longer exists. + UnprocessableEntityException + If the inventory is not in "completed" status. + """ + # Lazy import so the modules stay decoupled (exports never imports + # inventories). + from fastfuels_sdk.v2.exports import Export + + request_body = ExportInventoryRequest( + columns=_opt(columns), + expiration_days=expiration_days, + name=name, + description=description, + tags=_opt(tags), + ) + response = create_inventory_export.sync_detailed( + self.domain_id, + self.id, + InventoryExportFormat(format), + client=ensure_client(), + body=request_body, + ) + return Export._from_model(expect(response, HTTPStatus.CREATED)) + + def get_data_metadata(self) -> InventoryDataMetadata: + """Get the partition layout of the inventory's tree records. + + The tree records are served in fixed-size partitions. Use this to + discover how many partitions exist before retrieving them with + :meth:`get_data_partition` (or call :meth:`to_dataframe` to + retrieve everything at once). + + Returns + ------- + InventoryDataMetadata + With attributes ``num_partitions``, ``total_rows``, ``columns`` + (column names), and ``partitions`` (per-partition row counts). + An inventory with no data has ``num_partitions`` 0. + + Raises + ------ + NotFoundException + If the inventory no longer exists. + UnprocessableEntityException + If the inventory is not in "completed" status. + + Examples + -------- + >>> metadata = inventory.get_data_metadata() + >>> metadata.num_partitions + 1 + """ + response = get_inventory_data_metadata.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return expect(response) + + def column_summary(self, column: str): + """Return summary statistics for one column without downloading records. + + The server computes a per-column summary when an inventory completes, + so this provides a cheap overview without fetching the inventory's tree + records (unlike :meth:`to_dataframe`). + + Parameters + ---------- + column : str + The column key to summarize (see :attr:`columns` for available + keys). + + Returns + ------- + ContinuousColumnSummary or CategoricalColumnSummary or None + The column's summary, discriminated by its ``type_``: + ``"continuous"`` carries ``count``, ``null_count``, ``min_``, + ``max_``, ``mean``, and ``std``; ``"categorical"`` carries + ``count``, ``null_count``, and ``unique_count``. ``None`` until + the inventory completes (call :meth:`wait` first). + + Raises + ------ + ValueError + If ``column`` is not one of the inventory's columns. + + Examples + -------- + >>> inventory = ff.get_inventory(domain, "abc123").wait() + >>> inventory.column_summary("dbh").type_ + 'continuous' + """ + summary = self._column(column).summary + return None if summary is UNSET else summary + + def get_data_partition( + self, + partition_index: int, + columns: Optional[List[str]] = None, + json_orientation: str = "split", + ) -> InventoryDataResponse: + """Get one partition of the inventory's tree records. + + Parameters + ---------- + partition_index : int + Zero-indexed partition number. Must be less than the + ``num_partitions`` reported by :meth:`get_data_metadata`. + columns : List[str], optional + Column subset to retrieve (default: all columns). + json_orientation : {"split", "records"}, optional + JSON layout. ``"split"`` (default) returns rows as lists of + values in ``partition.columns`` order; ``"records"`` returns + self-describing row mappings. + + Returns + ------- + InventoryDataResponse + With attributes ``partition``, ``num_rows``, ``columns`` + (column names), and ``data`` (row lists for ``"split"`` or row + mappings for ``"records"``). + + Raises + ------ + NotFoundException + If the inventory no longer exists. + UnprocessableEntityException + If ``partition_index`` is past the last partition, or the + inventory is not in "completed" status. + ValueError + If ``json_orientation`` is not ``"split"`` or ``"records"``. + + Examples + -------- + >>> partition = inventory.get_data_partition(0) + >>> partition.num_rows + 1392 + """ + orientation = InventoryJsonOrientation(json_orientation) + response = get_inventory_data_json.sync_detailed( + self.domain_id, + self.id, + partition_index, + client=ensure_client(), + json_orientation=orientation, + columns=",".join(columns) if columns is not None else UNSET, + ) + return expect(response) + + def to_dataframe(self, columns: Optional[List[str]] = None) -> pd.DataFrame: + """Retrieve the inventory's tree records as a pandas DataFrame. + + Retrieves every partition as CSV, parses it with :func:`pandas.read_csv`, + and concatenates the partitions into a single DataFrame with one row + per tree, preserving source order. + + Parameters + ---------- + columns : List[str], optional + Column subset to retrieve (default: all columns). + + Returns + ------- + pd.DataFrame + DataFrame with one row per tree. Empty if the inventory + contains no trees. + + Raises + ------ + NotFoundException + If the inventory no longer exists. + UnprocessableEntityException + If the inventory is not in "completed" status. + + Examples + -------- + >>> inventory.wait() + >>> trees = inventory.to_dataframe() + """ + metadata = self.get_data_metadata() + frames = [] + for partition_index in range(metadata.num_partitions): + response = get_inventory_data_csv.sync_detailed( + self.domain_id, + self.id, + partition_index, + client=ensure_client(), + columns=",".join(columns) if columns is not None else UNSET, + ) + frames.append(pd.read_csv(StringIO(expect(response)))) + if not frames: + return pd.DataFrame(columns=columns or metadata.columns) + return pd.concat(frames, ignore_index=True) + + def to_json(self) -> str: + """Serialize the complete Inventory object to a JSON string. + + Returns + ------- + str + The Inventory as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + +# --------------------------------------------------------------------------- +# Create inventories (module-level functions) +# --------------------------------------------------------------------------- + + +def create_tree_inventory_from_pim_grid( + domain, + pim_grid, + seed: Optional[int] = None, + point_process: Optional[str] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, + treatments: Optional[list] = None, +) -> Inventory: + """Create a tree inventory by expanding a PIM (TreeMap) grid. + + Each pixel of the completed PIM grid imputes an FIA plot; expansion + generates individual tree records from those plots and assigns each + tree a coordinate with a spatial point process. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the inventory in. + pim_grid : Grid or str + A completed PIM grid (or its id) to expand. See + :func:`fastfuels_sdk.v2.grids.create_pim_grid_from_treemap`. + seed : int, optional + Random seed for reproducibility. Generated randomly if omitted. + point_process : str, optional + Spatial point process for tree coordinate assignment + (``PointProcess`` member or its string key, e.g. + "inhomogeneous_poisson"). + name, description : str, optional + Metadata for the inventory. + tags : List[str], optional + Tags for the inventory. + modifications : list, optional + Modification rules (``InventoryModification``) applied after + expansion. + treatments : list, optional + Silvicultural treatments (``InventoryBasalAreaTreatment`` or + ``InventoryDiameterTreatment``) applied after modifications. + + Returns + ------- + Inventory + The created Inventory object (job status "pending" or "running"). + + Examples + -------- + >>> import fastfuels_sdk as ff + >>> pim = ff.grids.create_pim_grid_from_treemap(domain) + >>> inventory = ff.inventories.create_tree_inventory_from_pim_grid( + ... domain, pim.wait(), seed=42 + ... ) + >>> inventory.wait() + """ + request_body = CreatePimInventoryRequest( + source_pim_grid_id=_domain_id(pim_grid), + seed=_opt(seed), + point_process=( + PointProcess(point_process) if point_process is not None else UNSET + ), + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + treatments=_opt(treatments), + ) + response = create_pim_inventory.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Inventory._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_tree_inventory_from_chm_grid( + domain, + chm_grid, + algorithm=None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, + modifications: Optional[list] = None, + treatments: Optional[list] = None, +) -> Inventory: + """Create a tree inventory by isolating stems in a canopy height model. + + Detects individual trees in a completed canopy height model (CHM) + grid and derives a tree record from each detected stem. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the inventory in. + chm_grid : Grid or str + A completed canopy height model grid (or its id). See + :func:`fastfuels_sdk.v2.grids.create_canopy_height_grid_from_meta` + and + :func:`fastfuels_sdk.v2.grids.create_canopy_height_grid_from_naip_chm`. + algorithm : StemIsolationLmf or StemIsolationVwf, optional + Stem isolation algorithm and its parameters: local maximum + filtering (``StemIsolationLmf``) or variable window filtering + (``StemIsolationVwf``). Defaults to the API's standard algorithm. + name, description : str, optional + Metadata for the inventory. + tags : List[str], optional + Tags for the inventory. + modifications : list, optional + Modification rules (``InventoryModification``) applied after stem + isolation. + treatments : list, optional + Silvicultural treatments (``InventoryBasalAreaTreatment`` or + ``InventoryDiameterTreatment``) applied after modifications. + + Returns + ------- + Inventory + The created Inventory object (job status "pending" or "running"). + + Examples + -------- + >>> import fastfuels_sdk as ff + >>> chm = ff.grids.create_canopy_height_grid_from_meta(domain) + >>> inventory = ff.inventories.create_tree_inventory_from_chm_grid( + ... domain, chm.wait() + ... ) + >>> inventory.wait() + """ + request_body = CreateChmInventoryRequest( + source_chm_grid_id=_domain_id(chm_grid), + algorithm=_opt(algorithm), + name=name, + description=description, + tags=_opt(tags), + modifications=_opt(modifications), + treatments=_opt(treatments), + ) + response = create_chm_inventory.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Inventory._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_tree_inventory_from_file( + domain, + path: str, + format: Optional[str] = None, + columns=None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Inventory: + """Create a tree inventory by uploading your own tree records. + + Creates the inventory resource, uploads the file to the returned + signed URL, and returns the (pending) Inventory. Coordinates must be + in the domain's CRS. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the inventory in. + path : str + Path to the local file: ``.csv``, ``.geojson``/``.json``, or + ``.gpkg``. + format : str, optional + Upload format: "csv", "geojson", or "geopackage". Inferred from + the file extension if omitted. + columns : dict or InventoryColumnMapping, optional + Mapping from the standard column roles to your file's column + names, e.g. ``{"x": "X_UTM", "y": "Y_UTM", "dbh": "DBH_CM"}``. + Roles: "x", "y", "height", "dbh", "crown_ratio", + "fia_species_code", "fia_status_code". Columns whose names + already match need no mapping. + name, description : str, optional + Metadata for the inventory. + tags : List[str], optional + Tags for the inventory. + + Returns + ------- + Inventory + The created Inventory object (job status "pending"). Call + :meth:`Inventory.wait` to block until the uploaded file is + processed. + + Raises + ------ + ValueError + If ``format`` is omitted and the file extension is not one of + ``.csv``, ``.geojson``, ``.json``, or ``.gpkg``. + + Examples + -------- + >>> import fastfuels_sdk as ff + >>> inventory = ff.inventories.create_tree_inventory_from_file( + ... domain, "plot_trees.csv", columns={"x": "X_UTM", "y": "Y_UTM"} + ... ) + >>> inventory.wait() + """ + if format is not None: + upload_format = InventoryUploadFormat(format) + else: + suffix = Path(path).suffix.lower() + if suffix not in _UPLOAD_FORMATS: + raise ValueError( + f"Cannot infer the upload format from {suffix!r}. Pass " + 'format="csv", "geojson", or "geopackage".' + ) + upload_format = _UPLOAD_FORMATS[suffix] + if isinstance(columns, dict): + columns = InventoryColumnMapping(**columns) + request_body = CreateInventoryUploadRequest( + format_=upload_format, + columns=_opt(columns), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_inventory_upload.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + created = expect(response, HTTPStatus.CREATED) + put_upload(created.upload, path) + return Inventory._from_model(created.inventory) + + +def create_tree_inventory_from_gdam( + domain, + source_inventory, + impute_columns: Optional[list] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> Inventory: + """Create a tree inventory by imputing morphology with GDAM allometry. + + GDAM fills in missing per-tree morphology — diameter at breast height, + crown ratio, and FIA species code — for a completed source tree inventory + (for example one uploaded with coordinates and heights only). Existing + values are preserved; only missing cells are imputed. The result is a new + inventory; the source is left unchanged. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the inventory in. + source_inventory : Inventory or str + A completed tree inventory (or its id) whose missing morphology + columns GDAM will fill in. + impute_columns : list, optional + Which morphology columns to impute + (``CreateGdamInventoryRequestImputeColumnsItem`` members or their + string keys: "dbh", "crown_ratio", "fia_species_code"). Defaults to all + three. Narrow it to impute fewer columns; columns left out keep the + source's values. + name, description : str, optional + Metadata for the inventory. + tags : List[str], optional + Tags for the inventory. + + Returns + ------- + Inventory + The created Inventory object (job status "pending"). Call + :meth:`Inventory.wait` to block until imputation finishes. + + Examples + -------- + >>> import fastfuels_sdk as ff + >>> sparse = ff.inventories.create_tree_inventory_from_file( + ... domain, "stems.csv" # x, y, height only + ... ) + >>> sparse.wait() + >>> full = ff.inventories.create_tree_inventory_from_gdam(domain, sparse) + >>> full.wait() + """ + request_body = CreateGdamInventoryRequest( + source_tree_inventory_id=_domain_id(source_inventory), + impute_columns=_enum_list( + impute_columns, CreateGdamInventoryRequestImputeColumnsItem + ), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_gdam_inventory.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + return Inventory._from_model(expect(response, HTTPStatus.CREATED)) + + +# --------------------------------------------------------------------------- +# Top-level fetch / list helpers +# --------------------------------------------------------------------------- + + +def list_inventories( + domain=None, + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + source: Optional[str] = None, + tag: Optional[str] = None, +) -> List[Inventory]: + """List inventories in a domain, or across all domains (single page). + + Parameters + ---------- + domain : Domain or str, optional + The domain (or its id) to list inventories in. If omitted, + inventories from all the user's domains are listed. + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of inventories per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + source : str, optional + Only return inventories from this source (e.g. "pim"). + tag : str, optional + Only return inventories carrying this tag. + + Returns + ------- + List[Inventory] + The requested page of Inventory objects. + """ + kwargs = dict( + client=ensure_client(), + page=page, + size=size, + sort_by=InventorySortField(sort_by) if sort_by else UNSET, + sort_order=SortOrder(sort_order) if sort_order else UNSET, + source=_opt(source), + tag=_opt(tag), + ) + if domain is None: + response = list_inventories_cross_domain.sync_detailed(**kwargs) + else: + response = list_inventories_endpoint.sync_detailed(_domain_id(domain), **kwargs) + list_response: ListInventoriesResponse = expect(response) + return [Inventory._from_model(i) for i in list_response.inventories] + + +def get_inventory(domain, inventory_id: str) -> Inventory: + """Retrieve a single inventory by its ID. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the inventory belongs to. + inventory_id : str + The unique identifier of the inventory. + + Returns + ------- + Inventory + The requested Inventory object. + + Raises + ------ + NotFoundException + If no inventory exists with the given IDs, or the user does not have + access. + """ + return Inventory.from_id(_domain_id(domain), inventory_id) diff --git a/fastfuels_sdk/v2/modifications.py b/fastfuels_sdk/v2/modifications.py new file mode 100644 index 0000000..ab4143a --- /dev/null +++ b/fastfuels_sdk/v2/modifications.py @@ -0,0 +1,291 @@ +""" +fastfuels_sdk/v2/modifications.py + +Modification primitives for grids and tree inventories. + +A modification is a rule applied to a resource: it pairs *conditions* (spatial +or value tests, all ANDed) with *actions* applied to whatever the conditions +select. ``mask`` builds a grid modification (the v2 replacement for v1's +``feature_masks``); ``tree_attribute`` / ``tree_within`` / ``remove_trees`` / +``modify_trees`` build inventory modifications so callers don't hand-assemble +the generated models. +""" + +from typing import List, Optional, Union + +from fastfuels_sdk.v2.client_library.models import ( + GridFeatureSpatialCondition, + GridModification, + GridModificationAction, + GridSpatialTarget, + InventoryAttribute, + InventoryFeatureSpatialCondition, + InventoryModification, + InventoryModificationAction, + InventoryModificationCondition, + Modifier, + Operator, + RemoveAction, + SpatialOperator, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +__all__ = [ + "mask", + "tree_attribute", + "tree_within", + "remove_trees", + "modify_trees", +] + +# Comparison operators accept both their enum names and the symbolic forms. +_OPERATORS = { + "<": Operator.LT, + "lt": Operator.LT, + "<=": Operator.LE, + "le": Operator.LE, + ">": Operator.GT, + "gt": Operator.GT, + ">=": Operator.GE, + "ge": Operator.GE, + "==": Operator.EQ, + "eq": Operator.EQ, + "!=": Operator.NE, + "ne": Operator.NE, +} + + +def _feature_id(feature) -> str: + """Accept a ``Feature`` record or a bare id string.""" + return getattr(feature, "id", feature) + + +def _operator(operator) -> Operator: + """Coerce a symbolic ("<") or named ("lt") operator, or enum member.""" + if isinstance(operator, Operator): + return operator + try: + return _OPERATORS[operator] + except (KeyError, TypeError): + raise ValueError( + f"Unknown operator {operator!r}. Use one of " + f"{sorted(_OPERATORS)} or an Operator member." + ) + + +def mask( + feature, + band: Union[str, List[str]], + value: Union[float, int, str] = 0.0, + *, + operator: str = "within", + buffer_m: Optional[float] = None, + target: Optional[str] = None, +) -> GridModification: + """Build a modification that overwrites a band where cells fall within a feature. + + Pass a completed feature (road, water, or layerset) in the same domain as + the grid, and every grid cell selected by the spatial ``operator`` has + ``band`` replaced with ``value``. The returned modification is suitable for + the ``modifications=`` argument of any grid creator. + + Parameters + ---------- + feature : Feature or str + The feature to mask against, or its id. Must be ``completed`` and + belong to the same domain as the grid being modified. + band : str or list of str + The band(s) to overwrite, using dot-notation keys (e.g. ``"fbfm"``, + ``"fuel_load.1hr"``). A list applies the same value to every band. + value : float, int, or str, default 0.0 + The replacement value written to ``band`` for matching cells. + operator : {"within", "intersects", "outside"}, default "within" + Which cells to select relative to the feature geometry. + buffer_m : float, optional + Buffer applied to the feature geometry, in meters in the domain's + projected CRS, before testing. Linestring features such as roads + usually need a buffer — or ``target="cell"`` — to catch the cells they + cross. Defaults to no buffer. + target : {"centroid", "cell"}, optional + Which part of each grid cell is tested against the geometry. Defaults + to the API default (``"centroid"``). Use ``"cell"`` to select every + cell a geometry touches. + + Returns + ------- + GridModification + A modification to pass in a creator's ``modifications=`` list. + + Examples + -------- + Mask roads to a non-burnable fuel model code, buffering the linestrings so + they cover whole cells: + + >>> import fastfuels_sdk.v2 as ff + >>> roads = ff.features.create_road_feature_from_osm(domain) + >>> roads.wait() + >>> grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40( + ... domain, + ... output_resolution_m=30, + ... modifications=[ff.mask(roads, "fbfm", 91, buffer_m=5)], + ... ) + """ + bands = [band] if isinstance(band, str) else list(band) + condition = GridFeatureSpatialCondition( + source="feature", + operator=SpatialOperator(operator), + feature_id=_feature_id(feature), + buffer_m=buffer_m if buffer_m is not None else UNSET, + target=GridSpatialTarget(target) if target is not None else UNSET, + ) + actions = [ + GridModificationAction(band=b, modifier=Modifier.REPLACE, value=value) + for b in bands + ] + return GridModification(conditions=[condition], actions=actions) + + +# --------------------------------------------------------------------------- +# Tree-inventory modifications +# --------------------------------------------------------------------------- +# +# An inventory modification pairs conditions (ANDed) with actions applied to +# the matching trees. ``tree_attribute`` / ``tree_within`` build conditions; +# ``remove_trees`` / ``modify_trees`` assemble the modification. Pass the result +# to a tree-inventory creator's ``modifications=`` argument or to +# ``Inventory.apply_modifications``. + + +def tree_attribute(attribute, operator, value, *, unit: Optional[str] = None): + """Build a condition testing a per-tree attribute. + + Parameters + ---------- + attribute : str + The tree attribute to test: "dbh", "height", "crown_ratio", or + "fia_species_code" (an ``InventoryAttribute`` member is also accepted). + operator : str + A comparison: ``"<"``, ``"<="``, ``">"``, ``">="``, ``"=="``, ``"!="`` + (or the names ``"lt"``, ``"le"``, ``"gt"``, ``"ge"``, ``"eq"``, + ``"ne"``; an ``Operator`` member is also accepted). + value : float, int, str, or list + The value to compare against. + unit : str, optional + Unit of ``value`` if not the attribute's default. + + Returns + ------- + InventoryModificationCondition + A condition for ``remove_trees`` / ``modify_trees``. + """ + return InventoryModificationCondition( + attribute=_tree_attribute(attribute), + operator=_operator(operator), + value=value, + unit=unit if unit is not None else UNSET, + ) + + +def tree_within(feature, *, buffer_m: Optional[float] = None, operator: str = "within"): + """Build a condition selecting trees by their location relative to a feature. + + Parameters + ---------- + feature : Feature or str + The feature to test against, or its id. Must be ``completed`` and in + the same domain as the inventory. + buffer_m : float, optional + Buffer applied to the feature geometry, in meters in the domain's + projected CRS, before testing. + operator : {"within", "intersects", "outside"}, default "within" + Which trees to select relative to the feature geometry. + + Returns + ------- + InventoryFeatureSpatialCondition + A condition for ``remove_trees`` / ``modify_trees``. + """ + return InventoryFeatureSpatialCondition( + source="feature", + operator=SpatialOperator(operator), + feature_id=_feature_id(feature), + buffer_m=buffer_m if buffer_m is not None else UNSET, + ) + + +def remove_trees(*conditions) -> InventoryModification: + """Build a modification that removes the trees matching every condition. + + Parameters + ---------- + *conditions + One or more conditions from :func:`tree_attribute` / :func:`tree_within` + (ANDed). At least one is required. + + Returns + ------- + InventoryModification + For a creator's ``modifications=`` list or + :meth:`Inventory.apply_modifications`. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> inventory.apply_modifications([ff.remove_trees(ff.tree_attribute("dbh", "<", 10))]) + """ + if not conditions: + raise ValueError("remove_trees requires at least one condition.") + return InventoryModification(conditions=list(conditions), actions=[RemoveAction()]) + + +def modify_trees( + attribute, modifier, value, *conditions, unit: Optional[str] = None +) -> InventoryModification: + """Build a modification that changes an attribute on the matching trees. + + Parameters + ---------- + attribute : str + The attribute to change ("dbh", "height", "crown_ratio", + "fia_species_code"; an ``InventoryAttribute`` member is also accepted). + modifier : str + How to change it: "replace", "add", "subtract", "multiply", or + "divide" (a ``Modifier`` member is also accepted). To drop trees, use + :func:`remove_trees` instead. + value : float, int, or str + The operand for ``modifier``. + *conditions + One or more conditions from :func:`tree_attribute` / :func:`tree_within` + (ANDed). At least one is required. + unit : str, optional + Unit of ``value`` if not the attribute's default. + + Returns + ------- + InventoryModification + For a creator's ``modifications=`` list or + :meth:`Inventory.apply_modifications`. + + Examples + -------- + >>> import fastfuels_sdk.v2 as ff + >>> inventory.apply_modifications([ + ... ff.modify_trees("height", "multiply", 0.9, ff.tree_attribute("dbh", ">", 0)) + ... ]) + """ + if not conditions: + raise ValueError("modify_trees requires at least one condition.") + action = InventoryModificationAction( + attribute=_tree_attribute(attribute), + modifier=Modifier(modifier), + value=value, + unit=unit if unit is not None else UNSET, + ) + return InventoryModification(conditions=list(conditions), actions=[action]) + + +def _tree_attribute(attribute) -> InventoryAttribute: + """Coerce a string or enum member to an ``InventoryAttribute``.""" + if isinstance(attribute, InventoryAttribute): + return attribute + return InventoryAttribute(attribute) diff --git a/fastfuels_sdk/v2/point_clouds.py b/fastfuels_sdk/v2/point_clouds.py new file mode 100644 index 0000000..a29c332 --- /dev/null +++ b/fastfuels_sdk/v2/point_clouds.py @@ -0,0 +1,478 @@ +""" +fastfuels_sdk/v2/point_clouds.py +""" + +# Core imports +import json +from http import HTTPStatus +from typing import List, Optional + +# Internal imports +from fastfuels_sdk.v2._jobs import wait as _wait +from fastfuels_sdk.v2._uploads import put_upload +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.exceptions import expect +from fastfuels_sdk.v2.client_library.api.point_clouds import ( + check_3dep_point_cloud_coverage, + create_3dep_point_cloud as create_3dep_point_cloud_endpoint, + create_point_cloud_upload, + delete_point_cloud, + get_point_cloud as get_point_cloud_endpoint, + list_point_clouds as list_point_clouds_endpoint, + list_point_clouds_cross_domain, + update_point_cloud, +) +from fastfuels_sdk.v2.client_library.models import ( + PointCloud as PointCloudModel, + CreatePointCloudUploadRequest, + CreateThreeDepPointCloudRequest, + ListPointCloudsResponse, + PointCloudThreeDepCoverageResponse, + PointCloudSortField, + PointCloudType, + SortOrder, + UpdatePointCloudRequestBody, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +# External imports +import attrs + +__all__ = [ + "PointCloud", + "check_3dep_coverage", + "create_point_cloud_from_3dep", + "create_point_cloud_from_file", + "list_point_clouds", + "get_point_cloud", +] + + +def _domain_id(domain) -> str: + """Resolve a Domain object or a domain-id string to the id string.""" + return getattr(domain, "id", domain) + + +def _opt(value): + """Map ``None`` to the generated UNSET sentinel, else pass through.""" + return value if value is not None else UNSET + + +def _point_cloud_type(value) -> PointCloudType: + """Coerce a string or enum member to a ``PointCloudType``.""" + return value if isinstance(value, PointCloudType) else PointCloudType(value) + + +class PointCloud(PointCloudModel): + """Point cloud resource for the FastFuels v2 API. + + A point cloud is a 3D LiDAR dataset within a domain, either fetched from + USGS 3DEP or uploaded from your own ALS (airborne) or TLS (terrestrial) + scan. Point clouds are asynchronous job resources — creation starts a + background job and returns a *pending* record; call :meth:`wait` to block + until the data is processed. + + Attributes + ---------- + id : str + Unique identifier for the point cloud. + domain_id : str + Identifier of the domain the point cloud belongs to. + type_ : PointCloudType + Scan type: "als" (airborne) or "tls" (terrestrial). + status : JobStatus + Job status: "pending", "running", "completed", or "failed". + source : PointCloudSource + Where the point cloud data comes from. + name : str + Human-readable name for the point cloud. + description : str + Detailed description of the point cloud. + progress : JobProgress, optional + Progress info while the job is running. + checksum : str, optional + Version marker for the point cloud's content; changes each time the + data is rebuilt, unaffected by metadata-only edits. + georeference : PointCloudGeoreference, optional + Spatial reference (CRS and bounds); populated when the job completes. + summary : PointCloudSummary, optional + Summary statistics of the points; populated when the job completes. + error : JobError, optional + Error details if the job failed. + tags : List[str], optional + User-defined tags for organization. + created_on : datetime + When the point cloud was created. + modified_on : datetime + When the point cloud was last modified. + + Examples + -------- + Upload a point cloud and wait for it to process: + >>> import fastfuels_sdk.v2 as ff + >>> pc = ff.point_clouds.create_point_cloud_from_file( + ... domain, "scan.laz", point_cloud_type="als" + ... ) + >>> pc.wait() + + Get a point cloud by ID: + >>> pc = ff.get_point_cloud(domain, "abc123") + + See Also + -------- + create_point_cloud_from_3dep : Fetch public airborne LiDAR from USGS 3DEP. + create_point_cloud_from_file : Upload a local LiDAR file. + list_point_clouds : List point clouds in a domain or across all domains. + """ + + @classmethod + def _from_model(cls, model: PointCloudModel) -> "PointCloud": + """Build a PointCloud from a generated PointCloud model instance. + + Round-trips through the generated to_dict/from_dict — from_dict + constructs ``cls``, i.e. this subclass. + """ + return cls.from_dict(model.to_dict()) + + def _copy_fields_from(self, model: PointCloudModel) -> "PointCloud": + """Copy all generated-model fields from `model` onto self (in-place).""" + for field in attrs.fields(PointCloudModel): + if field.init: + setattr(self, field.name, getattr(model, field.name)) + self.additional_properties = dict(model.additional_properties) + return self + + @classmethod + def from_id(cls, domain_id: str, point_cloud_id: str) -> "PointCloud": + """Retrieve an existing PointCloud resource by its ID. + + Parameters + ---------- + domain_id : str + The unique identifier of the domain the point cloud belongs to. + point_cloud_id : str + The unique identifier of the point cloud to retrieve. + + Returns + ------- + PointCloud + The requested PointCloud object. + + Raises + ------ + NotFoundException + If no point cloud exists with the given IDs, or the user does not + have access to it. + """ + response = get_point_cloud_endpoint.sync_detailed( + domain_id, point_cloud_id, client=ensure_client() + ) + return cls._from_model(expect(response)) + + def refresh(self) -> "PointCloud": + """Update this PointCloud in place with the latest data from the API. + + Returns + ------- + PointCloud + ``self``, updated with the latest data (so calls chain). + + Raises + ------ + NotFoundException + If the point cloud no longer exists. + """ + response = get_point_cloud_endpoint.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + return self._copy_fields_from(expect(response)) + + def wait( + self, timeout: Optional[float] = None, verbose: bool = False + ) -> "PointCloud": + """Poll the point cloud job until it reaches a terminal status. + + Parameters + ---------- + timeout : float, optional + Maximum seconds to wait. ``None`` (default) waits indefinitely; the + job runs server-side regardless, so a bounded wait is resumable. + verbose : bool, optional + If True, print the job status at each poll. + + Returns + ------- + PointCloud + ``self``, updated to its terminal state (so calls chain). + + Raises + ------ + TimeoutError + If ``timeout`` is set and elapses before a terminal status. + JobFailedError + If the job finished with status "failed". + """ + return _wait(self, timeout=timeout, verbose=verbose) + + def update( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> "PointCloud": + """Update the point cloud's metadata (name, description, tags) in place. + + Only provided fields are sent. If no fields are provided, no API call + is made. + + Parameters + ---------- + name : str, optional + New name for the point cloud. + description : str, optional + New description for the point cloud. + tags : List[str], optional + New tags for the point cloud (replaces existing tags). + + Returns + ------- + PointCloud + ``self``, updated (so calls chain). + + Raises + ------ + NotFoundException + If the point cloud no longer exists. + """ + if name is None and description is None and tags is None: + return self + request_body = UpdatePointCloudRequestBody( + name=_opt(name), description=_opt(description), tags=_opt(tags) + ) + response = update_point_cloud.sync_detailed( + self.domain_id, self.id, client=ensure_client(), body=request_body + ) + return self._copy_fields_from(expect(response)) + + def delete(self) -> None: + """Delete this point cloud and its data. + + Raises + ------ + NotFoundException + If the point cloud no longer exists. + """ + response = delete_point_cloud.sync_detailed( + self.domain_id, self.id, client=ensure_client() + ) + expect(response, HTTPStatus.NO_CONTENT) + + def to_json(self) -> str: + """Serialize the complete PointCloud object to a JSON string. + + Returns + ------- + str + The PointCloud as a pretty-printed JSON string. + """ + return json.dumps(self.to_dict(), default=str, indent=2) + + +# --------------------------------------------------------------------------- +# Create point clouds +# --------------------------------------------------------------------------- + + +def check_3dep_coverage(domain) -> PointCloudThreeDepCoverageResponse: + """Check USGS 3DEP LiDAR coverage before creating a point cloud. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to check. + + Returns + ------- + PointCloudThreeDepCoverageResponse + Availability, coverage fraction, estimated point count and budget, + and the contributing acquisitions. + """ + response = check_3dep_point_cloud_coverage.sync_detailed( + _domain_id(domain), + client=ensure_client(), + ) + return expect(response) + + +def create_point_cloud_from_3dep( + domain, + datasets: Optional[List[str]] = None, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> PointCloud: + """Create an airborne point cloud from USGS 3DEP LiDAR. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the point cloud in. + datasets : List[str], optional + Acquisition names to read, in priority order. Use + :func:`check_3dep_coverage` to discover names. If omitted, the API + selects acquisitions automatically. + name, description : str, optional + Metadata for the point cloud. + tags : List[str], optional + Tags for the point cloud. + + Returns + ------- + PointCloud + The created airborne PointCloud (job status "pending" or "running"). + """ + request_body = CreateThreeDepPointCloudRequest( + datasets=_opt(datasets), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_3dep_point_cloud_endpoint.sync_detailed( + _domain_id(domain), + client=ensure_client(), + body=request_body, + ) + return PointCloud._from_model(expect(response, HTTPStatus.CREATED)) + + +def create_point_cloud_from_file( + domain, + path: str, + point_cloud_type: str, + name: str = "", + description: str = "", + tags: Optional[List[str]] = None, +) -> PointCloud: + """Create a point cloud by uploading a local LiDAR file. + + Creates the point cloud resource, uploads the file to the returned signed + URL, and returns the (pending) PointCloud. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) to create the point cloud in. + path : str + Path to the local LiDAR file (e.g. ``.las``/``.laz``). + point_cloud_type : str + Scan type: "als" (airborne) or "tls" (terrestrial). A + ``PointCloudType`` member is also accepted. + name, description : str, optional + Metadata for the point cloud. + tags : List[str], optional + Tags for the point cloud. + + Returns + ------- + PointCloud + The created PointCloud object (job status "pending"). Call + :meth:`PointCloud.wait` to block until the uploaded file is processed. + """ + request_body = CreatePointCloudUploadRequest( + type_=_point_cloud_type(point_cloud_type), + name=name, + description=description, + tags=_opt(tags), + ) + response = create_point_cloud_upload.sync_detailed( + _domain_id(domain), client=ensure_client(), body=request_body + ) + created = expect(response, HTTPStatus.CREATED) + put_upload(created.upload, path) + return PointCloud._from_model(created.point_cloud) + + +# --------------------------------------------------------------------------- +# Top-level fetch / list helpers +# --------------------------------------------------------------------------- + + +def list_point_clouds( + domain=None, + page: int = 0, + size: int = 100, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + point_cloud_type: Optional[str] = None, + source: Optional[str] = None, + tag: Optional[str] = None, +) -> List[PointCloud]: + """List point clouds in a domain, or across all domains (single page). + + Parameters + ---------- + domain : Domain or str, optional + The domain (or its id) to list point clouds in. If omitted, point + clouds from all the user's domains are listed. + page : int, optional + The page number to retrieve, zero-indexed (default 0). + size : int, optional + The number of point clouds per page (default 100). + sort_by : str, optional + Field to sort by: "name", "created_on", or "modified_on". + sort_order : str, optional + Sort direction: "ascending" or "descending". + point_cloud_type : str, optional + Only return point clouds of this scan type: "als" or "tls". + source : str, optional + Only return point clouds from this source. + tag : str, optional + Only return point clouds carrying this tag. + + Returns + ------- + List[PointCloud] + The requested page of PointCloud objects. + """ + kwargs = dict( + client=ensure_client(), + page=page, + size=size, + sort_by=PointCloudSortField(sort_by) if sort_by else UNSET, + sort_order=SortOrder(sort_order) if sort_order else UNSET, + type_=_point_cloud_type(point_cloud_type) if point_cloud_type else UNSET, + source=_opt(source), + tag=_opt(tag), + ) + if domain is None: + response = list_point_clouds_cross_domain.sync_detailed(**kwargs) + else: + response = list_point_clouds_endpoint.sync_detailed( + _domain_id(domain), **kwargs + ) + list_response: ListPointCloudsResponse = expect(response) + return [PointCloud._from_model(pc) for pc in list_response.point_clouds] + + +def get_point_cloud(domain, point_cloud_id: str) -> PointCloud: + """Retrieve a single point cloud by its ID. + + Parameters + ---------- + domain : Domain or str + The domain (or its id) the point cloud belongs to. + point_cloud_id : str + The unique identifier of the point cloud. + + Returns + ------- + PointCloud + The requested PointCloud object. + + Raises + ------ + NotFoundException + If no point cloud exists with the given IDs, or the user does not have + access. + """ + return PointCloud.from_id(_domain_id(domain), point_cloud_id) diff --git a/fastfuels_sdk/v2/treatments.py b/fastfuels_sdk/v2/treatments.py new file mode 100644 index 0000000..a3744b1 --- /dev/null +++ b/fastfuels_sdk/v2/treatments.py @@ -0,0 +1,130 @@ +""" +fastfuels_sdk/v2/treatments.py + +Treatment primitives for tree inventories. + +A treatment thins a tree inventory by removing stems until the stand reaches a +target — either a residual basal area or a diameter limit. Pass treatments to a +tree-inventory creator's ``treatments=`` argument, or to +:meth:`fastfuels_sdk.v2.inventories.Inventory.apply_treatments` on an inventory +you already hold. These builders construct the generated treatment models so +callers don't have to. + +The optional ``conditions`` argument restricts a treatment to a subarea +(``InventoryFeatureSpatialCondition`` / ``InventoryGeometrySpatialCondition``); +it is passed through as-is — there is no condition-builder vocabulary yet. +""" + +from typing import Optional + +from fastfuels_sdk.v2.client_library.models import ( + InventoryBasalAreaTreatment, + InventoryDiameterTreatment, + InventoryDiameterTreatmentMethod, + InventoryTreatmentMethod, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +__all__ = ["basal_area_treatment", "diameter_treatment"] + + +def basal_area_treatment( + method: str, + value: float, + *, + unit: Optional[str] = None, + conditions: Optional[list] = None, +) -> InventoryBasalAreaTreatment: + """Build a treatment that thins an inventory to a residual basal area. + + Parameters + ---------- + method : {"from_below", "from_above", "proportional"} + How stems are removed: "from_below" removes the smallest trees first, + "from_above" the largest first, "proportional" removes across all size + classes. An ``InventoryTreatmentMethod`` member is also accepted. + value : float + The residual basal area to thin to (default unit m**2/ha). + unit : str, optional + Unit of ``value`` if not the default. + conditions : list, optional + Spatial conditions restricting the treatment to a subarea + (``InventoryFeatureSpatialCondition`` / ``InventoryGeometrySpatialCondition``). + + Returns + ------- + InventoryBasalAreaTreatment + A treatment for a creator's ``treatments=`` list or + :meth:`Inventory.apply_treatments`. + + Examples + -------- + Thin from below to a residual basal area of 25 m**2/ha: + + >>> import fastfuels_sdk.v2 as ff + >>> inventory.apply_treatments([ff.basal_area_treatment("from_below", 25.0)]) + """ + return InventoryBasalAreaTreatment( + method=_basal_area_method(method), + value=value, + unit=unit if unit is not None else UNSET, + conditions=list(conditions) if conditions is not None else UNSET, + ) + + +def diameter_treatment( + method: str, + value: float, + *, + unit: Optional[str] = None, + conditions: Optional[list] = None, +) -> InventoryDiameterTreatment: + """Build a treatment that thins an inventory to a diameter limit. + + Parameters + ---------- + method : {"from_below", "from_above"} + "from_below" removes trees smaller than ``value`` (clears suppressed + understory stems); "from_above" removes those larger than ``value``. An + ``InventoryDiameterTreatmentMethod`` member is also accepted. + value : float + The diameter limit (default unit cm dbh). + unit : str, optional + Unit of ``value`` if not the default. + conditions : list, optional + Spatial conditions restricting the treatment to a subarea + (``InventoryFeatureSpatialCondition`` / ``InventoryGeometrySpatialCondition``). + + Returns + ------- + InventoryDiameterTreatment + A treatment for a creator's ``treatments=`` list or + :meth:`Inventory.apply_treatments`. + + Examples + -------- + Remove trees smaller than 10 cm dbh: + + >>> import fastfuels_sdk.v2 as ff + >>> inventory.apply_treatments([ff.diameter_treatment("from_below", 10.0)]) + """ + return InventoryDiameterTreatment( + method=_diameter_method(method), + value=value, + unit=unit if unit is not None else UNSET, + conditions=list(conditions) if conditions is not None else UNSET, + ) + + +def _basal_area_method(method) -> InventoryTreatmentMethod: + """Coerce a string or enum member to an ``InventoryTreatmentMethod``.""" + if isinstance(method, InventoryTreatmentMethod): + return method + return InventoryTreatmentMethod(method) + + +def _diameter_method(method) -> InventoryDiameterTreatmentMethod: + """Coerce a string or enum member to an ``InventoryDiameterTreatmentMethod``.""" + if isinstance(method, InventoryDiameterTreatmentMethod): + return method + return InventoryDiameterTreatmentMethod(method) diff --git a/fastfuels_sdk/v2/v2_api_design.md b/fastfuels_sdk/v2/v2_api_design.md new file mode 100644 index 0000000..b02b076 --- /dev/null +++ b/fastfuels_sdk/v2/v2_api_design.md @@ -0,0 +1,360 @@ +# v2 SDK — API Design (WORKING DRAFT) + +> Scratch design notes for the v2 SDK refactor (umbrella #176; grids = #178). +> **Temporary** — delete or fold into real docs before merge. +> Captures the surface settled in the 2026-06 design discussion. + +## Decision: hybrid — functional creation, method-based instances + +Two separate decisions, each resolved differently: + +1. **Creation lives in module-level functions.** Every class-home was rejected: + classmethod on the resource (`Feature.create_osm_road(domain.id)`, the + original complaint), method on `Domain` (God-class), method on a collection + object (`domain.grids.topography.from_3dep()`, the object tree). Functions + are what's left, and they read cold. +2. **Everything you do with a resource you already hold is a method** on the + returned record — `grid.wait()`, `grid.to_xarray()`, `trees.voxelize(...)`, + `grid.resample(...)`, `grid.delete()`. Methods read naturally, chain, answer + "what can I do with this?" via `grid.`, and are mostly what the shipped + class-based `domains.py`/`features.py` already have — so the migration is + "move creation off the classes into functions," not a rewrite, and there's + no split-brain. + +The rule a user learns: **don't have the resource yet → function from the +domain; have it → method on it.** + +Rejected wholesale: fully object-navigation (`domain.grids.topography.…`), +fully functional (free `ff.wait(grid)` / `ff.to_xarray(grid)` — loses chaining +and the shipped methods), typed spec objects, fluent builders, a "fuelscape" +façade. + +## Access pattern (locked) + +One import; reach everything through the package namespace (`np`/`pd`/`gpd` +idiom): + +```python +import fastfuels_sdk as ff +``` + +- **Creators are module-qualified functions:** + `ff.grids.create_topography_grid_from_3dep(domain, …)`, + `ff.features.create_road_feature_from_osm(domain)`. Scoped tab-completion: + `ff.grids.`. +- **Held-resource ops are methods:** `grid.wait()`, `grid.to_xarray()`, + `trees.voxelize(…)`, `grid.resample(…)`, `grid.export(…)`, `grid.delete()`. +- **Cross-cutting helpers are top-level functions** (operate on many / build + descriptors / start from nothing): `ff.wait_all([...])`, `ff.mask(...)`, + `ff.list_grids(domain)`, `ff.list_all_grids()`, `ff.get_grid(domain, id)`, + `ff.Domain`, `ff.set_api_key`. (Fuller modifications vocab — `modify`, + `within`, `remove`, `thin_to_*` — lives in `ff.modifications`, least-settled.) +- `ff.` is a **module, not an object** — no God-class. Not the rejected + object tree. +- The resource noun stays **in the create-function name** (`…_grid_…`, + `…_feature_…`) even though the module repeats it, so it reads cold under a + bare import too. + +## Guiding principles + +1. **Read it cold** — one call, you know exactly what it does. No jargon, no + magic values, units in names (`_m`), real domain nouns, named arguments. +2. **The shape encodes where the input comes from:** `create___from_(domain, …)` + = external dataset; `create__from_(domain, path)` = your + file; `create_(domain, …)` = you supply values; `resource.()` + = transform something you hold. +3. **Resource noun in every `create_*` name** (`grid`/`feature`/`inventory`). +4. **Concrete kind nouns**, grounded in the grid's bands — never vague + umbrellas (`canopy` → `canopy_height` vs `canopy_fuel`). + +## Wait / job model (decision: A — explicit) + +API is async-job (create → poll → terminal). Surfaced explicitly: + +- **Creation never blocks, never auto-waits** — returns a *pending* record. + Lets you fan out and join (the v1 `export_roi` pattern). +- `grid.wait(timeout=None, verbose=False)` — method; blocks to terminal, + updates & returns self (chains). `timeout=None` waits indefinitely; the job + runs server-side regardless, so a bounded timeout is resumable. +- `ff.wait_all([...])` — function; join many, raises naming the first failure. +- **Deriving from a still-pending resource raises** a clear error. No hidden + block. +- Failure → `JobFailedError(code, message, suggestion)`. Progress quiet by + default; `verbose=True`. + +## Resolution & alignment (verified against the models) + +**v2 moved resolution off the domain and onto each grid** (v1 set it on the +domain). `Domain` has only `pad_to_resolution` (optional footprint snapping). +Per-creator: + +- **2D source/derive grids** (`topography×2`, `canopy_height×2`, `canopy_fuel`, + `fuel_model/fbfm40`, `resample`, `rasterize`) carry an **`alignment`** union. + All three targets also hold `resolution: float` (horizontal) and an optional + `method` (resampling). Friendly mapping: + - `output_resolution_m=N` → `target="domain"` (anchor to domain origin at N m) — default. + - `align_to=` → `target="grid"` (match that grid's lattice). + - `align="native"` → `target="native"` (keep source pixels). + - `resampling=` → `method`; one of `average / bilinear / cubic / + cubic_spline / lanczos / min / max / median / mode / first_quartile`. +- **`create_uniform_grid`**: direct `resolution: float` → `resolution_m=`. +- **`voxelize`** (3D): direct `resolution: Resolution3D = {horizontal, vertical}` + (horizontal isotropic x/y, vertical independent), **no alignment** → + `horizontal_resolution_m=` + `vertical_resolution_m=`. +- **`create_fuel_grid_from_fbfm40_lookup`, geotiff/netcdf upload**: no resolution/alignment — inherit + the source grid's lattice (lookup) or the file (uploads). +- **`landfire_fccs`** reached alignment parity with the other LANDFIRE creators + in FastFuels-API-v2 #358 (it now carries `alignment` + `extent_buffer_cells`), + resolving the earlier asymmetry. + +## Conventions + +- Every creator also accepts `name=`, `description=`, `tags=`, + `modifications=[...]` (applied server-side after build; `modifications` is a + real field on grid creators *and* the FBFM40 lookup / `rasterize`). +- **Data out (methods, hide chunk/partition plumbing + signed-URL handshake):** + `grid.to_xarray()`, `grid.to_numpy(band)`, `feature.to_geodataframe()`. +- **Generic methods on any record:** `.wait()`, `.refresh()`, `.update(...)`, + `.delete()`. + +## Module layout + +``` +fastfuels_sdk/ + __init__.py set_api_key, Domain, wait_all, mask, basal_area_treatment, list_*, get_*, ... + domains.py Domain (record + from_* + methods), list_domains + features.py create_*_feature_from_* (fns); Feature record + methods + grids.py create_*_grid_from_* (fns; incl. create_fuel_grid_from_fbfm40_lookup); + Grid record + methods (wait/to_xarray/resample/duplicate/ + apply_modifications/band_summary/export/...) + inventories.py create_tree_inventory_from_* (fns; pim/chm/file/gdam); Inventory + methods + (duplicate/apply_modifications/apply_treatments/voxelize/export/...) + point_clouds.py create_point_cloud_from_file (fn); PointCloud record + methods (upload-only) + exports.py create_quicfire_export (fn); Export record + methods + modifications.py mask() -> GridModification + treatments.py basal_area_treatment()/diameter_treatment() -> Inventory*Treatment + _uploads.py put_upload(spec, path) — shared signed-upload helper (grids/inventories/point_clouds) + _jobs.py wait()/wait_all()/JobFailedError +``` + +## Features — 10 endpoints (rewrite creation only; keep instance methods) + +| API endpoint | SDK surface | Kind | +|---|---|---| +| `create_osm_road_feature` | `ff.features.create_road_feature_from_osm(domain)` | fn | +| `create_osm_water_feature` | `ff.features.create_water_feature_from_osm(domain)` | fn | +| `create_layerset` | `ff.features.create_layerset_feature_from_geojson(domain, geojson)` · `…_from_geodataframe(domain, gdf)` | fn | +| `get_feature` | `ff.get_feature(domain, feature_id)` · `feature.refresh()` | fn / method | +| `update_feature` | `feature.update(…)` | method | +| `delete_feature` | `feature.delete()` | method | +| `list_features` | `ff.list_features(domain)` | fn | +| `list_features_cross_domain` | `ff.list_all_features()` | fn | +| `get_feature_data_metadata` + `…_partition` | `feature.to_geodataframe()` | method (hides paging) | + +## Grids — 26 endpoints (greenfield, #178) + +All `create_*_grid_*` functions share an internal `_grid_request_base(...)` + +alignment-translation helper (plain function, not a base class). + +**Create from external source (functions)** + +| API endpoint | SDK surface | +|---|---| +| `create_3dep_topography` | `ff.grids.create_topography_grid_from_3dep(domain, source_resolution_m=10, output_resolution_m=…)` | +| `create_landfire_topography` | `ff.grids.create_topography_grid_from_landfire(domain, version=…)` | +| `create_landfire_canopy` | `ff.grids.create_canopy_fuel_grid_from_landfire(domain, version=…)` | +| `create_meta_chm` | `ff.grids.create_canopy_height_grid_from_meta(domain, version=…)` | +| `create_naip_chm` | `ff.grids.create_canopy_height_grid_from_naip_chm(domain)` | +| `create_landfire_fbfm40` | `ff.grids.create_fuel_model_grid_from_landfire_fbfm40(domain, version=…, remove_non_burnable=…)` | +| `create_landfire_fccs` | `ff.grids.create_fuel_model_grid_from_landfire_fccs(domain, version=…, remove_bare_ground=…, output_resolution_m=…)` | +| `create_treemap` | `ff.grids.create_pim_grid_from_treemap(domain, version=…, bands=[…])` (PIM = Plot Imputation Map; bands tm_id/plt_cn) | + +**From your file / generated (functions)** + +| `create_geotiff_upload` | `ff.grids.create_grid_from_geotiff(domain, path, bands=[…])` | +| `create_netcdf_upload` | `ff.grids.create_grid_from_netcdf(domain, path)` | +| `create_uniform_grid` | `ff.grids.create_uniform_grid(domain, resolution_m=…, bands={…})` | + +**Transform a resource you hold** + +A transform is a *method* when it applies to any instance of the resource +(every grid can resample/export; every inventory can voxelize). A transform +that only makes sense for a *particular kind* of grid is a **function** instead, +so it never appears on a grid that cannot perform it (the alternative — a method +that raises for the wrong grid type — is the wart this avoids). The FBFM40 +lookup is the only such case today: it needs a grid carrying `fbfm` codes. + +| `create_fbfm40_lookup` | `ff.grids.create_fuel_grid_from_fbfm40_lookup(fbfm_grid, bands=[…])` (fn — FBFM40-only) | +| `create_tree_inventory_grid` | `inventory.voxelize(horizontal_resolution_m=…, vertical_resolution_m=…, bands=…)` (method) | +| `create_resample` | `grid.resample(output_resolution_m=… / align_to=…, resampling=…)` (method) | +| `create_layerset_rasterize` | `layerset.rasterize(output_resolution_m=…, overlap_method=…)` (method) | + +**Export** + +| `create_grid_export` | `grid.export(format="geotiff")` → Export (method) | +| `create_quicfire_export` | `ff.exports.create_quicfire_export(domain, topography=…, surface=…, canopy=…)` (fn — assembled from many) | + +**Lifecycle** · `get_grid` → `ff.get_grid(domain, grid_id)` + `grid.refresh()` · `update_grid` → `grid.update(…)` · `delete_grid` → `grid.delete()` · `list_grids` → `ff.list_grids(domain)` · `list_grids_cross_domain` → `ff.list_all_grids()` + +**Data out** · `get_chunk_metadata` + `get_grid_data_json` + `get_grid_data_binary` → `grid.to_xarray()` / `grid.to_numpy(band)` (chunk reassembly + signed-URL hidden) + +**Utility** · `check_3dep_coverage` → `ff.grids.check_3dep_coverage(domain)` + +(`create_treemap` is a first-class grid source — see `create_pim_grid_from_treemap` in the "Create from external source" table above. It is distinct from `create_tree_inventory_grid` → `inventory.voxelize`, which produces a 3D voxel grid from an inventory.) + +## v1 workflow findings (from mining `docs/v1`) + +- **Masking simplifies.** v1's separate feature grid + `feature_masks=["road", + "water"]` becomes `modifications=[ff.mask(feature)]` on the grids that need it + (the API `GridModification` references a Feature by id). One fewer resource. +- **`to_geodataframe`/`to_xarray` erase the pagination ritual** — v1's + `get_data` → `get_all_data` → `from_features` collapses to `feature.to_geodataframe()`. +- **Custom road/water is a model change to document:** v1 typed + `create_road_feature_from_geodataframe`; v2 does road/water from OSM only, so + bring-your-own geometry goes through `create_layerset_feature_from_*`. +- **`export_roi` wants a v2 home** — a plain function, not a façade. + +## Open / flagged + +- ~~`landfire_fccs` alignment/resolution asymmetry~~ — resolved (FastFuels-API-v2 + #358 added `alignment` + `extent_buffer_cells`; the SDK creator now exposes them). +- Single-grid export is a method (`grid.export`) but the QUIC-Fire bundle is a + function (`ff.exports.create_quicfire_export`) — consistent with the rule + (one held resource → method; assembled-from-many → function), but worth a look. +- Modifications/treatments vocab (`mask`/`modify`/`within`/`thin_to_*`) — now + settled: builders ship for all three — `mask` (grids), `basal_area_treatment`/ + `diameter_treatment` (inventory treatments), and `tree_attribute`/`tree_within`/ + `remove_trees`/`modify_trees` (inventory modifications). `tree_within` builds an + `InventoryFeatureSpatialCondition`, which treatments' `conditions=` also accept; + geometry/expression conditions remain a raw pass-through. +- Returned-record implementation (wrap generated attrs model vs clean dataclass) + — parked; shows up on every record. + +## Post-regen additions (2026-06-12) + +Wired after re-syncing the client to the live spec (FastFuels-API-v2 #358 + +later merges). Each new resource/endpoint kept to the settled rules above. + +- **point_clouds** — new upload-only resource: `PointCloud` record (lifecycle + only — no transforms/data-out), `create_point_cloud_from_file(domain, path, + point_cloud_type=)`, `list_point_clouds`/`get_point_cloud`. Nothing else + sources from a point cloud yet. +- **Inventory treatments** — `Inventory.apply_treatments([...])` (in-place + re-derive) + the `ff.basal_area_treatment`/`ff.diameter_treatment` builders + (`treatments.py`). The former #333 fork-safety blocker for both in-place + processing paths is fixed. +- **GDAM** — `create_tree_inventory_from_gdam(domain, source_inventory, + impute_columns=)`. A *create* (new inventory) → a function in the + `create_tree_inventory_from_*` family, **not** a method — same call as the + fbfm40-lookup precedent (derive-a-new-resource = function). +- **Grid** gained `duplicate` (byte-copy clone, mirrors `Inventory.duplicate`), + `apply_modifications` (in-place re-derive; grid `modifications` list is *not* + echoed in the pending response, unlike inventories), and `band_summary(band)` + (cheap per-band stats from the new `Band.summary`; shares a `_band` helper + with `to_numpy`). +- **FCCS** reached alignment parity (#358) — creator now takes the alignment kwargs. +- **Uploads** — one shared `_uploads.put_upload(spec, path)` echoes the + server's signed `spec.headers` verbatim; replaced the two divergent + `_put_upload` copies (grids had been missing the GCS content-length-range). + +## Post-regen backlog (2026-08-04) + +Client re-synced against the live spec: 94 operations, 236 schemas. 19 +operations have no SDK surface. Ordered by what unblocks a user workflow. + +### Breaking (landed with the regen) + +- `get_inventory_data` split into `get_inventory_data_json` (adds + `json_orientation`) and `get_inventory_data_csv`; `InventoryDataFormat` is + gone. `Inventory.get_data_partition` now calls the JSON variant — behavior + unchanged. The CSV variant is unused (see *data out* below). +- FastFuels-API-v2#489 resolved both schema-title collisions at the source. + `generate_client.sh` now consumes the production spec without patching it. + The generated domain request model is `GeoJsonFeatureCollection`, and the + two 3DEP coverage models are `PointCloudThreeDepCoverageResponse` and + `TopographyThreeDepCoverageResponse`. + +### Quotas — cross-cutting, affects every creator + +41 of 94 operations can now return **429** with a structured +`QuotaExceededDetail` (`quota`, `current`, `limit`, `window_reset_on`). +`exceptions.py` has no 429 mapping, so today it degrades to a bare +`ApiException`. Needs a `QuotaExceededException` carrying those fields — +`window_reset_on` is what a caller retries on. + +- `ff.get_quotas()` / `ff.get_usage()` ← `users/me`, `users/me/usage` + (`Quotas`, `Usage` per resource type: active/total counts, storage bytes, + weekly dispatch windows, TTL policy). Top-level functions: owner-scoped, + no held resource. + +### New resource sources + +- **3DEP point clouds** — `ff.point_clouds.create_point_cloud_from_3dep( + domain, datasets=)` + `ff.point_clouds.check_3dep_coverage(domain)` + (returns `available`, `coverage_fraction`, `estimated_point_count`, + `point_budget`, `exceeds_point_budget`, per-acquisition `datasets`). + Mirrors the existing `grids.check_3dep_coverage` pre-flight pattern; this + is the first non-upload point cloud source. +- **Point cloud → CHM** — `ff.grids.create_canopy_height_grid_from_point_cloud( + point_cloud, ...)`. First consumer of a point cloud, which closes the + 3DEP → CHM → tree inventory chain entirely inside the SDK. + +### New grid creators + +- **DUET** — `ff.grids.create_surface_fuel_grid_from_duet(source_grid, + years_since_burn=, wind_direction=, wind_variability=, bands=, + calibration=)`. Needs a `duet_calibration(...)` builder in the + `modifications.py`/`treatments.py` family: `DuetCalibration` nests + fuel_load/fuel_depth/fuel_moisture → per-fuel-type + (grass/coniferous/deciduous/litter/all) targets in three shapes + (constant / max-min / mean-sd). +- **FBFM13** — `create_fuel_model_grid_from_landfire_fbfm13` (version + 2023/2024, `remove_non_burnable=`) and + `create_fuel_grid_from_fbfm13_lookup` (9 bands), matching the FBFM40 pair. +- **FCCS lookup** — `create_fuel_grid_from_fccs_lookup` (12 bands incl. duff + and live components). The FCCS *source* creator exists; the lookup that + turns it into fuel parameters does not. +- **Compose** — `create_grid_from_compose(inputs, select=, compute=)`: grid + algebra over aliased input bands (`add`/`subtract`/`multiply`/`divide`/ + `min`/`max`/`average`, conditional `select` with `else_`). The largest + design question in this batch — it is a small DSL, not a creator with + kwargs, and it needs builders to be usable from Python. + +### New export + +- **Landscape (LCP)** — `ff.exports.create_landscape_export(...)`: 8-band + FlamMap/IFTDSS/WFDSS GeoTIFF assembled from 8 named + `LandscapeFieldSource` (grid_id + band) plus `fire_behavior_fuel_model` + (fbfm13/fbfm40) and alignment. Assembled-from-many → function, same shape + as `create_quicfire_export`. + +### Records and data out + +- `Inventory.forestry_metrics` — new `TreeForestryMetrics` on the inventory + record (tree_count, basal area/acre, TPA, QMD, dominant FIA species + groups). Read-only accessor; the parallel of `Grid.band_summary`. +- `Inventory.column_summary(column)` — `Column.summary` is new + (categorical/continuous), exactly mirroring `Band.summary`; reuse the + `band_summary` shape. +- `Inventory.to_dataframe` should fetch CSV partitions and `pd.read_csv` + them instead of rebuilding frames from JSON row lists. +- `get_chunk_metadata` / `get_grid_data_json` stay unwrapped — the binary + chunk path already covers `to_numpy`/`to_xarray`. + +### Account management (deliberately generated-client only) + +**Decision (2026-08-04): do not add high-level wrappers for `applications` +or `keys`.** The public SDK consumes an existing API key; application and key +provisioning remain account-console concerns. + +The ten generated endpoints remain available in `client_library`, including +the new `Application.tier`/`quota_overrides` fields, but they are not exported +through `fastfuels_sdk.v2`. This keeps one-time key secrets, credential +rotation/revocation, and destructive account operations out of ordinary data +workflows. It also matches v1, where these endpoints exist only in the +generated client and never gained a high-level wrapper. + +Revisit this boundary only if programmatic credential provisioning becomes a +supported SDK use case. That work should be designed as a dedicated account +module with explicit secret-handling requirements rather than added piecemeal +to the resource wrappers. diff --git a/mkdocs.yml b/mkdocs.yml index 61580be..6908c26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,28 +1,70 @@ # mkdocs.yml +# +# One config builds either docs version: DOCS_DIR selects the tree +# (defaults to docs/v1; set DOCS_DIR=docs/v2 for v2), and each tree +# defines its own nav in a SUMMARY.md (literate-nav). Versions deploy via +# mike as independent site snapshots picked from the header version +# selector; see docs/deploy.sh. Local preview: +# uv run mkdocs serve # v1 only +# DOCS_DIR=docs/v2 uv run mkdocs serve # v2 only +# ./docs/deploy.sh # both versions + selector site_name: FastFuels SDK Documentation +# site_url is required for mike's version selector to keep the reader on +# the same page when switching versions +site_url: https://silvxlabs.github.io/fastfuels-sdk-python/ + +docs_dir: !ENV [DOCS_DIR, docs/v1] + +repo_url: https://github.com/silvxlabs/fastfuels-sdk-python +repo_name: silvxlabs/fastfuels-sdk-python theme: name: "material" + icon: + repo: fontawesome/brands/github + palette: + # Palette toggle: light mode -> dark mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle: dark mode -> light mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode features: + - navigation.top + - navigation.tracking + - navigation.footer + - toc.follow + - search.suggest + - search.highlight + - search.share - content.code.copy + - content.code.annotate + - content.tooltips + +extra: + version: + provider: mike + alias: true + social: + - icon: fontawesome/brands/github + link: https://github.com/silvxlabs/fastfuels-sdk-python + name: FastFuels SDK on GitHub + +copyright: Copyright © Silvx Labs -nav: - - Home: index.md - - v1 (current): - - How-To Guides: - - Authentication: v1/guides/authentication.md - - Domains: v1/guides/domains.md - - Inventories: v1/guides/inventories.md - - Point Clouds: v1/guides/point_clouds.md - - Features: v1/guides/features.md - - Grids: v1/guides/grids.md - - Tutorials: - - Export to QUIC-Fire: v1/tutorials/export_to_quicfire.md - - ALS Point Cloud: v1/tutorials/point_cloud_example.md - - Reference: v1/reference.md +extra_css: + - stylesheets/extra.css plugins: - search + - literate-nav: + nav_file: SUMMARY.md - mkdocstrings: handlers: python: @@ -39,7 +81,17 @@ plugins: show_signature_annotations: true markdown_extensions: + # Python Markdown + - abbr - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + # PyMdown Extensions - pymdownx.details - pymdownx.highlight: anchor_linenums: true @@ -48,3 +100,10 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg diff --git a/pyproject.toml b/pyproject.toml index 210557f..aec6826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,13 +21,17 @@ classifiers = [ "Topic :: Scientific/Engineering", ] dependencies = [ + "attrs>=22.2.0", "geopandas", + "httpx>=0.23.0", "numpy", "pandas", "pydantic>=2", + "python-dateutil>=2.9.0.post0", "requests", "scipy", "urllib3>=2.1.0", + "xarray", "zarr", ] @@ -40,23 +44,34 @@ dev = [ "pytest", "pre-commit", "ipykernel", - "httpx>=0.23.0,<0.29.0", + "httpx>=0.23.0", "attrs>=22.2.0", "python-dateutil>=2.9.0.post0", + # reads exported GeoTIFFs to ground-truth Grid.to_numpy against the server + "rasterio", +] +docs = [ + "mike>=2.2.0", + "mkdocs", + # pinned pre-0.6.3: 0.6.3 co-installs the properdocs mkdocs-fork and + # warns when run under mkdocs; revisit when the fork situation settles + "mkdocs-literate-nav==0.6.2", + # major pinned per the Material docs (semver; majors break config) + "mkdocs-material>=9.7,<10", + "mkdocstrings", + "mkdocstrings-python", ] -docs = ["mkdocs", "mkdocs-material", "mkdocstrings", "mkdocstrings-python"] [tool.hatch.version] source = "vcs" [tool.hatch.build] exclude = [ - # v2 is under development and not yet distributable: its runtime deps - # (httpx, attrs, python-dateutil) live in the dev group until the v2 - # preview ships. The Phase 2 release swaps this for fine-grained - # excludes (v2/*.sh, v2/COMPARISON.md, v2/draft_*.py), mirroring v1's. - "fastfuels_sdk/v2", - # Client regeneration scaffolding — repo-only, not for distribution + # Client regeneration scaffolding and design notes — repo-only, not for + # distribution. v2 ships as a beta preview; only its non-runtime files + # are excluded (mirroring v1's client_library excludes below). + "fastfuels_sdk/v2/*.sh", + "fastfuels_sdk/v2/*.md", "fastfuels_sdk/v1/client_library/*.sh", "fastfuels_sdk/v1/client_library/README.md", "fastfuels_sdk/v1/client_library/api_spec.json", diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/v2/conftest.py b/tests/v2/conftest.py new file mode 100644 index 0000000..1df202f --- /dev/null +++ b/tests/v2/conftest.py @@ -0,0 +1,165 @@ +""" +tests/v2/conftest.py + +Session-scoped resources shared across the v2 test modules. + +These fixtures are READ-ONLY by convention: they are shared by every +module in the session, so any test that mutates or deletes a resource +must create its own throwaway instead. Expensive job resources (the +completed OSM road feature and the completed topography grid) are built +once per session and torn down with the domain — deleting the domain +cascades to everything created inside it. + +Fixtures that wait on a job carry a ``completed_`` prefix; resources +that are born complete (layerset uploads) don't need one. +""" + +import pytest + +from fastfuels_sdk.v2.features import ( + create_layerset_feature_from_geojson, + create_road_feature_from_osm, +) +from fastfuels_sdk.v2.grids import ( + create_fuel_model_grid_from_landfire_fbfm13, + create_fuel_model_grid_from_landfire_fbfm40, + create_fuel_model_grid_from_landfire_fccs, + create_pim_grid_from_treemap, + create_topography_grid_from_3dep, +) +from fastfuels_sdk.v2.inventories import create_tree_inventory_from_pim_grid +from tests.v2.utils import ( + create_default_domain, + create_default_layerset_geojson, + sweep_leftover_domains, +) + + +@pytest.fixture(scope="session") +def test_domain(): + """The session-wide test domain. READ-ONLY: shared by every module.""" + sweep_leftover_domains() + domain = create_default_domain() + yield domain + # Cleanup: force-delete cascades to the domain's features and grids + domain.delete(force=True) + + +@pytest.fixture(scope="session") +def completed_road_feature(test_domain): + """A completed OSM road feature. READ-ONLY: shared by every module.""" + feature = create_road_feature_from_osm( + test_domain, + name="test_road", + description="Road feature for testing v2 feature operations", + tags=["test"], + ) + feature.wait() + return feature + + +@pytest.fixture(scope="session") +def layerset_feature(test_domain): + """A layerset feature (born completed). READ-ONLY: shared by every module.""" + return create_layerset_feature_from_geojson( + test_domain, + create_default_layerset_geojson(), + name="test_layerset", + description="Layerset feature for testing v2 feature operations", + tags=["layerset-test"], + ) + + +@pytest.fixture(scope="session") +def completed_topography_grid(test_domain): + """A completed 3DEP topography grid. READ-ONLY: shared by every module.""" + grid = create_topography_grid_from_3dep( + test_domain, + output_resolution_m=10, + name="test_topography", + description="Topography grid for testing v2 grid operations", + tags=["test"], + ) + grid.wait() + return grid + + +@pytest.fixture(scope="session") +def completed_fbfm40_grid(test_domain): + """A completed LANDFIRE FBFM40 fuel model grid. READ-ONLY: shared by + every module. Provides the FBFM40 codes that + ``create_fuel_grid_from_fbfm40_lookup`` reads. + """ + grid = create_fuel_model_grid_from_landfire_fbfm40( + test_domain, + output_resolution_m=30, + name="test_fbfm40", + description="FBFM40 grid for testing v2 grid lookup operations", + tags=["test"], + ) + grid.wait() + return grid + + +@pytest.fixture(scope="session") +def completed_fbfm13_grid(test_domain): + """A completed LANDFIRE FBFM13 fuel model grid. READ-ONLY.""" + grid = create_fuel_model_grid_from_landfire_fbfm13( + test_domain, + version="2024", + remove_non_burnable=["NB1", "NB2"], + output_resolution_m=30, + name="test_fbfm13", + description="FBFM13 grid for testing v2 grid lookup operations", + tags=["test"], + ) + grid.wait() + return grid + + +@pytest.fixture(scope="session") +def completed_fccs_grid(test_domain): + """A completed LANDFIRE FCCS fuelbed grid. READ-ONLY.""" + grid = create_fuel_model_grid_from_landfire_fccs( + test_domain, + remove_bare_ground=True, + output_resolution_m=30, + name="test_fccs", + description="FCCS grid for testing v2 grid lookup operations", + tags=["test"], + ) + grid.wait() + return grid + + +@pytest.fixture(scope="session") +def completed_pim_grid(test_domain): + """A completed TreeMap PIM grid. READ-ONLY: shared by every module.""" + grid = create_pim_grid_from_treemap( + test_domain, + output_resolution_m=30, + resampling="nearest", + name="test_pim", + description="PIM grid for testing v2 inventory operations", + tags=["test"], + ) + grid.wait() + return grid + + +@pytest.fixture(scope="session") +def completed_tree_inventory(test_domain, completed_pim_grid): + """A completed PIM-expanded tree inventory. READ-ONLY: shared by every + module. Tests that mutate (update, apply_modifications) work on a + duplicate or a throwaway instead. + """ + inventory = create_tree_inventory_from_pim_grid( + test_domain, + completed_pim_grid, + seed=42, + name="test_inventory", + description="Tree inventory for testing v2 inventory operations", + tags=["test"], + ) + inventory.wait() + return inventory diff --git a/tests/v2/test_api.py b/tests/v2/test_api.py new file mode 100644 index 0000000..ac38a3a --- /dev/null +++ b/tests/v2/test_api.py @@ -0,0 +1,76 @@ +"""Tests for v2 client configuration and owner-scoped API helpers.""" + +from http import HTTPStatus + +import fastfuels_sdk.v2 as ff +import fastfuels_sdk.v2.api as api +from fastfuels_sdk.v2.client_library.models import ( + CountUsage, + JobResourceUsage, + Quotas, + Usage, + UsageCount, + UsageLifecycle, + UsageStorage, + UserMeResponse, + UserMeResponseKind, +) +from fastfuels_sdk.v2.client_library.types import Response + + +def _response(parsed): + return Response( + status_code=HTTPStatus.OK, + content=b"", + headers={}, + parsed=parsed, + ) + + +def test_get_quotas_returns_authenticated_owner_quotas(monkeypatch): + client = object() + quotas = Quotas(max_active_grids=3) + owner = UserMeResponse( + id="owner-id", + kind=UserMeResponseKind.USER, + tier="standard", + quotas=quotas, + ) + monkeypatch.setattr(api, "ensure_client", lambda: client) + monkeypatch.setattr( + api.get_me, + "sync_detailed", + lambda *, client: _response(owner), + ) + + assert ff.get_quotas() is quotas + + +def test_get_usage_returns_authenticated_owner_usage(monkeypatch): + client = object() + count = UsageCount(usage=1, limit=10) + storage = UsageStorage(usage_bytes=1024, limit_bytes=2048) + job_usage = JobResourceUsage(active=count, total=count, storage=storage) + count_usage = CountUsage(total=count) + usage = Usage( + grids=job_usage, + exports=job_usage, + inventories=job_usage, + features=job_usage, + pointclouds=job_usage, + domains=count_usage, + applications=count_usage, + api_keys=count_usage, + lifecycle=UsageLifecycle( + resource_ttl_days=180, + failed_resource_ttl_days=14, + ), + ) + monkeypatch.setattr(api, "ensure_client", lambda: client) + monkeypatch.setattr( + api.get_me_usage, + "sync_detailed", + lambda *, client: _response(usage), + ) + + assert ff.get_usage() is usage diff --git a/tests/v2/test_calibrations.py b/tests/v2/test_calibrations.py new file mode 100644 index 0000000..25309ed --- /dev/null +++ b/tests/v2/test_calibrations.py @@ -0,0 +1,84 @@ +"""Tests for v2 calibration builders.""" + +import pytest + +from fastfuels_sdk.v2.calibrations import duet_calibration +from fastfuels_sdk.v2.client_library.models import ( + DuetConstantCalibrationTarget, + DuetMaxMinCalibrationTarget, + DuetMeanSdCalibrationTarget, +) + + +def test_duet_calibration_builds_each_target_method(): + calibration = duet_calibration( + fuel_load={ + "grass": {"mean": 0.5, "sd": 0.25}, + "litter": {"max": 5.0}, + }, + fuel_depth={"all": {"value": 0.3}}, + ) + + assert isinstance(calibration.fuel_load.grass, DuetMeanSdCalibrationTarget) + assert isinstance(calibration.fuel_load.litter, DuetMaxMinCalibrationTarget) + assert calibration.fuel_load.litter.min_ == 0.0 + assert isinstance(calibration.fuel_depth.all_, DuetConstantCalibrationTarget) + assert calibration.to_dict() == { + "fuel_load": { + "grass": {"method": "meansd", "mean": 0.5, "sd": 0.25}, + "litter": {"method": "maxmin", "max": 5.0, "min": 0.0}, + }, + "fuel_depth": { + "all": {"method": "constant", "value": 0.3}, + }, + } + + +def test_duet_calibration_accepts_explicit_method(): + calibration = duet_calibration( + fuel_moisture={ + "grass": {"method": "constant", "value": 12}, + } + ) + + assert calibration.fuel_moisture.grass.value == 12 + + +def test_duet_calibration_requires_a_parameter(): + with pytest.raises(ValueError, match="at least one"): + duet_calibration() + + +def test_duet_calibration_all_is_exclusive(): + with pytest.raises(ValueError, match="cannot be combined"): + duet_calibration( + fuel_load={ + "all": {"value": 1}, + "grass": {"value": 1}, + } + ) + + +def test_duet_calibration_litter_is_exclusive_of_components(): + with pytest.raises(ValueError, match="litter"): + duet_calibration( + fuel_load={ + "litter": {"value": 1}, + "coniferous": {"value": 1}, + } + ) + + +@pytest.mark.parametrize( + "target,match", + [ + ({"method": "unknown", "value": 1}, "Unknown calibration method"), + ({"mean": 1}, "missing required fields"), + ({"max": 1, "min": 2}, "greater than or equal"), + ({"value": -1}, "nonnegative"), + ({"value": 1, "max": 2}, "not used"), + ], +) +def test_duet_calibration_rejects_invalid_targets(target, match): + with pytest.raises(ValueError, match=match): + duet_calibration(fuel_load={"grass": target}) diff --git a/tests/v2/test_compose.py b/tests/v2/test_compose.py new file mode 100644 index 0000000..977d529 --- /dev/null +++ b/tests/v2/test_compose.py @@ -0,0 +1,312 @@ +"""Tests for v2 grid-compose builders and creator.""" + +from http import HTTPStatus + +import numpy as np +import pytest + +from fastfuels_sdk.v2 import compose, grids +from fastfuels_sdk.v2.client_library.models import ( + Band, + BandType, + ComposeComparisonOperator, + ComposeCompute, + ComposeLiteral, + ComposeOperator, + ComposeSelect, + GridSource, + InlineCompute, + JobStatus, +) +from fastfuels_sdk.v2.client_library.types import Response +from fastfuels_sdk.v2.grids import ( + Grid, + create_fuel_grid_from_fbfm40_lookup, + create_grid_from_compose, +) + + +def _grid( + id_="source-grid", + domain_id="domain-id", + status=JobStatus.COMPLETED, + bands=None, +): + return Grid( + id=id_, + domain_id=domain_id, + status=status, + source=GridSource(), + bands=bands + or [ + Band( + key="fuel_load.1hr", + type_=BandType.CONTINUOUS, + index=0, + unit="kg/m**2", + ), + Band( + key="fuel_depth", + type_=BandType.CONTINUOUS, + index=1, + unit="m", + ), + ], + ) + + +class TestComposeBuilders: + def test_select(self): + operation = compose.select("fuel_depth", "fuels.fuel_depth") + + assert isinstance(operation, ComposeSelect) + assert operation.to_dict() == { + "output": "fuel_depth", + "from": "fuels.fuel_depth", + } + + def test_compute(self): + operation = compose.compute( + "fuel_load.1hr", + "multiply", + ["fuels.fuel_load.1hr", 0.5], + unit="kg/m**2", + ) + + assert isinstance(operation, ComposeCompute) + assert operation.operator == ComposeOperator.MULTIPLY + assert operation.to_dict() == { + "operator": "multiply", + "operands": ["fuels.fuel_load.1hr", 0.5], + "output": "fuel_load.1hr", + "unit": "kg/m**2", + } + + def test_conditional_typed_literal_fallback(self): + where = compose.condition("fuels.fbfm", "in", ["GR1", "GR2"]) + fallback = compose.literal(0, unit="kg/m**2") + operation = compose.select( + "fuel_load.1hr", + "fuels.fuel_load.1hr", + conditions=[where], + else_=fallback, + ) + + assert where.operator == ComposeComparisonOperator.IN + assert isinstance(fallback, ComposeLiteral) + assert operation.to_dict()["conditions"] == [ + { + "band": "fuels.fbfm", + "operator": "in", + "value": ["GR1", "GR2"], + } + ] + assert operation.to_dict()["else"] == { + "type": "literal", + "value": 0, + "unit": "kg/m**2", + } + + def test_inline_compute(self): + fallback = compose.inline_compute( + "average", + ["base.fuel_load.1hr", "alternate.fuel_load.1hr"], + ) + + assert isinstance(fallback, InlineCompute) + assert fallback.to_dict() == { + "operator": "average", + "operands": [ + "base.fuel_load.1hr", + "alternate.fuel_load.1hr", + ], + } + + @pytest.mark.parametrize( + "operator,operands", + [ + ("add", ["fuels.fuel_load.1hr"]), + ("subtract", ["fuels.fuel_load.1hr", 1, 2]), + ("divide", [1, 2]), + ], + ) + def test_compute_rejects_invalid_operands(self, operator, operands): + with pytest.raises(ValueError): + compose.compute("output", operator, operands) + + def test_condition_in_requires_list(self): + with pytest.raises(ValueError, match="requires a list"): + compose.condition("fuels.fbfm", "in", "GR1") + + def test_condition_ordering_requires_scalar(self): + with pytest.raises(ValueError, match="requires a scalar"): + compose.condition("fuels.fuel_depth", "gt", [0, 1]) + + def test_conditions_require_fallback(self): + with pytest.raises(ValueError, match="else_"): + compose.select( + "fuel_depth", + "fuels.fuel_depth", + conditions=[compose.condition("fuels.fuel_depth", "gt", 0)], + ) + + def test_string_literal_cannot_have_unit(self): + with pytest.raises(ValueError, match="cannot carry a unit"): + compose.literal("GR1", unit="kg/m**2") + + +class TestCreateGridFromCompose: + def test_builds_request(self, monkeypatch): + source = _grid() + created = _grid(id_="composed-grid", status=JobStatus.PENDING) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_compose_grid, + "sync_detailed", + fake_create, + ) + + result = create_grid_from_compose( + {"fuels": source}, + select=[compose.select("fuel_depth", "fuels.fuel_depth")], + compute=[ + compose.compute( + "fuel_load.1hr", + "multiply", + ["fuels.fuel_load.1hr", 0.5], + ) + ], + name="Composed fuels", + tags=["test"], + ) + + assert result.id == "composed-grid" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert [item.to_dict() for item in captured["body"].inputs] == [ + {"grid_id": "source-grid", "alias": "fuels"} + ] + assert captured["body"].select[0].output == "fuel_depth" + assert captured["body"].compute[0].operator == ComposeOperator.MULTIPLY + assert captured["body"].name == "Composed fuels" + assert captured["body"].tags == ["test"] + + @pytest.mark.parametrize( + "inputs,match,error", + [ + ([], "mapping", TypeError), + ({}, "at least one", ValueError), + ({"1bad": _grid()}, "Invalid compose alias", ValueError), + ( + {"a": _grid(status=JobStatus.PENDING)}, + "Cannot compose", + ValueError, + ), + ( + {"a": _grid(), "b": _grid()}, + "more than one alias", + ValueError, + ), + ( + {"a": _grid(), "b": _grid(id_="other", domain_id="other")}, + "same domain", + ValueError, + ), + ], + ) + def test_rejects_invalid_inputs(self, inputs, match, error): + with pytest.raises(error, match=match): + create_grid_from_compose( + inputs, + select=[compose.select("fuel_depth", "a.fuel_depth")], + ) + + def test_requires_an_operation(self): + with pytest.raises(ValueError, match="At least one"): + create_grid_from_compose({"fuels": _grid()}) + + def test_rejects_duplicate_outputs(self): + with pytest.raises(ValueError, match="must be unique"): + create_grid_from_compose( + {"fuels": _grid()}, + select=[compose.select("fuel", "fuels.fuel_depth")], + compute=[ + compose.compute( + "fuel", + "multiply", + ["fuels.fuel_load.1hr", 0.5], + ) + ], + ) + + def test_rejects_empty_output(self): + with pytest.raises(ValueError, match="nonempty output"): + create_grid_from_compose( + {"fuels": _grid()}, + select=[ComposeSelect(output="", from_="fuels.fuel_depth")], + ) + + @pytest.mark.parametrize( + "reference,match", + [ + ("other.fuel_depth", "Unknown compose band reference"), + ("fuels.unknown", "has no 'unknown' band"), + ], + ) + def test_rejects_unknown_references(self, reference, match): + with pytest.raises(ValueError, match=match): + create_grid_from_compose( + {"fuels": _grid()}, + select=[compose.select("fuel_depth", reference)], + ) + + def test_create_live(self, completed_fbfm40_grid): + fuel_grid = create_fuel_grid_from_fbfm40_lookup( + completed_fbfm40_grid, + bands=["fuel_load.1hr", "fuel_depth"], + name="test_compose_source", + tags=["test"], + ) + composed = None + try: + fuel_grid.wait() + composed = create_grid_from_compose( + {"fuels": fuel_grid}, + select=[compose.select("fuel_depth", "fuels.fuel_depth")], + compute=[ + compose.compute( + "fuel_load.1hr", + "multiply", + ["fuels.fuel_load.1hr", 0.5], + conditions=[compose.condition("fuels.fuel_load.1hr", "gt", 0)], + else_=compose.literal(0, unit="kg/m**2"), + ) + ], + name="test_composed_fuels", + tags=["test"], + ) + composed.wait() + assert composed.status == JobStatus.COMPLETED + assert [band.key for band in composed.bands] == [ + "fuel_depth", + "fuel_load.1hr", + ] + fuel_load = composed.to_numpy("fuel_load.1hr") + assert fuel_load.ndim == 2 + assert np.isfinite(fuel_load).any() + finally: + if composed is not None: + composed.delete() + fuel_grid.delete() diff --git a/tests/v2/test_domains.py b/tests/v2/test_domains.py new file mode 100644 index 0000000..108854a --- /dev/null +++ b/tests/v2/test_domains.py @@ -0,0 +1,282 @@ +""" +tests/v2/test_domains.py +""" + +# Core imports +import json +from uuid import uuid4 + +# Internal imports +from tests import TEST_DATA_DIR +from tests.v2.utils import create_default_domain +from fastfuels_sdk.v2.domains import Domain, list_domains, reproject_geojson +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +# External imports +import pytest +import geopandas as gpd + +# The test_domain fixture is session-scoped and shared across modules +# (tests/v2/conftest.py). It is READ-ONLY: tests that mutate or delete +# create throwaways. + + +def feature_names(domain: Domain) -> set: + """Return the named features in a domain response.""" + return {feature.to_dict()["properties"]["name"] for feature in domain.features} + + +class TestCreateDomain: + test_files = ["blue_mtn", "blue_mtn_5070"] + test_formats = ["geojson", "kml", "shp"] + + domain_name = "test_domain" + domain_description = "Domain for testing v2 domain operations" + pad_to_resolution = 2.0 + + def assert_valid_domain( + self, + domain: Domain, + input_gdf: gpd.GeoDataFrame, + expected_crs_name: str = None, + ): + """Shared assertions for a freshly created domain.""" + assert len(domain.id) > 0 + assert domain.name == self.domain_name + assert domain.description == self.domain_description + assert domain.pad_to_resolution == self.pad_to_resolution + + # v2 responses carry the normalized working extent only. + assert feature_names(domain) == {"domain"} + + # If the input CRS is projected, the domain preserves it (echoing + # the CRS name as sent, e.g. "urn:ogc:def:crs:EPSG::5070"); + # otherwise the geometry is projected to the appropriate UTM zone + if input_gdf.crs.is_projected: + expected = expected_crs_name or ":".join(input_gdf.crs.to_authority()) + assert domain.crs.properties.name == expected + else: + assert domain.crs.properties.name == input_gdf.estimate_utm_crs().srs + + @pytest.mark.parametrize("test_name", test_files) + @pytest.mark.parametrize("geojson_type", ["Feature", "FeatureCollection"]) + def test_from_geojson(self, test_name, geojson_type): + # Load test GeoJSON data + geojson_gdf = gpd.GeoDataFrame.from_file(TEST_DATA_DIR / f"{test_name}.geojson") + with open(TEST_DATA_DIR / f"{test_name}.geojson") as f: + geojson = json.load(f) + + if geojson_type == "Feature": + feature_geojson = geojson["features"][0] + if "crs" in geojson: + feature_geojson["crs"] = geojson["crs"] + geojson = feature_geojson + + # Create a domain using the GeoJSON (a single Feature is wrapped + # into a FeatureCollection by the SDK) + domain = Domain.from_geojson( + geojson, + name=self.domain_name, + description=self.domain_description, + pad_to_resolution=self.pad_to_resolution, + ) + # A projected domain echoes the CRS name exactly as the file + # declares it + file_crs = geojson.get("crs", {}).get("properties", {}).get("name") + self.assert_valid_domain(domain, geojson_gdf, expected_crs_name=file_crs) + domain.delete() + + @pytest.mark.parametrize("test_name", test_files) + def test_from_geodataframe(self, test_name): + geojson_gdf = gpd.GeoDataFrame.from_file(TEST_DATA_DIR / f"{test_name}.geojson") + + # Create a domain using the GeoDataFrame. The GeoDataFrame CRS is + # forwarded to the API, so projected inputs (blue_mtn_5070) must + # come back in their original CRS, not the EPSG:4326 default. + domain = Domain.from_geodataframe( + geojson_gdf, + name=self.domain_name, + description=self.domain_description, + pad_to_resolution=self.pad_to_resolution, + ) + self.assert_valid_domain(domain, geojson_gdf) + domain.delete() + + @pytest.mark.parametrize("test_format", test_formats) + def test_from_file(self, test_format): + geojson_gdf = gpd.GeoDataFrame.from_file(TEST_DATA_DIR / "blue_mtn.geojson") + + domain = Domain.from_file( + TEST_DATA_DIR / f"blue_mtn.{test_format}", + name=self.domain_name, + description=self.domain_description, + pad_to_resolution=self.pad_to_resolution, + ) + self.assert_valid_domain(domain, geojson_gdf) + domain.delete() + + def test_from_geojson_invalid_type(self): + with pytest.raises(ValueError, match="FeatureCollection"): + Domain.from_geojson({"type": "Point", "coordinates": [0, 0]}) + + def test_tags(self): + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + domain = Domain.from_geojson(geojson, tags=["test", "v2"]) + assert domain.tags == ["test", "v2"] + domain.delete() + + +class TestPreviewDomain: + def test_preview(self): + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + + previewed = Domain.preview(geojson, pad_to_resolution=2.0) + + # A preview is never persisted; its id is the "preview" sentinel + assert previewed.id == "preview" + assert feature_names(previewed) == {"domain"} + assert previewed.pad_to_resolution == 2.0 + assert len(previewed.bbox) >= 4 + + +class TestFromId: + def test_success(self, test_domain): + domain = Domain.from_id(test_domain.id) + assert domain.id == test_domain.id + assert domain.name == test_domain.name + + def test_not_found(self): + with pytest.raises(NotFoundException): + Domain.from_id(uuid4().hex) + + +class TestRefreshDomain: + def test_refresh_returns_self(self, test_domain): + # refresh() re-fetches and updates in place, returning the same object + refreshed = test_domain.refresh() + assert refreshed is test_domain + assert refreshed.id == test_domain.id + + +class TestUpdateDomain: + @pytest.fixture(scope="class") + def update_domain(self): + """A throwaway domain to mutate (the shared fixture is read-only).""" + domain = create_default_domain() + yield domain + domain.delete() + + def test_update_name(self, update_domain): + # update() mutates in place and returns self (chains) + updated = update_domain.update(name="updated_name") + assert updated is update_domain + assert update_domain.name == "updated_name" + # The remote resource reflects the update + assert Domain.from_id(update_domain.id).name == "updated_name" + + def test_update_description(self, update_domain): + updated = update_domain.update(description="updated description") + assert updated is update_domain + assert update_domain.description == "updated description" + + def test_update_tags(self, update_domain): + update_domain.update(tags=["updated"]) + assert update_domain.tags == ["updated"] + + def test_update_no_fields_makes_no_api_call(self, update_domain): + # No fields provided: returns self without touching the API + assert update_domain.update() is update_domain + + +class TestGetLattice: + def test_get_lattice(self, test_domain): + lattice = test_domain.get_lattice(resolution=2.0) + assert lattice.resolution == 2.0 + assert lattice.num_buffer_cells == 0 + assert lattice.crs == test_domain.crs.properties.name + # Affine coefficients [a, b, c, d, e, f] (rasterio convention) + assert len(lattice.transform) == 6 + assert lattice.transform[0] == 2.0 + assert abs(lattice.transform[4]) == 2.0 + # [height, width] in pixels + assert len(lattice.shape) == 2 + assert all(dim > 0 for dim in lattice.shape) + + def test_buffer_cells_expand_the_lattice(self, test_domain): + lattice = test_domain.get_lattice(resolution=2.0) + buffered = test_domain.get_lattice(resolution=2.0, num_buffer_cells=5) + assert buffered.shape[0] == lattice.shape[0] + 10 + assert buffered.shape[1] == lattice.shape[1] + 10 + + def test_invalid_resolution(self, test_domain): + with pytest.raises(UnprocessableEntityException): + test_domain.get_lattice(resolution=-1.0) + + +class TestListDomains: + def test_list_domains(self, test_domain): + domains = list_domains() + assert test_domain.id in [domain.id for domain in domains] + + def test_sorting(self, test_domain): + domains = list_domains(sort_by="created_on", sort_order="descending") + assert len(domains) > 0 + + def test_invalid_sort_field(self): + with pytest.raises(ValueError): + list_domains(sort_by="not_a_field") + + +class TestReprojectGeojson: + def test_reproject(self): + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + + projected = reproject_geojson(geojson, target_epsg=5070) + + assert projected["crs"]["properties"]["name"] == "EPSG:5070" + assert len(projected["features"]) == len(geojson["features"]) + # Coordinates actually moved out of degrees + original_coord = geojson["features"][0]["geometry"]["coordinates"][0][0] + projected_coord = projected["features"][0]["geometry"]["coordinates"][0][0] + assert original_coord != projected_coord + + def test_invalid_target_epsg(self): + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + with pytest.raises(UnprocessableEntityException): + reproject_geojson(geojson, target_epsg=999999) + + +class TestToGeodataframe: + def test_to_geodataframe(self, test_domain): + gdf = test_domain.to_geodataframe() + + assert len(gdf) == len(test_domain.features) + assert set(gdf["name"]) == {"domain"} + assert (gdf["domain_id"] == test_domain.id).all() + assert ":".join(gdf.crs.to_authority()) == test_domain.crs.properties.name + + +class TestToJson: + def test_to_json(self, test_domain): + domain_dict = json.loads(test_domain.to_json()) + assert domain_dict["id"] == test_domain.id + assert domain_dict["type"] == "FeatureCollection" + + +class TestDeleteDomain: + def test_delete_domain(self): + domain = create_default_domain() + domain.delete() + + with pytest.raises(NotFoundException): + Domain.from_id(domain.id) + + with pytest.raises(NotFoundException): + domain.delete() diff --git a/tests/v2/test_exceptions.py b/tests/v2/test_exceptions.py new file mode 100644 index 0000000..5f1c217 --- /dev/null +++ b/tests/v2/test_exceptions.py @@ -0,0 +1,83 @@ +"""Tests for v2 API error translation.""" + +import datetime +import json +from http import HTTPStatus + +import pytest + +from fastfuels_sdk.v2.client_library.models import QuotaExceededDetail +from fastfuels_sdk.v2.client_library.types import Response +from fastfuels_sdk.v2.exceptions import QuotaExceededException, expect + + +def _quota_response(detail, *, parsed=True, retry_after=None): + headers = {} + if retry_after is not None: + headers["Retry-After"] = str(retry_after) + return Response( + status_code=HTTPStatus.TOO_MANY_REQUESTS, + content=json.dumps({"detail": detail.to_dict()}).encode(), + headers=headers, + parsed=detail if parsed else None, + ) + + +def test_quota_exceeded_exception_exposes_structured_detail(): + detail = QuotaExceededDetail( + quota="max_active_grids", + current=25, + limit=25, + message="Too many active grid jobs.", + ) + + with pytest.raises(QuotaExceededException) as exc_info: + expect(_quota_response(detail, retry_after=60), HTTPStatus.CREATED) + + error = exc_info.value + assert error.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert error.detail is detail + assert error.quota == "max_active_grids" + assert error.current == 25 + assert error.limit == 25 + assert error.window_reset_on is None + assert error.message == "Too many active grid jobs." + assert error.reason == "QUOTA_EXCEEDED" + assert error.retry_after == 60 + assert str(error) == "(429) Too many active grid jobs." + + +def test_quota_exceeded_exception_parses_raw_response_content(): + reset = datetime.datetime(2026, 8, 10, tzinfo=datetime.timezone.utc) + detail = QuotaExceededDetail( + quota="max_weekly_grid_dispatches", + current=500, + limit=500, + window_reset_on=reset, + message="Weekly grid dispatch quota reached.", + ) + + with pytest.raises(QuotaExceededException) as exc_info: + expect(_quota_response(detail, parsed=False), HTTPStatus.CREATED) + + error = exc_info.value + assert error.quota == "max_weekly_grid_dispatches" + assert error.window_reset_on == reset + assert error.message == "Weekly grid dispatch quota reached." + assert error.retry_after is None + + +def test_generated_quota_parser_accepts_fastapi_detail_envelope(): + detail = QuotaExceededDetail.from_dict( + { + "detail": { + "quota": "max_grids", + "current": 1000, + "limit": 1000, + "message": "Grid quota reached.", + } + } + ) + + assert detail.quota == "max_grids" + assert detail.message == "Grid quota reached." diff --git a/tests/v2/test_exports.py b/tests/v2/test_exports.py new file mode 100644 index 0000000..e59c654 --- /dev/null +++ b/tests/v2/test_exports.py @@ -0,0 +1,544 @@ +""" +tests/v2/test_exports.py +""" + +# Core imports +import json +import zipfile +from http import HTTPStatus +from types import SimpleNamespace +from uuid import uuid4 + +# Internal imports +from fastfuels_sdk.v2.exports import ( + Export, + _field_source, + _landscape_field_source, + create_landscape_export, + create_quicfire_export, + get_export, + list_exports, +) +from fastfuels_sdk.v2.grids import ( + create_canopy_fuel_grid_from_landfire, + create_fuel_grid_from_fbfm40_lookup, + create_fuel_model_grid_from_landfire_fbfm40, + create_topography_grid_from_3dep, + create_uniform_grid, +) +from fastfuels_sdk.v2.client_library.models import ( + Export as ExportModel, + ExportSource, + FieldSource, + JobStatus, + LandscapeExportAlignmentDomainTarget, + LandscapeExportAlignmentGridTarget, + LandscapeFieldSource, +) +from fastfuels_sdk.v2.client_library.types import UNSET, Response +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +# External imports +import numpy as np +import pytest +import rasterio + +# The test_domain, completed_topography_grid, and completed_tree_inventory +# fixtures are session-scoped and shared across modules (tests/v2/conftest.py). +# They are READ-ONLY: tests that mutate or delete create throwaway exports. + + +class TestFieldSource: + """Pure unit tests for the (grid, band) translator (no API).""" + + def test_tuple_with_grid_object(self, completed_topography_grid): + source = _field_source((completed_topography_grid, "elevation"), "topography") + assert source.grid_id == completed_topography_grid.id + assert source.band == "elevation" + + def test_tuple_with_grid_id(self): + source = _field_source(("abc123", "fuel_depth"), "surface_fuel_depth") + assert source.grid_id == "abc123" + + def test_field_source_passes_through(self): + source = FieldSource(grid_id="abc123", band="fuel_depth") + assert _field_source(source, "surface_fuel_depth") is source + + def test_invalid_value_raises(self): + with pytest.raises(ValueError, match="surface_moisture"): + _field_source("not-a-pair", "surface_moisture") + + +def _landscape_roles(): + return { + "elevation": ("topography-grid", "elevation"), + "slope": ("topography-grid", "slope"), + "aspect": ("topography-grid", "aspect"), + "fuel_model": ("fuel-model-grid", "fbfm"), + "canopy_cover": ("canopy-grid", "cc"), + "canopy_height": ("canopy-grid", "chm"), + "canopy_base_height": ("canopy-grid", "cbh"), + "canopy_bulk_density": ("canopy-grid", "cbd"), + } + + +def _pending_export_model(): + source = ExportSource() + source.additional_properties = {"name": "landscape"} + return ExportModel( + id="landscape-export-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=source, + ) + + +class TestLandscapeFieldSource: + def test_tuple_with_grid_object(self): + source = _landscape_field_source( + (SimpleNamespace(id="grid-id"), "elevation"), "elevation" + ) + + assert source.to_dict() == {"grid_id": "grid-id", "band": "elevation"} + + def test_model_passes_through(self): + source = LandscapeFieldSource(grid_id="grid-id", band="elevation") + assert _landscape_field_source(source, "elevation") is source + + def test_invalid_value_names_role(self): + with pytest.raises(ValueError, match="canopy_height"): + _landscape_field_source("not-a-pair", "canopy_height") + + +class TestLandscapeExport: + @staticmethod + def _mock_endpoint(monkeypatch, response=None): + captured = {} + response = response or Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=_pending_export_model(), + ) + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return response + + client = object() + monkeypatch.setattr("fastfuels_sdk.v2.exports.ensure_client", lambda: client) + monkeypatch.setattr( + "fastfuels_sdk.v2.exports.create_landscape_export_endpoint.sync_detailed", + fake_create, + ) + return captured, client + + def test_builds_request(self, monkeypatch): + captured, client = self._mock_endpoint(monkeypatch) + + export = create_landscape_export( + SimpleNamespace(id="domain-id"), + fire_behavior_fuel_model="fbfm40", + name="Landscape", + tags=["test"], + **_landscape_roles(), + ) + + assert isinstance(export, Export) + assert export.id == "landscape-export-id" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert captured["body"].alignment is UNSET + assert captured["body"].fire_behavior_fuel_model.value == "fbfm40" + assert captured["body"].elevation.to_dict() == { + "grid_id": "topography-grid", + "band": "elevation", + } + assert captured["body"].canopy_bulk_density.to_dict() == { + "grid_id": "canopy-grid", + "band": "cbd", + } + assert captured["body"].name == "Landscape" + assert captured["body"].tags == ["test"] + + def test_domain_alignment(self, monkeypatch): + captured, _ = self._mock_endpoint(monkeypatch) + + create_landscape_export( + "domain-id", + fire_behavior_fuel_model="fbfm40", + resolution_m=10, + **_landscape_roles(), + ) + + assert isinstance( + captured["body"].alignment, LandscapeExportAlignmentDomainTarget + ) + assert captured["body"].alignment.resolution == 10 + + def test_grid_alignment(self, monkeypatch): + captured, _ = self._mock_endpoint(monkeypatch) + + create_landscape_export( + "domain-id", + fire_behavior_fuel_model="fbfm13", + align_to=SimpleNamespace(id="master-grid"), + **_landscape_roles(), + ) + + assert isinstance( + captured["body"].alignment, LandscapeExportAlignmentGridTarget + ) + assert captured["body"].alignment.grid_id == "master-grid" + + def test_alignment_arguments_are_exclusive(self): + with pytest.raises(ValueError, match="not both"): + create_landscape_export( + "domain-id", + fire_behavior_fuel_model="fbfm40", + resolution_m=30, + align_to="master-grid", + **_landscape_roles(), + ) + + def test_alignment_error_is_preserved(self, monkeypatch): + response = Response( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + content=b'{"detail":"fuel_model grid is not aligned with landscape"}', + headers={}, + parsed=None, + ) + self._mock_endpoint(monkeypatch, response=response) + + with pytest.raises(UnprocessableEntityException) as exc_info: + create_landscape_export( + "domain-id", + fire_behavior_fuel_model="fbfm40", + **_landscape_roles(), + ) + + assert exc_info.value.detail == ( + "fuel_model grid is not aligned with landscape" + ) + + @pytest.fixture(scope="class") + def topography_grid(self, test_domain): + grid = create_topography_grid_from_3dep( + test_domain, + source_resolution_m=10, + output_resolution_m=30, + bands=["elevation", "slope", "aspect"], + name="landscape_topography", + tags=["test"], + ) + grid.wait() + return grid + + @pytest.fixture(scope="class") + def canopy_grid(self, test_domain): + grid = create_canopy_fuel_grid_from_landfire( + test_domain, + output_resolution_m=30, + bands=["cc", "chm", "cbh", "cbd"], + name="landscape_canopy", + tags=["test"], + ) + grid.wait() + return grid + + def test_landscape_roundtrip( + self, + test_domain, + topography_grid, + completed_fbfm40_grid, + canopy_grid, + tmp_path, + ): + export = create_landscape_export( + test_domain, + fire_behavior_fuel_model="fbfm40", + elevation=(topography_grid, "elevation"), + slope=(topography_grid, "slope"), + aspect=(topography_grid, "aspect"), + fuel_model=(completed_fbfm40_grid, "fbfm"), + canopy_cover=(canopy_grid, "cc"), + canopy_height=(canopy_grid, "chm"), + canopy_base_height=(canopy_grid, "cbh"), + canopy_bulk_density=(canopy_grid, "cbd"), + name="test_landscape", + tags=["test"], + ) + try: + assert isinstance(export, Export) + assert export.status in (JobStatus.PENDING, JobStatus.RUNNING) + assert export.source["name"] == "landscape" + assert len(export.source["georeference"]["shape"]) == 2 + + export.wait() + destination = export.to_file(tmp_path / "landscape.tif") + with rasterio.open(destination) as dataset: + assert dataset.count == 8 + assert dataset.dtypes == ("int16",) * 8 + assert dataset.descriptions == ( + "Elevation", + "Slope", + "Aspect", + "Fuel Model", + "Canopy Cover", + "Canopy Height", + "Canopy Base Height", + "Canopy Bulk Density", + ) + finally: + export.delete() + + +class TestGridExportLifecycle: + @pytest.fixture(scope="class") + def completed_export(self, completed_topography_grid): + """A completed GeoTIFF export of the shared topography grid.""" + export = completed_topography_grid.export(format="geotiff", tags=["test"]) + export.wait() + return export + + def test_export_returns_wrapped_record(self, completed_topography_grid): + export = completed_topography_grid.export(format="geotiff") + assert isinstance(export, Export) + assert export.domain_id == completed_topography_grid.domain_id + + def test_completed_export_carries_signed_url(self, completed_export): + assert completed_export.status == JobStatus.COMPLETED + assert completed_export.signed_url + assert completed_export.expires_on is not None + + def test_to_file_explicit_path(self, completed_export, tmp_path): + destination = completed_export.to_file(tmp_path / "elevation.tif") + assert destination == tmp_path / "elevation.tif" + assert destination.stat().st_size > 0 + + def test_to_file_directory_uses_default_filename(self, completed_export, tmp_path): + destination = completed_export.to_file(tmp_path) + assert destination.parent == tmp_path + assert destination.stat().st_size > 0 + + def test_geotiff_matches_grid_to_numpy( + self, completed_export, completed_topography_grid, tmp_path + ): + # Ground-truth check for Grid.to_numpy: the array it reconstructs from + # the binary chunk endpoint must match the same grid rendered to a + # GeoTIFF by the server and read back with rasterio -- an independent + # reader that applies the georeference, validating absolute orientation + # too. Reuses the export fixture, so it adds no export job. + path = completed_export.to_file(tmp_path / "topography.tif") + expected = completed_topography_grid.to_numpy("elevation") + + with rasterio.open(path) as src: + band_number = src.descriptions.index("elevation") + 1 + actual = src.read(band_number).astype(float) + actual[actual == src.nodata] = np.nan + + valid = np.isfinite(actual) & np.isfinite(expected) + assert valid.any() + assert np.allclose(actual[valid], expected[valid], rtol=1e-4, atol=1e-2) + + def test_to_file_requires_completed(self, completed_topography_grid): + export = completed_topography_grid.export(format="geotiff") + if export.status == JobStatus.COMPLETED: + pytest.skip("export completed too quickly to test the guard") + with pytest.raises(ValueError, match="download"): + export.to_file("nope.tif") + + def test_from_id_and_get_export(self, completed_export): + fetched = Export.from_id(completed_export.id) + assert fetched.id == completed_export.id + assert get_export(completed_export.id).id == completed_export.id + + def test_not_found(self): + with pytest.raises(NotFoundException): + Export.from_id(uuid4().hex) + + def test_refresh_returns_self(self, completed_export): + assert completed_export.refresh() is completed_export + + def test_update_name(self, completed_export): + updated = completed_export.update(name="updated_export") + assert updated is completed_export + assert get_export(completed_export.id).name == "updated_export" + + def test_update_no_fields_makes_no_api_call(self, completed_export): + assert completed_export.update() is completed_export + + def test_list_exports(self, test_domain, completed_export): + export_ids = [e.id for e in list_exports(test_domain)] + assert completed_export.id in export_ids + + def test_list_exports_tag_filter(self, completed_export): + export_ids = [e.id for e in list_exports(tag="test")] + assert completed_export.id in export_ids + + def test_to_json(self, completed_export): + export_dict = json.loads(completed_export.to_json()) + assert export_dict["id"] == completed_export.id + + def test_delete(self, completed_topography_grid): + export = completed_topography_grid.export(format="geotiff") + export.delete() + with pytest.raises(NotFoundException): + Export.from_id(export.id) + + +class TestInventoryExport: + def test_csv_roundtrip(self, completed_tree_inventory, tmp_path): + export = completed_tree_inventory.export(format="csv") + assert isinstance(export, Export) + export.wait() + destination = export.to_file(tmp_path / "trees.csv") + header = destination.read_text().splitlines()[0] + assert "height" in header + + +class TestQuicfireExport: + @pytest.fixture(scope="class") + def voxel_grid(self, completed_tree_inventory): + """A 3D canopy grid carrying bulk density + moisture bands.""" + voxels = completed_tree_inventory.voxelize( + horizontal_resolution_m=2.0, + vertical_resolution_m=1.0, + bands=["bulk_density.foliage.live", "fuel_moisture.live"], + name="qf_voxels", + ) + voxels.wait() + return voxels + + @pytest.fixture(scope="class") + def surface_grid(self, test_domain): + """A 2 m uniform surface grid carrying load, depth, and moisture.""" + grid = create_uniform_grid( + test_domain, + resolution_m=2.0, + bands={ + "fuel_load.1hr": 0.5, + "fuel_depth": 0.3, + "fuel_moisture.1hr": 10.0, + }, + name="qf_surface", + ) + grid.wait() + return grid + + @pytest.fixture(scope="class") + def lookup_surface_grid(self, test_domain): + """Surface load + depth from a real FBFM40 lookup, on the 2 m lattice. + + Unlike the uniform ``surface_grid``, this is a genuine data-derived + grid: an FBFM40 grid built on the 2 m domain lattice, then looked up + into fuel-parameter bands. It must align cell-for-cell with the fire + grid (which defaults to the domain bbox at 2 m) for the export to + slice it without resampling. + """ + fbfm = create_fuel_model_grid_from_landfire_fbfm40( + test_domain, output_resolution_m=2.0, name="qf_fbfm" + ) + fbfm.wait() + surface = create_fuel_grid_from_fbfm40_lookup( + fbfm, bands=["fuel_load.1hr", "fuel_depth"], name="qf_surface_lookup" + ) + surface.wait() + return surface + + @pytest.fixture(scope="class") + def aligned_topography_grid(self, test_domain): + """3DEP elevation on the 2 m lattice, so it aligns with the fire grid.""" + grid = create_topography_grid_from_3dep( + test_domain, + source_resolution_m=10, + output_resolution_m=2.0, + bands=["elevation"], + name="qf_topo", + ) + grid.wait() + return grid + + def test_align_to_excludes_resolution(self, test_domain, voxel_grid): + with pytest.raises(ValueError, match="not both"): + create_quicfire_export( + test_domain, + canopy_bulk_density=(voxel_grid, "bulk_density.foliage.live"), + canopy_moisture=(voxel_grid, "fuel_moisture.live"), + surface_fuel_load=("g", "fuel_load.1hr"), + surface_fuel_depth=("g", "fuel_depth"), + surface_moisture=("g", "fuel_moisture.1hr"), + align_to=voxel_grid, + horizontal_resolution_m=2.0, + ) + + def test_bundle_roundtrip(self, test_domain, voxel_grid, surface_grid, tmp_path): + export = create_quicfire_export( + test_domain, + canopy_bulk_density=(voxel_grid, "bulk_density.foliage.live"), + canopy_moisture=(voxel_grid, "fuel_moisture.live"), + surface_fuel_load=(surface_grid, "fuel_load.1hr"), + surface_fuel_depth=(surface_grid, "fuel_depth"), + surface_moisture=(surface_grid, "fuel_moisture.1hr"), + name="qf_bundle", + ) + assert isinstance(export, Export) + assert export.status in (JobStatus.PENDING, JobStatus.RUNNING) + export.wait() + + destination = export.to_file(tmp_path) + with zipfile.ZipFile(destination) as archive: + names = set(archive.namelist()) + assert { + "treesrhof.dat", + "treesmoist.dat", + "treesfueldepth.dat", + "metadata.json", + "domain.geojson", + } <= names + export.delete() + + def test_bundle_with_lookup_surface_and_topography( + self, + test_domain, + voxel_grid, + lookup_surface_grid, + surface_grid, + aligned_topography_grid, + tmp_path, + ): + # The realistic QUIC-Fire workflow (and the export tutorial): surface + # load/depth come from a data-derived FBFM40 lookup grid rather than a + # uniform grid, and a 3DEP topography grid is supplied -- so the bundle + # must additionally contain topo.dat. Every role grid sits on the 2 m + # fire-grid lattice; the exporter crops but never resamples, so this + # also guards that an FBFM40-lookup grid and a 3DEP grid align with the + # voxel grid cell-for-cell. + export = create_quicfire_export( + test_domain, + canopy_bulk_density=(voxel_grid, "bulk_density.foliage.live"), + canopy_moisture=(voxel_grid, "fuel_moisture.live"), + surface_fuel_load=(lookup_surface_grid, "fuel_load.1hr"), + surface_fuel_depth=(lookup_surface_grid, "fuel_depth"), + surface_moisture=(surface_grid, "fuel_moisture.1hr"), + topography=(aligned_topography_grid, "elevation"), + name="qf_realistic", + ) + assert isinstance(export, Export) + export.wait() + + destination = export.to_file(tmp_path) + with zipfile.ZipFile(destination) as archive: + names = set(archive.namelist()) + assert { + "treesrhof.dat", + "treesmoist.dat", + "treesfueldepth.dat", + "topo.dat", + "metadata.json", + "domain.geojson", + } <= names + export.delete() diff --git a/tests/v2/test_features.py b/tests/v2/test_features.py new file mode 100644 index 0000000..25b633a --- /dev/null +++ b/tests/v2/test_features.py @@ -0,0 +1,315 @@ +""" +tests/v2/test_features.py +""" + +# Core imports +import json +from uuid import uuid4 + +# Internal imports +from tests import TEST_DATA_DIR +from tests.v2.utils import create_default_layerset_geojson +from fastfuels_sdk.v2.features import ( + Feature, + create_layerset_feature_from_geodataframe, + create_layerset_feature_from_geojson, + create_road_feature_from_osm, + create_water_feature_from_osm, + get_feature, + list_features, +) +from fastfuels_sdk.v2.client_library.models import FeatureType, JobStatus +from fastfuels_sdk.v2.exceptions import ( + NotFoundException, + UnprocessableEntityException, +) + +# External imports +import pytest +import geopandas as gpd + +# The test_domain, completed_road_feature, and layerset_feature fixtures are +# session-scoped and shared across modules (tests/v2/conftest.py). +# They are READ-ONLY: tests that mutate or delete create throwaways. + + +class TestCreateOsmRoadFeature: + def test_create(self, test_domain): + feature = create_road_feature_from_osm(test_domain, name="throwaway_road") + + # Feature generation is an asynchronous job + assert len(feature.id) > 0 + assert feature.domain_id == test_domain.id + assert feature.type_ == FeatureType.ROAD + assert feature.status in (JobStatus.PENDING, JobStatus.RUNNING) + assert feature.source.additional_properties["product"] == "osm" + feature.delete() + + def test_completed_fixture(self, test_domain, completed_road_feature): + assert completed_road_feature.status == JobStatus.COMPLETED + assert completed_road_feature.name == "test_road" + assert completed_road_feature.tags == ["test"] + # The georeference is populated once the job completes + assert completed_road_feature.georeference.crs.startswith("EPSG:") + assert len(completed_road_feature.georeference.bounds) == 4 + + def test_invalid_extent_buffer(self, test_domain): + with pytest.raises(UnprocessableEntityException): + create_road_feature_from_osm(test_domain, extent_buffer_m=500) + + +class TestCreateOsmWaterFeature: + def test_create(self, test_domain): + feature = create_water_feature_from_osm(test_domain, name="throwaway_water") + + assert len(feature.id) > 0 + assert feature.domain_id == test_domain.id + assert feature.type_ == FeatureType.WATER + assert feature.status in (JobStatus.PENDING, JobStatus.RUNNING) + feature.delete() + + +class TestCreateLayerset: + def test_create(self, test_domain, layerset_feature): + # Layerset uploads are synchronous: the feature is already complete + assert layerset_feature.domain_id == test_domain.id + assert layerset_feature.type_ == FeatureType.LAYERSET + assert layerset_feature.status == JobStatus.COMPLETED + assert layerset_feature.name == "test_layerset" + + def test_single_feature_is_wrapped(self, test_domain): + layerset_geojson = create_default_layerset_geojson() + feature_geojson = layerset_geojson["features"][0] + feature_geojson["crs"] = layerset_geojson["crs"] + + feature = create_layerset_feature_from_geojson(test_domain, feature_geojson) + assert feature.status == JobStatus.COMPLETED + feature.delete() + + def test_geographic_crs_rejected(self, test_domain): + # Layersets require a projected CRS; blue_mtn is EPSG:4326 + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + for feature in geojson["features"]: + feature["properties"] = create_default_layerset_geojson()["features"][0][ + "properties" + ] + + with pytest.raises(UnprocessableEntityException, match="geographic"): + create_layerset_feature_from_geojson(test_domain, geojson) + + def test_invalid_geojson_type(self, test_domain): + with pytest.raises(ValueError, match="FeatureCollection"): + create_layerset_feature_from_geojson( + test_domain, {"type": "Point", "coordinates": [0, 0]} + ) + + +class TestCreateLayersetFromGeodataframe: + def test_create(self, test_domain): + # The GeoDataFrame carries the projected CRS and the fuelbed + # property columns; both are forwarded to the API + gdf = gpd.read_file(json.dumps(create_default_layerset_geojson())) + assert gdf.crs.is_projected + + feature = create_layerset_feature_from_geodataframe( + test_domain, gdf, name="gdf_layerset" + ) + assert feature.type_ == FeatureType.LAYERSET + assert feature.status == JobStatus.COMPLETED + assert feature.get_data_metadata().total_features == len(gdf) + feature.delete() + + +class TestFromId: + def test_success(self, test_domain, layerset_feature): + feature = Feature.from_id(test_domain.id, layerset_feature.id) + assert feature.id == layerset_feature.id + assert feature.domain_id == test_domain.id + assert feature.type_ == FeatureType.LAYERSET + + def test_not_found(self, test_domain): + with pytest.raises(NotFoundException): + Feature.from_id(test_domain.id, uuid4().hex) + + +class TestRefreshFeature: + def test_refresh_returns_self(self, layerset_feature): + # refresh() updates in place and returns the same object (chains) + refreshed = layerset_feature.refresh() + assert refreshed is layerset_feature + assert refreshed.id == layerset_feature.id + + def test_get_feature_returns_new_instance(self, test_domain, layerset_feature): + # The "fetch a fresh, separate copy" use case is get_feature(...) + feature = get_feature(test_domain, layerset_feature.id) + assert feature.id == layerset_feature.id + assert feature is not layerset_feature + + +class TestWait: + def test_timeout(self, test_domain): + feature = create_water_feature_from_osm(test_domain) + with pytest.raises(TimeoutError): + feature.wait(timeout=0) + feature.delete() + + +class TestGetDataMetadata: + def test_metadata(self, completed_road_feature): + metadata = completed_road_feature.get_data_metadata() + + assert metadata.total_features > 0 + assert metadata.partition_count >= 1 + assert len(metadata.partitions) == metadata.partition_count + assert ( + sum(partition.num_features for partition in metadata.partitions) + == metadata.total_features + ) + + def test_not_completed(self, test_domain): + # Data is only available once the job is complete + feature = create_road_feature_from_osm(test_domain) + with pytest.raises(UnprocessableEntityException, match="status"): + feature.get_data_metadata() + feature.delete() + + +class TestGetDataPartition: + def test_partition(self, completed_road_feature): + metadata = completed_road_feature.get_data_metadata() + partition = completed_road_feature.get_data_partition(0) + + assert partition["type"] == "FeatureCollection" + assert len(partition["features"]) == metadata.partitions[0].num_features + assert "crs" in partition + + def test_partition_out_of_range(self, completed_road_feature): + metadata = completed_road_feature.get_data_metadata() + with pytest.raises(UnprocessableEntityException, match="out of range"): + completed_road_feature.get_data_partition(metadata.partition_count) + + +class TestGetData: + def test_get_data(self, completed_road_feature): + metadata = completed_road_feature.get_data_metadata() + data = completed_road_feature.get_data() + + assert data["type"] == "FeatureCollection" + assert len(data["features"]) == metadata.total_features + assert "crs" in data + + +class TestToGeodataframe: + def test_to_geodataframe(self, completed_road_feature): + metadata = completed_road_feature.get_data_metadata() + gdf = completed_road_feature.to_geodataframe() + + assert len(gdf) == metadata.total_features + # The data comes back in the projected CRS reported by the + # feature's georeference + assert ( + ":".join(gdf.crs.to_authority()) == completed_road_feature.georeference.crs + ) + + +class TestListFeatures: + def test_list_in_domain( + self, test_domain, completed_road_feature, layerset_feature + ): + features = list_features(test_domain) + feature_ids = [feature.id for feature in features] + assert completed_road_feature.id in feature_ids + assert layerset_feature.id in feature_ids + + def test_list_cross_domain(self, layerset_feature): + # No domain: list features across all the user's domains + features = list_features() + assert layerset_feature.id in [feature.id for feature in features] + + def test_filter_by_type( + self, test_domain, completed_road_feature, layerset_feature + ): + features = list_features(test_domain, feature_type="road") + feature_ids = [feature.id for feature in features] + assert all(feature.type_ == FeatureType.ROAD for feature in features) + assert completed_road_feature.id in feature_ids + assert layerset_feature.id not in feature_ids + + def test_filter_by_product( + self, test_domain, completed_road_feature, layerset_feature + ): + features = list_features(test_domain, product="osm") + feature_ids = [feature.id for feature in features] + assert completed_road_feature.id in feature_ids + assert layerset_feature.id not in feature_ids + + def test_filter_by_tag(self, test_domain, layerset_feature): + features = list_features(test_domain, tag="layerset-test") + assert [feature.id for feature in features] == [layerset_feature.id] + + def test_sorting(self, test_domain, layerset_feature): + features = list_features( + test_domain, sort_by="created_on", sort_order="descending" + ) + assert len(features) > 0 + + def test_invalid_sort_field(self): + with pytest.raises(ValueError): + list_features(sort_by="not_a_field") + + def test_invalid_feature_type(self): + with pytest.raises(ValueError): + list_features(feature_type="not_a_type") + + +class TestUpdateFeature: + @pytest.fixture(scope="class") + def update_feature(self, test_domain): + """A throwaway feature to mutate (the shared fixtures are read-only).""" + feature = create_layerset_feature_from_geojson( + test_domain, create_default_layerset_geojson(), name="update_target" + ) + yield feature + feature.delete() + + def test_update_name(self, test_domain, update_feature): + # update() mutates in place and returns self (chains) + updated = update_feature.update(name="updated_name") + assert updated is update_feature + assert update_feature.name == "updated_name" + # The remote resource reflects the update + assert get_feature(test_domain, update_feature.id).name == "updated_name" + + def test_update_description(self, update_feature): + updated = update_feature.update(description="updated description") + assert updated is update_feature + assert update_feature.description == "updated description" + + def test_update_tags(self, update_feature): + update_feature.update(tags=["updated"]) + assert update_feature.tags == ["updated"] + + def test_update_no_fields_makes_no_api_call(self, update_feature): + # No fields provided: returns self without touching the API + assert update_feature.update() is update_feature + + +class TestToJson: + def test_to_json(self, layerset_feature): + feature_dict = json.loads(layerset_feature.to_json()) + assert feature_dict["id"] == layerset_feature.id + assert feature_dict["domain_id"] == layerset_feature.domain_id + assert feature_dict["type"] == "layerset" + + +class TestDeleteFeature: + def test_delete_feature(self, test_domain): + feature = create_water_feature_from_osm(test_domain) + feature.delete() + + with pytest.raises(NotFoundException): + Feature.from_id(test_domain.id, feature.id) + + with pytest.raises(NotFoundException): + feature.delete() diff --git a/tests/v2/test_grids.py b/tests/v2/test_grids.py new file mode 100644 index 0000000..7b1e01e --- /dev/null +++ b/tests/v2/test_grids.py @@ -0,0 +1,1366 @@ +""" +tests/v2/test_grids.py +""" + +# Core imports +import inspect +import json +import math +from http import HTTPStatus +from types import SimpleNamespace +from uuid import uuid4 + +# Internal imports +from fastfuels_sdk.v2 import grids +from fastfuels_sdk.v2.calibrations import duet_calibration +from fastfuels_sdk.v2.grids import ( + Grid, + _build_alignment, + _decode_grid_chunk, + _domain_id, + _enum_list, + _fill_for, + _opt, + check_3dep_coverage, + create_canopy_fuel_grid_from_landfire, + create_canopy_height_grid_from_meta, + create_canopy_height_grid_from_naip_chm, + create_canopy_height_grid_from_point_cloud, + create_fuel_grid_from_fccs_lookup, + create_fuel_grid_from_fbfm13_lookup, + create_fuel_grid_from_fbfm40_lookup, + create_fuel_model_grid_from_landfire_fbfm13, + create_fuel_model_grid_from_landfire_fbfm40, + create_fuel_model_grid_from_landfire_fccs, + create_grid_from_geotiff, + create_pim_grid_from_treemap, + create_surface_fuel_grid_from_duet, + create_topography_grid_from_3dep, + create_topography_grid_from_landfire, + create_uniform_grid, + get_grid, + list_grids, +) +from fastfuels_sdk.v2.api import ensure_client +from fastfuels_sdk.v2.client_library.api.grids import get_grid_data_json +from fastfuels_sdk.v2.client_library.models import ( + Band, + BandType, + ContinuousBandSummary, + DuetBand, + FccsLookupBand, + Fbfm13LookupBand, + GridAlignmentDomainTarget, + GridAlignmentGridTarget, + GridAlignmentNativeTarget, + GridDataArrayFormat, + GridDataOrder, + GridModification, + GridModificationAction, + GridModificationCondition, + GridSource, + JobStatus, + Modifier, + Operator, + PointCloudType, + ResamplingMethod, + TopographyBand, + UploadBandDefinition, +) +from fastfuels_sdk.v2.client_library.types import UNSET, Response +from fastfuels_sdk.v2.exceptions import NotFoundException, expect +from fastfuels_sdk.v2.modifications import mask + +# External imports +import numpy as np +import pytest +import rasterio +from rasterio.transform import from_origin + +# The test_domain and completed_topography_grid fixtures are session-scoped +# and shared across modules (tests/v2/conftest.py). They are READ-ONLY: +# tests that mutate or delete create throwaways. + + +class TestBuildAlignment: + """Pure unit tests for the alignment-keyword translator (no API).""" + + def test_none_is_unset(self): + assert _build_alignment() is UNSET + + def test_output_resolution_is_domain_target(self): + alignment = _build_alignment(output_resolution_m=10) + assert isinstance(alignment, GridAlignmentDomainTarget) + assert alignment.resolution == 10 + + def test_align_native(self): + alignment = _build_alignment(align="native") + assert isinstance(alignment, GridAlignmentNativeTarget) + + def test_align_to_grid_id(self): + alignment = _build_alignment(align_to="abc123") + assert isinstance(alignment, GridAlignmentGridTarget) + assert alignment.grid_id == "abc123" + + def test_align_to_grid_object(self): + # A Grid (or any object with .id) is accepted, not just an id string + alignment = _build_alignment(align_to=SimpleNamespace(id="gid")) + assert alignment.grid_id == "gid" + + def test_resampling_method(self): + alignment = _build_alignment(output_resolution_m=10, resampling="bilinear") + assert alignment.method == ResamplingMethod.BILINEAR + + def test_conflicting_targets_raise(self): + with pytest.raises(ValueError): + _build_alignment(output_resolution_m=10, align="native") + with pytest.raises(ValueError): + _build_alignment(align_to="abc", align="native") + + def test_bad_resampling_raises(self): + with pytest.raises(ValueError): + _build_alignment(resampling="not_a_method") + + def test_bad_align_value_raises(self): + with pytest.raises(ValueError): + _build_alignment(align="bogus") + + +class TestHelpers: + """Pure unit tests for the small request-marshalling helpers (no API).""" + + def test_opt_none_is_unset(self): + assert _opt(None) is UNSET + + def test_opt_passes_values_through(self): + assert _opt("x") == "x" + # 0 and "" are not None, so they pass through unchanged + assert _opt(0) == 0 + assert _opt("") == "" + + def test_domain_id_from_string(self): + assert _domain_id("abc123") == "abc123" + + def test_domain_id_from_object(self): + assert _domain_id(SimpleNamespace(id="gid")) == "gid" + + def test_enum_list_none_is_unset(self): + assert _enum_list(None, TopographyBand) is UNSET + + def test_enum_list_coerces_strings(self): + assert _enum_list(["elevation", "slope"], TopographyBand) == [ + TopographyBand.ELEVATION, + TopographyBand.SLOPE, + ] + + def test_enum_list_passes_members_through(self): + assert _enum_list([TopographyBand.ASPECT], TopographyBand) == [ + TopographyBand.ASPECT + ] + + def test_enum_list_invalid_value_raises(self): + with pytest.raises(ValueError): + _enum_list(["not_a_band"], TopographyBand) + + +class TestFccsSignature: + """FCCS create reached alignment parity with the other LANDFIRE creators + (FastFuels-API-v2 #358), so the SDK creator exposes the same kwargs.""" + + def test_fccs_exposes_alignment_kwargs(self): + params = inspect.signature( + grids.create_fuel_model_grid_from_landfire_fccs + ).parameters + for expected in ( + "output_resolution_m", + "align_to", + "align", + "resampling", + "extent_buffer_cells", + ): + assert expected in params + + def test_fbfm40_has_alignment_kwargs(self): + params = inspect.signature( + grids.create_fuel_model_grid_from_landfire_fbfm40 + ).parameters + assert "output_resolution_m" in params + assert "align_to" in params + + +class TestCreateTopographyGridFrom3dep: + def test_create(self, test_domain): + grid = create_topography_grid_from_3dep( + test_domain, output_resolution_m=10, name="throwaway_topo" + ) + + # Grid generation is an asynchronous job + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + assert grid.source is not None + grid.delete() + + def test_completed_fixture(self, completed_topography_grid): + assert completed_topography_grid.status == JobStatus.COMPLETED + assert completed_topography_grid.name == "test_topography" + assert completed_topography_grid.tags == ["test"] + # The georeference and chunk layout are populated once complete + assert completed_topography_grid.georeference is not None + assert len(completed_topography_grid.bands) > 0 + + +class TestCreateTopographyGridFromLandfire: + def test_create(self, test_domain): + grid = create_topography_grid_from_landfire( + test_domain, + output_resolution_m=30, + bands=["elevation"], + name="throwaway_lf_topo", + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + assert grid.source is not None + grid.delete() + + +class TestCreateCanopyFuelGridFromLandfire: + def test_create(self, test_domain): + grid = create_canopy_fuel_grid_from_landfire( + test_domain, + output_resolution_m=30, + bands=["cbd"], + name="throwaway_canopy_fuel", + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreateCanopyHeightGridFromMeta: + def test_create(self, test_domain): + grid = create_canopy_height_grid_from_meta( + test_domain, output_resolution_m=30, name="throwaway_meta_chm" + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreateCanopyHeightGridFromNaipChm: + def test_create(self, test_domain): + grid = create_canopy_height_grid_from_naip_chm( + test_domain, output_resolution_m=30, name="throwaway_naip_chm" + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreateCanopyHeightGridFromPointCloud: + @staticmethod + def _point_cloud(status=JobStatus.COMPLETED, type_=PointCloudType.ALS): + return SimpleNamespace( + id="pc-id", + domain_id="domain-id", + status=status, + type_=type_, + ) + + def test_builds_request_from_completed_als_cloud(self, monkeypatch): + created = Grid( + id="grid-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=GridSource(), + bands=[Band(key="chm", type_=BandType.CONTINUOUS, index=0, unit="m")], + ) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_point_cloud_chm, + "sync_detailed", + fake_create, + ) + + grid = create_canopy_height_grid_from_point_cloud( + self._point_cloud(), + output_resolution_m=2, + name="Point-cloud CHM", + ) + + assert isinstance(grid, Grid) + assert grid.id == "grid-id" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert captured["body"].source_point_cloud_id == "pc-id" + assert captured["body"].alignment.resolution == 2 + assert captured["body"].name == "Point-cloud CHM" + + def test_requires_completed_cloud(self): + with pytest.raises(ValueError, match="must be completed"): + create_canopy_height_grid_from_point_cloud( + self._point_cloud(status=JobStatus.PENDING) + ) + + def test_requires_airborne_cloud(self): + with pytest.raises(ValueError, match="airborne"): + create_canopy_height_grid_from_point_cloud( + self._point_cloud(type_=PointCloudType.TLS) + ) + + +class TestCreateSurfaceFuelGridFromDuet: + @staticmethod + def _source_grid( + status=JobStatus.COMPLETED, + omit=None, + ): + required = [ + ("bulk_density.foliage.live", BandType.CONTINUOUS), + ("spcd", BandType.CATEGORICAL), + ("fuel_moisture.live", BandType.CONTINUOUS), + ] + return Grid( + id="tree-grid-id", + domain_id="domain-id", + status=status, + source=GridSource(), + bands=[ + Band(key=key, type_=type_, index=index) + for index, (key, type_) in enumerate(required) + if key != omit + ], + ) + + def test_builds_request_from_completed_tree_grid(self, monkeypatch): + created = Grid( + id="duet-grid-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=GridSource(), + bands=[], + ) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + calibration = duet_calibration(fuel_load={"grass": {"mean": 0.5, "sd": 0.25}}) + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_duet_grid, + "sync_detailed", + fake_create, + ) + + grid = create_surface_fuel_grid_from_duet( + self._source_grid(), + years_since_burn=20, + wind_direction=225, + wind_variability=45, + bands=["fuel_load.grass", DuetBand.FUEL_LOAD_LITTER], + calibration=calibration, + name="DUET surface fuels", + tags=["test"], + ) + + assert grid.id == "duet-grid-id" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert captured["body"].source_grid_id == "tree-grid-id" + assert captured["body"].years_since_burn == 20 + assert captured["body"].wind_direction == 225 + assert captured["body"].wind_variability == 45 + assert captured["body"].bands == [ + DuetBand.FUEL_LOAD_GRASS, + DuetBand.FUEL_LOAD_LITTER, + ] + assert captured["body"].calibration is calibration + assert captured["body"].name == "DUET surface fuels" + assert captured["body"].tags == ["test"] + + def test_requires_completed_tree_grid(self): + with pytest.raises(ValueError, match=r"Call \.wait\(\)"): + create_surface_fuel_grid_from_duet( + self._source_grid(status=JobStatus.PENDING), + years_since_burn=20, + ) + + def test_requires_duet_source_bands(self): + with pytest.raises(ValueError, match="fuel_moisture.live"): + create_surface_fuel_grid_from_duet( + self._source_grid(omit="fuel_moisture.live"), + years_since_burn=20, + ) + + @pytest.mark.parametrize( + "kwargs,error", + [ + ({"years_since_burn": 0}, ValueError), + ({"years_since_burn": 101}, ValueError), + ({"years_since_burn": 1.5}, TypeError), + ({"years_since_burn": 20, "wind_direction": 360}, ValueError), + ({"years_since_burn": 20, "wind_variability": 181}, ValueError), + ({"years_since_burn": 20, "bands": []}, ValueError), + ( + { + "years_since_burn": 20, + "bands": ["fuel_load.grass", "fuel_load.grass"], + }, + ValueError, + ), + ], + ) + def test_validates_request_parameters(self, kwargs, error): + with pytest.raises(error): + create_surface_fuel_grid_from_duet(self._source_grid(), **kwargs) + + def test_create_live(self, completed_tree_inventory): + voxels = completed_tree_inventory.voxelize( + horizontal_resolution_m=2, + vertical_resolution_m=1, + bands=[ + "bulk_density.foliage.live", + "spcd", + "fuel_moisture.live", + ], + name="test_duet_source", + tags=["test"], + ) + surface = None + try: + voxels.wait() + surface = create_surface_fuel_grid_from_duet( + voxels, + years_since_burn=25, + bands=[ + "fuel_load.grass", + "fuel_load.litter", + "fuel_depth.grass", + "fuel_depth.litter", + ], + calibration=duet_calibration( + fuel_load={ + "grass": {"mean": 0.5, "sd": 0.25}, + "litter": {"max": 5, "min": 0}, + }, + fuel_depth={ + "grass": {"value": 0.3}, + "litter": {"value": 0.06}, + }, + ), + name="test_duet_surface_fuels", + tags=["test"], + ) + surface.wait() + assert surface.status == JobStatus.COMPLETED + assert {band.key for band in surface.bands} == { + "fuel_load.grass", + "fuel_load.litter", + "fuel_depth.grass", + "fuel_depth.litter", + } + grass_load = surface.to_numpy("fuel_load.grass") + assert grass_load.ndim == 2 + assert np.isfinite(grass_load).any() + finally: + if surface is not None: + surface.delete() + voxels.delete() + + +class TestCreateFuelModelGridFromLandfireFbfm40: + def test_create(self, test_domain): + # remove_non_burnable exercises the string -> enum list coercion + grid = create_fuel_model_grid_from_landfire_fbfm40( + test_domain, + output_resolution_m=30, + remove_non_burnable=["NB1", "NB2"], + name="throwaway_fbfm40", + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreateFuelModelGridFromLandfireFbfm13: + def test_builds_request(self, monkeypatch): + created = Grid( + id="fbfm13-grid-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=GridSource(), + bands=[], + ) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_landfire_fbfm13, + "sync_detailed", + fake_create, + ) + + grid = create_fuel_model_grid_from_landfire_fbfm13( + SimpleNamespace(id="domain-id"), + version="2024", + remove_non_burnable=["NB1", "NB2"], + output_resolution_m=30, + name="Anderson 13", + ) + + assert grid.id == "fbfm13-grid-id" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert captured["body"].version.value == "2024" + assert [value.value for value in captured["body"].remove_non_burnable] == [ + "NB1", + "NB2", + ] + assert captured["body"].alignment.resolution == 30 + assert captured["body"].name == "Anderson 13" + + def test_completed_fixture(self, completed_fbfm13_grid): + assert completed_fbfm13_grid.status == JobStatus.COMPLETED + assert [band.key for band in completed_fbfm13_grid.bands] == ["fbfm13"] + + +class TestMask: + def test_mask_payload_shape(self): + # Unit: a single band masks one feature to one replacement value + mod = mask("feat123", "fbfm", 91, buffer_m=5, target="cell") + payload = mod.to_dict() + assert payload["conditions"] == [ + { + "source": "feature", + "operator": "within", + "feature_id": "feat123", + "buffer_m": 5, + "target": "cell", + } + ] + assert payload["actions"] == [ + {"band": "fbfm", "modifier": "replace", "value": 91} + ] + + def test_mask_multiple_bands(self): + # A list of bands fans out to one action per band, sharing the condition + mod = mask("feat999", ["fuel_load.1hr", "fuel_depth"]) + bands = [action["band"] for action in mod.to_dict()["actions"]] + assert bands == ["fuel_load.1hr", "fuel_depth"] + + def test_mask_accepts_feature_object(self): + # A Feature-like object contributes its id + mod = mask(SimpleNamespace(id="feat-from-object"), "fbfm") + assert mod.to_dict()["conditions"][0]["feature_id"] == "feat-from-object" + + def test_mask_applied_to_grid_creation(self, test_domain, completed_road_feature): + # Live: masking an FBFM40 grid against a completed road feature + grid = create_fuel_model_grid_from_landfire_fbfm40( + test_domain, + output_resolution_m=30, + modifications=[mask(completed_road_feature, "fbfm", 91, buffer_m=5)], + name="throwaway_masked_fbfm40", + ) + assert len(grid.id) > 0 + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreateFuelModelGridFromLandfireFccs: + def test_create(self, test_domain): + grid = create_fuel_model_grid_from_landfire_fccs( + test_domain, remove_bare_ground=True, name="throwaway_fccs" + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + def test_create_with_alignment(self, test_domain): + # #358 brought FCCS to alignment parity; output_resolution_m anchors + # output cells to the domain origin, like the other LANDFIRE creators. + grid = create_fuel_model_grid_from_landfire_fccs( + test_domain, output_resolution_m=30, name="throwaway_fccs_aligned" + ) + assert len(grid.id) > 0 + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestCreatePimGridFromTreemap: + def test_create(self, test_domain): + grid = create_pim_grid_from_treemap( + test_domain, + output_resolution_m=30, + resampling="nearest", + name="throwaway_treemap", + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + grid.delete() + + +class TestUploadCreators: + """The GeoTIFF/NetCDF upload creators need a binary raster whose CRS + matches the domain CRS. The GeoTIFF path builds one on the fly with + rasterio; the NetCDF path is deferred (no on-the-fly NetCDF writer here). + """ + + def test_create_grid_from_geotiff(self, test_domain, tmp_path): + # Build a single-band float GeoTIFF on the domain's lattice and CRS, + # upload it, and confirm it processes to completion. This exercises the + # signed-upload header contract end-to-end (the unit-level check lives + # in tests/v2/test_uploads.py); the grid upload path was missing the + # GCS x-goog-content-length-range header until that was centralized. + minx, miny, maxx, maxy = test_domain.bbox + crs = test_domain.get_lattice(resolution=30.0).crs + res = 30.0 + width = max(1, math.ceil((maxx - minx) / res)) + height = max(1, math.ceil((maxy - miny) / res)) + data = np.arange(width * height, dtype="float32").reshape(height, width) + path = tmp_path / "elevation.tif" + with rasterio.open( + path, + "w", + driver="GTiff", + height=height, + width=width, + count=1, + dtype="float32", + crs=crs, + transform=from_origin(minx, maxy, res, res), + ) as dst: + dst.write(data, 1) + + grid = create_grid_from_geotiff( + test_domain, + str(path), + bands=[UploadBandDefinition(key="elevation", type_=BandType.CONTINUOUS)], + name="throwaway_geotiff", + ) + assert grid.domain_id == test_domain.id + grid.wait() + assert grid.status == JobStatus.COMPLETED + assert [b.key for b in grid.bands] == ["elevation"] + grid.delete() + + @pytest.mark.skip( + reason="needs an on-the-fly NetCDF writer matching the domain CRS" + ) + def test_create_grid_from_netcdf(self, test_domain): + pass + + +class TestCreateUniformGrid: + def test_create(self, test_domain): + grid = create_uniform_grid( + test_domain, + resolution_m=30, + bands={"fuel_depth": 0.5, "fuel_load.1hr": 0.2}, + name="throwaway_uniform", + ) + assert len(grid.id) > 0 + assert grid.domain_id == test_domain.id + assert grid.status in ( + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.COMPLETED, + ) + grid.delete() + + def test_invalid_band_key(self, test_domain): + with pytest.raises(ValueError): + create_uniform_grid(test_domain, resolution_m=30, bands={"not_a_band": 1.0}) + + +class TestFromId: + def test_success(self, test_domain, completed_topography_grid): + grid = Grid.from_id(test_domain.id, completed_topography_grid.id) + assert grid.id == completed_topography_grid.id + assert grid.domain_id == test_domain.id + + def test_not_found(self, test_domain): + with pytest.raises(NotFoundException): + Grid.from_id(test_domain.id, uuid4().hex) + + +class TestGetGrid: + def test_get_grid_returns_new_instance( + self, test_domain, completed_topography_grid + ): + grid = get_grid(test_domain, completed_topography_grid.id) + assert grid.id == completed_topography_grid.id + assert grid is not completed_topography_grid + + +class TestRefreshGrid: + def test_refresh_returns_self(self, completed_topography_grid): + refreshed = completed_topography_grid.refresh() + assert refreshed is completed_topography_grid + assert refreshed.id == completed_topography_grid.id + + +class TestWait: + def test_timeout(self, test_domain): + grid = create_topography_grid_from_3dep(test_domain, output_resolution_m=10) + if grid.status == JobStatus.COMPLETED: + grid.delete() + pytest.skip("grid completed too quickly to test the timeout") + with pytest.raises(TimeoutError): + grid.wait(timeout=0) + grid.delete() + + +class TestUpdateGrid: + @pytest.fixture(scope="class") + def update_grid(self, test_domain): + """A throwaway grid to mutate (the shared fixtures are read-only).""" + grid = create_uniform_grid( + test_domain, + resolution_m=30, + bands={"fuel_depth": 0.5}, + name="update_target", + ) + yield grid + grid.delete() + + def test_update_name(self, test_domain, update_grid): + # update() mutates in place and returns self (chains) + updated = update_grid.update(name="updated_name") + assert updated is update_grid + assert update_grid.name == "updated_name" + assert get_grid(test_domain, update_grid.id).name == "updated_name" + + def test_update_tags(self, update_grid): + update_grid.update(tags=["updated"]) + assert update_grid.tags == ["updated"] + + def test_update_no_fields_makes_no_api_call(self, update_grid): + assert update_grid.update() is update_grid + + +class TestResample: + def test_resample_returns_new_pending_grid(self, completed_topography_grid): + resampled = completed_topography_grid.resample( + output_resolution_m=30, name="resampled" + ) + assert isinstance(resampled, Grid) + assert resampled.id != completed_topography_grid.id + assert resampled.domain_id == completed_topography_grid.domain_id + resampled.delete() + + def test_resample_requires_completed_source(self, test_domain): + grid = create_topography_grid_from_3dep(test_domain, output_resolution_m=10) + if grid.status == JobStatus.COMPLETED: + grid.delete() + pytest.skip("grid completed too quickly to test the guard") + with pytest.raises(ValueError, match="resample"): + grid.resample(output_resolution_m=30) + grid.delete() + + +class TestApplyModifications: + def test_apply_modifications_requires_completed(self): + # Pure guard: a non-completed grid raises before any API call. + grid = Grid( + id="g", + domain_id="d", + status=JobStatus.PENDING, + source=GridSource(), + bands=[], + ) + with pytest.raises(ValueError, match="apply modifications"): + grid.apply_modifications([]) + + def test_apply_modifications_rederives_in_place(self, test_domain): + # A throwaway uniform grid -- grids have no duplicate yet (#16) and the + # shared fixture is read-only. A value-based modification (band > 0, + # matching every cell) avoids needing a feature. + grid = create_uniform_grid( + test_domain, + resolution_m=30, + bands={"fuel_load.1hr": 0.5}, + name="grid_modify_test", + ) + grid.wait() + original_checksum = grid.checksum + + modification = GridModification( + conditions=[ + GridModificationCondition( + band="fuel_load.1hr", operator=Operator.GT, value=0 + ) + ], + actions=[ + GridModificationAction( + band="fuel_load.1hr", modifier=Modifier.MULTIPLY, value=0.9 + ) + ], + ) + modified = grid.apply_modifications([modification]) + + assert modified is grid # in place: same object, same id + assert modified.id == grid.id + # The grid re-derives in place; once it settles its content has + # changed (the multiply-by-0.9 action), so the checksum differs. (The + # `modifications` list is not echoed in the immediate pending response, + # unlike inventories.) + grid.wait() + assert grid.status == JobStatus.COMPLETED + assert grid.checksum != original_checksum + grid.delete() + + +class TestFbfm40Lookup: + def test_lookup_returns_new_pending_grid(self, completed_fbfm40_grid): + fuel_grid = create_fuel_grid_from_fbfm40_lookup( + completed_fbfm40_grid, + bands=["fuel_load.1hr", "fuel_depth"], + name="throwaway_lookup", + ) + assert isinstance(fuel_grid, Grid) + assert fuel_grid.id != completed_fbfm40_grid.id + assert fuel_grid.domain_id == completed_fbfm40_grid.domain_id + assert fuel_grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + fuel_grid.delete() + + def test_lookup_requires_completed_source(self, test_domain): + grid = create_fuel_model_grid_from_landfire_fbfm40( + test_domain, output_resolution_m=30 + ) + if grid.status == JobStatus.COMPLETED: + grid.delete() + pytest.skip("grid completed too quickly to test the guard") + with pytest.raises(ValueError, match="look up fuel"): + create_fuel_grid_from_fbfm40_lookup(grid, bands=["fuel_load.1hr"]) + grid.delete() + + def test_lookup_rejects_non_fbfm_grid(self, completed_topography_grid): + # A topography grid has no `fbfm` band; the guard rejects it before any + # API call, naming the missing band and the right creator to use. + with pytest.raises(ValueError, match="fbfm"): + create_fuel_grid_from_fbfm40_lookup( + completed_topography_grid, bands=["fuel_load.1hr"] + ) + + +class TestFbfm13Lookup: + @staticmethod + def _source(status=JobStatus.COMPLETED, band="fbfm13"): + return Grid( + id="fbfm13-grid-id", + domain_id="domain-id", + status=status, + source=GridSource(), + bands=[Band(key=band, type_=BandType.CATEGORICAL, index=0)], + ) + + def test_builds_request(self, monkeypatch): + created = Grid( + id="fuel-grid-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=GridSource(), + bands=[], + ) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_fbfm13_lookup, + "sync_detailed", + fake_create, + ) + + result = create_fuel_grid_from_fbfm13_lookup( + self._source(), + bands=["fuel_load.1hr", Fbfm13LookupBand.FUEL_DEPTH], + name="Anderson fuel parameters", + ) + + assert result.id == "fuel-grid-id" + assert captured["body"].source_grid_id == "fbfm13-grid-id" + assert captured["body"].source_band == "fbfm13" + assert captured["body"].bands == [ + Fbfm13LookupBand.FUEL_LOAD_1HR, + Fbfm13LookupBand.FUEL_DEPTH, + ] + assert captured["body"].name == "Anderson fuel parameters" + + def test_lookup_returns_new_pending_grid(self, completed_fbfm13_grid): + fuel_grid = create_fuel_grid_from_fbfm13_lookup( + completed_fbfm13_grid, + bands=["fuel_load.1hr", "fuel_depth"], + name="throwaway_fbfm13_lookup", + ) + assert fuel_grid.id != completed_fbfm13_grid.id + assert fuel_grid.domain_id == completed_fbfm13_grid.domain_id + assert fuel_grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + fuel_grid.delete() + + def test_requires_completed_source(self): + with pytest.raises(ValueError, match="look up fuel"): + create_fuel_grid_from_fbfm13_lookup( + self._source(status=JobStatus.PENDING), + bands=["fuel_load.1hr"], + ) + + def test_rejects_non_fbfm13_grid(self): + with pytest.raises(ValueError, match="fbfm13"): + create_fuel_grid_from_fbfm13_lookup( + self._source(band="elevation"), + bands=["fuel_load.1hr"], + ) + + +class TestFccsLookup: + @staticmethod + def _source(status=JobStatus.COMPLETED, band="fccs"): + return Grid( + id="fccs-grid-id", + domain_id="domain-id", + status=status, + source=GridSource(), + bands=[Band(key=band, type_=BandType.CATEGORICAL, index=0)], + ) + + def test_builds_request(self, monkeypatch): + created = Grid( + id="fuel-grid-id", + domain_id="domain-id", + status=JobStatus.PENDING, + source=GridSource(), + bands=[], + ) + captured = {} + + def fake_create(domain_id, *, client, body): + captured.update(domain_id=domain_id, client=client, body=body) + return Response( + status_code=HTTPStatus.CREATED, + content=b"", + headers={}, + parsed=created, + ) + + client = object() + monkeypatch.setattr(grids, "ensure_client", lambda: client) + monkeypatch.setattr( + grids.create_fccs_lookup, + "sync_detailed", + fake_create, + ) + + result = create_fuel_grid_from_fccs_lookup( + self._source(), + bands=["fuel_load.duff", FccsLookupBand.DUFF_DEPTH], + name="FCCS fuel parameters", + tags=["test"], + ) + + assert result.id == "fuel-grid-id" + assert captured["domain_id"] == "domain-id" + assert captured["client"] is client + assert captured["body"].source_grid_id == "fccs-grid-id" + assert captured["body"].source_band == "fccs" + assert captured["body"].bands == [ + FccsLookupBand.FUEL_LOAD_DUFF, + FccsLookupBand.DUFF_DEPTH, + ] + assert captured["body"].name == "FCCS fuel parameters" + assert captured["body"].tags == ["test"] + + def test_lookup_returns_new_pending_grid(self, completed_fccs_grid): + fuel_grid = create_fuel_grid_from_fccs_lookup( + completed_fccs_grid, + bands=[ + "fuel_load.litter", + "fuel_load.duff", + "duff_depth", + "fuel_load.live_shrub", + ], + name="throwaway_fccs_lookup", + ) + assert fuel_grid.id != completed_fccs_grid.id + assert fuel_grid.domain_id == completed_fccs_grid.domain_id + assert fuel_grid.status in (JobStatus.PENDING, JobStatus.RUNNING) + fuel_grid.delete() + + def test_requires_completed_source(self): + with pytest.raises(ValueError, match="look up fuel"): + create_fuel_grid_from_fccs_lookup( + self._source(status=JobStatus.PENDING), + bands=["fuel_load.duff"], + ) + + def test_rejects_non_fccs_grid(self): + with pytest.raises(ValueError, match="fccs"): + create_fuel_grid_from_fccs_lookup( + self._source(band="elevation"), + bands=["fuel_load.duff"], + ) + + +class TestExport: + def test_export_returns_pending_export(self, completed_topography_grid): + # Topography is 2D, so a GeoTIFF export is valid + export = completed_topography_grid.export(format="geotiff") + assert len(export.id) > 0 + assert export.domain_id == completed_topography_grid.domain_id + assert export.status in ( + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.COMPLETED, + ) + + +class TestCheck3depCoverage: + def test_coverage(self, test_domain): + coverage = check_3dep_coverage(test_domain, resolution_m=10) + # The test domain sits in CONUS, which 3DEP 10 m covers + assert coverage.available is True + assert coverage.tile_count >= 1 + + +class TestListGrids: + def test_list_in_domain(self, test_domain, completed_topography_grid): + grid_ids = [grid.id for grid in list_grids(test_domain)] + assert completed_topography_grid.id in grid_ids + + def test_list_cross_domain(self, completed_topography_grid): + # No domain: list grids across all the user's domains. The SDK returns + # one page at a time, so search subsequent pages when the account has + # more than the default page size of 100 grids. + for page in range(100): + grids = list_grids( + page=page, + size=100, + sort_by="created_on", + sort_order="descending", + ) + if completed_topography_grid.id in [grid.id for grid in grids]: + return + if len(grids) < 100: + break + + pytest.fail( + f"Grid {completed_topography_grid.id} was not found in the " + "cross-domain grid pages." + ) + + def test_filter_by_tag(self, test_domain, completed_topography_grid): + grids_with_tag = list_grids(test_domain, tag="test") + assert completed_topography_grid.id in [grid.id for grid in grids_with_tag] + + def test_invalid_sort_field(self): + with pytest.raises(ValueError): + list_grids(sort_by="not_a_field") + + +class TestToJson: + def test_to_json(self, completed_topography_grid): + grid_dict = json.loads(completed_topography_grid.to_json()) + assert grid_dict["id"] == completed_topography_grid.id + assert grid_dict["domain_id"] == completed_topography_grid.domain_id + + +class TestDeleteGrid: + def test_delete(self, test_domain): + grid = create_uniform_grid( + test_domain, resolution_m=30, bands={"fuel_depth": 0.5} + ) + grid.delete() + + with pytest.raises(NotFoundException): + Grid.from_id(test_domain.id, grid.id) + + with pytest.raises(NotFoundException): + grid.delete() + + +class TestDuplicateGrid: + def test_duplicate_is_a_clone(self, completed_topography_grid): + # Duplicating creates a new grid (the shared fixture is not mutated), + # byte-copying the data so the copy carries the same checksum. + copy = completed_topography_grid.duplicate(name="grid_duplicate_test") + assert copy.id != completed_topography_grid.id + assert copy.name == "grid_duplicate_test" + assert copy.domain_id == completed_topography_grid.domain_id + copy.wait() + assert copy.status == JobStatus.COMPLETED + assert copy.checksum == completed_topography_grid.checksum + copy.delete() + + +class TestDecodeGridChunk: + """Offline unit tests for binary chunk decoding (no API required).""" + + def test_dense_c_order(self): + values = np.arange(6, dtype=np.float32) + offset, block = _decode_grid_chunk( + values.tobytes(), + { + "X-Data-Shape": "2,3", + "X-Data-Offset": "0,0", + "X-Data-Order": "C", + "X-Data-Format": "dense", + "X-Data-Dtype": "float32", + }, + ) + assert offset == (0, 0) + assert block.dtype == np.float32 + assert np.array_equal(block, np.arange(6).reshape(2, 3)) + + def test_dense_f_order_with_offset(self): + values = np.arange(6, dtype=np.float32) + offset, block = _decode_grid_chunk( + values.tobytes(), + { + "X-Data-Shape": "2,3", + "X-Data-Offset": "4,8", + "X-Data-Order": "F", + "X-Data-Format": "dense", + "X-Data-Dtype": "float32", + }, + ) + assert offset == (4, 8) + assert np.array_equal(block, np.arange(6).reshape(2, 3, order="F")) + + def test_sparse_with_fill_value(self): + content = ( + np.array([1], dtype=np.int32).tobytes() + + np.array([9.0], dtype=np.float32).tobytes() + ) + _, block = _decode_grid_chunk( + content, + { + "X-Data-Shape": "2,2", + "X-Data-Offset": "0,0", + "X-Data-Order": "C", + "X-Data-Format": "sparse", + "X-Data-NNZ": "1", + "X-Data-Index-Dtype": "int32", + "X-Data-Value-Dtype": "float32", + "X-Data-Fill-Value": "0", + }, + ) + assert np.array_equal(block, np.array([[0, 9], [0, 0]], dtype=np.float32)) + + def test_sparse_without_fill_uses_nan(self): + content = ( + np.array([0, 3], dtype=np.int32).tobytes() + + np.array([5.0, 7.0], dtype=np.float32).tobytes() + ) + _, block = _decode_grid_chunk( + content, + { + "X-Data-Shape": "2,2", + "X-Data-Offset": "0,0", + "X-Data-Order": "C", + "X-Data-Format": "sparse", + "X-Data-NNZ": "2", + "X-Data-Index-Dtype": "int32", + "X-Data-Value-Dtype": "float32", + }, + ) + assert block[0, 0] == 5 and block[1, 1] == 7 + assert np.isnan(block[0, 1]) and np.isnan(block[1, 0]) + + def test_fill_for(self): + assert np.isnan(_fill_for(np.dtype("float32"))) + assert _fill_for(np.dtype("int32")) == 0 + assert _fill_for(np.dtype("float32"), -9999) == -9999 + assert _fill_for(np.dtype("int32"), UNSET) == 0 + + +def _reassemble_band_via_json(grid, band): + """Reassemble one band from the JSON chunk endpoint, independently of + ``Grid.to_numpy``. + + This shares no code with the binary path: it uses the generated client's + fully-typed JSON parser (``get_grid_data_json``) rather than hand-decoding + raw bytes. Comparing the two arrays validates the binary decode end to end + — dtype, byte order, sparse split, reshape order, and chunk placement. + """ + is_3d = len(grid.georeference.shape) == 3 + array_format = GridDataArrayFormat.SPARSE if is_3d else GridDataArrayFormat.DENSE + full = np.full(tuple(grid.georeference.shape), np.nan) + for chunk_index in range(grid.chunks.count): + response = expect( + get_grid_data_json.sync_detailed( + grid.domain_id, + grid.id, + band, + chunk_index, + client=ensure_client(), + array_format=array_format, + order=GridDataOrder.C, + ) + ) + shape = response.shape + order = response.order.value + if response.data.format_ == "dense": + block = np.array(response.data.values, dtype=float).reshape( + shape, order=order + ) + else: + fill = response.data.fill_value + flat = np.full(int(np.prod(shape)), np.nan if fill is None else float(fill)) + flat[np.array(response.data.indices, dtype=int)] = response.data.values + block = flat.reshape(shape, order=order) + slices = tuple(slice(o, o + s) for o, s in zip(response.metadata.offset, shape)) + full[slices] = block + return full + + +class TestDataOut: + """Live tests for reading grid data into memory.""" + + def test_to_numpy_topography(self, completed_topography_grid): + grid = completed_topography_grid + array = grid.to_numpy(grid.bands[0].key) + assert array.shape == tuple(grid.georeference.shape) + assert array.ndim == len(grid.georeference.shape) + assert np.isfinite(array).any() + + def test_to_numpy_matches_json_transport(self, completed_topography_grid): + # The binary reconstruction must agree, value for value, with an + # independent reassembly over the JSON chunk endpoint. + grid = completed_topography_grid + band = grid.bands[0].key + binary = grid.to_numpy(band) + reference = _reassemble_band_via_json(grid, band) + assert binary.shape == reference.shape + assert np.allclose(binary, reference, equal_nan=True) + + def test_topography_values_are_plausible(self, completed_topography_grid): + # Guards against all-zero / constant-fill / garbage decodes that a + # shape-only check would miss: real terrain varies and sits within + # Earth's elevation range (meters). + grid = completed_topography_grid + elevation = grid.to_numpy("elevation") + finite = elevation[np.isfinite(elevation)] + assert finite.size > 0 + assert finite.std() > 0 + assert -500 < finite.min() and finite.max() < 9000 + + def test_to_numpy_pim(self, completed_pim_grid): + # Exercises whichever encoding the PIM grid uses (3D -> sparse). + grid = completed_pim_grid + array = grid.to_numpy(grid.bands[0].key) + assert array.shape == tuple(grid.georeference.shape) + + def test_to_numpy_unknown_band(self, completed_topography_grid): + with pytest.raises(ValueError): + completed_topography_grid.to_numpy("not_a_band") + + def test_to_numpy_requires_completed(self, test_domain): + grid = create_topography_grid_from_3dep(test_domain, output_resolution_m=10) + try: + with pytest.raises(ValueError): + grid.to_numpy("elevation") + finally: + grid.delete() + + def test_to_xarray(self, completed_topography_grid): + grid = completed_topography_grid + dataset = grid.to_xarray() + assert set(dataset.data_vars) == {band.key for band in grid.bands} + assert dataset.sizes["x"] == grid.georeference.shape[-1] + assert dataset.sizes["y"] == grid.georeference.shape[-2] + assert dataset.attrs["crs"] == grid.georeference.crs + + +class TestBandSummary: + """Unit tests for the band-summary accessor (no API), plus a live check.""" + + def _grid_with_band(self, band): + return Grid( + id="g", + domain_id="d", + status=JobStatus.COMPLETED, + source=GridSource(), + bands=[band], + ) + + def test_returns_band_summary(self): + summary = ContinuousBandSummary( + type_="continuous", + count=10, + nodata_count=0, + min_=1.0, + max_=5.0, + mean=3.0, + std=1.0, + ) + grid = self._grid_with_band( + Band(key="elevation", type_=BandType.CONTINUOUS, index=0, summary=summary) + ) + assert grid.band_summary("elevation") is summary + assert grid.band_summary("elevation").mean == 3.0 + + def test_none_when_not_computed(self): + # summary defaults to UNSET (e.g. a pending grid) -> normalized to None + grid = self._grid_with_band( + Band(key="elevation", type_=BandType.CONTINUOUS, index=0) + ) + assert grid.band_summary("elevation") is None + + def test_unknown_band_raises(self): + grid = self._grid_with_band( + Band(key="elevation", type_=BandType.CONTINUOUS, index=0) + ) + with pytest.raises(ValueError, match="no band"): + grid.band_summary("nope") + + def test_continuous_summary_live(self, completed_topography_grid): + summary = completed_topography_grid.band_summary("elevation") + assert summary.type_ == "continuous" + assert summary.count > 0 + assert summary.mean is not None diff --git a/tests/v2/test_inventories.py b/tests/v2/test_inventories.py new file mode 100644 index 0000000..c7be653 --- /dev/null +++ b/tests/v2/test_inventories.py @@ -0,0 +1,707 @@ +""" +tests/v2/test_inventories.py +""" + +# Core imports +import json +from http import HTTPStatus +from types import SimpleNamespace +from uuid import uuid4 + +# Internal imports +from fastfuels_sdk.v2.grids import Grid +from fastfuels_sdk.v2.inventories import ( + Inventory, + create_tree_inventory_from_file, + create_tree_inventory_from_gdam, + create_tree_inventory_from_pim_grid, + get_inventory, + list_inventories, +) +from fastfuels_sdk.v2.treatments import basal_area_treatment, diameter_treatment +from fastfuels_sdk.v2.modifications import remove_trees, tree_attribute +from fastfuels_sdk.v2.client_library.models import ( + CategoricalColumnSummary, + Column, + ColumnType, + ContinuousColumnSummary, + FIASpeciesGroupShare, + Inventory as InventoryModel, + InventoryAttribute, + InventoryDataResponse, + InventoryJsonOrientation, + InventoryModification, + InventoryModificationAction, + InventoryModificationCondition, + InventorySource, + InventoryType, + JobStatus, + Modifier, + Operator, + TreeForestryMetrics, +) +from fastfuels_sdk.v2.client_library.types import UNSET, Response +from fastfuels_sdk.v2.exceptions import NotFoundException + +# External imports +import numpy as np +import pandas as pd +import pytest + +# The test_domain, completed_pim_grid, and completed_tree_inventory fixtures +# are session-scoped and shared across modules (tests/v2/conftest.py). They +# are READ-ONLY: tests that mutate or delete create throwaways (duplicates). + + +@pytest.fixture(scope="class") +def throwaway_inventory(completed_tree_inventory): + """A mutable copy of the shared inventory (the fixtures are read-only).""" + inventory = completed_tree_inventory.duplicate(name="throwaway_copy") + inventory.wait() + yield inventory + inventory.delete() + + +class TestCreateTreeInventoryFromPimGrid: + def test_create(self, test_domain, completed_pim_grid): + inventory = create_tree_inventory_from_pim_grid( + test_domain, + completed_pim_grid, + seed=42, + name="throwaway_pim_inventory", + ) + assert len(inventory.id) > 0 + assert inventory.domain_id == test_domain.id + assert inventory.status in (JobStatus.PENDING, JobStatus.RUNNING) + inventory.delete() + + def test_accepts_grid_id_string(self, test_domain, completed_pim_grid): + inventory = create_tree_inventory_from_pim_grid( + test_domain, completed_pim_grid.id, seed=42 + ) + assert inventory.domain_id == test_domain.id + inventory.delete() + + def test_completed_fixture(self, completed_tree_inventory): + assert completed_tree_inventory.status == JobStatus.COMPLETED + assert completed_tree_inventory.georeference is not None + assert completed_tree_inventory.checksum + + +class TestForestryMetrics: + @staticmethod + def _model(forestry_metrics=UNSET): + return InventoryModel( + id="inventory-id", + domain_id="domain-id", + type_=InventoryType.TREE, + status=JobStatus.COMPLETED, + source=InventorySource(), + forestry_metrics=forestry_metrics, + ) + + def test_wraps_metrics_record(self): + metrics = TreeForestryMetrics( + type_="tree", + tree_count=120, + basal_area_per_area=87.5, + tree_density=240.0, + quadratic_mean_diameter=9.4, + dominant_species_groups=[ + FIASpeciesGroupShare( + spgrpcd=3, + name="Douglas-fir", + basal_area_share=0.62, + ) + ], + ) + + inventory = Inventory._from_model(self._model(metrics)) + + assert isinstance(inventory.forestry_metrics, TreeForestryMetrics) + assert inventory.forestry_metrics.tree_count == 120 + assert inventory.forestry_metrics.dominant_species_groups[0].spgrpcd == 3 + + def test_normalizes_missing_metrics_to_none(self): + inventory = Inventory._from_model(self._model()) + + assert inventory.forestry_metrics is None + + def test_completed_inventory_metrics_live(self, completed_tree_inventory): + metrics = completed_tree_inventory.forestry_metrics + + assert isinstance(metrics, TreeForestryMetrics) + assert metrics.type_ == "tree" + assert ( + metrics.tree_count + == completed_tree_inventory.get_data_metadata().total_rows + ) + assert metrics.basal_area_per_area > 0 + assert metrics.tree_density > 0 + assert metrics.quadratic_mean_diameter > 0 + assert len(metrics.dominant_species_groups) > 0 + assert [ + group.basal_area_share for group in metrics.dominant_species_groups + ] == ( + sorted( + (group.basal_area_share for group in metrics.dominant_species_groups), + reverse=True, + ) + ) + + +class TestColumnSummary: + @staticmethod + def _inventory_with_column(column): + return Inventory( + id="inventory-id", + domain_id="domain-id", + type_=InventoryType.TREE, + status=JobStatus.COMPLETED, + source=InventorySource(), + columns=[column], + ) + + def test_returns_continuous_summary(self): + summary = ContinuousColumnSummary( + type_="continuous", + count=10, + null_count=0, + min_=1.0, + max_=5.0, + mean=3.0, + std=1.0, + ) + inventory = self._inventory_with_column( + Column(key="dbh", type_=ColumnType.CONTINUOUS, summary=summary) + ) + + assert inventory.column_summary("dbh") is summary + assert inventory.column_summary("dbh").mean == 3.0 + + def test_returns_categorical_summary(self): + summary = CategoricalColumnSummary( + type_="categorical", count=10, null_count=1, unique_count=3 + ) + inventory = self._inventory_with_column( + Column( + key="fia_species_code", + type_=ColumnType.CATEGORICAL, + summary=summary, + ) + ) + + assert inventory.column_summary("fia_species_code") is summary + assert inventory.column_summary("fia_species_code").unique_count == 3 + + def test_none_when_not_computed(self): + inventory = self._inventory_with_column( + Column(key="dbh", type_=ColumnType.CONTINUOUS) + ) + + assert inventory.column_summary("dbh") is None + + def test_unknown_column_raises(self): + inventory = self._inventory_with_column( + Column(key="dbh", type_=ColumnType.CONTINUOUS) + ) + + with pytest.raises(ValueError, match="no column"): + inventory.column_summary("height") + + def test_summaries_live(self, completed_tree_inventory): + dbh = completed_tree_inventory.column_summary("dbh") + species = completed_tree_inventory.column_summary("fia_species_code") + tree_count = completed_tree_inventory.get_data_metadata().total_rows + + assert isinstance(dbh, ContinuousColumnSummary) + assert dbh.type_ == "continuous" + assert dbh.count == tree_count + assert dbh.null_count == 0 + assert dbh.mean > 0 + + assert isinstance(species, CategoricalColumnSummary) + assert species.type_ == "categorical" + assert species.count == tree_count + assert species.null_count == 0 + assert species.unique_count > 0 + + +class TestCreateTreeInventoryFromFile: + def test_unknown_extension_raises(self, test_domain): + with pytest.raises(ValueError, match="upload format"): + create_tree_inventory_from_file(test_domain, "trees.parquet") + + def test_create_from_csv(self, test_domain, tmp_path): + # Tree records at the center of the domain, in the domain CRS + x_center = (test_domain.bbox[0] + test_domain.bbox[2]) / 2 + y_center = (test_domain.bbox[1] + test_domain.bbox[3]) / 2 + trees = pd.DataFrame( + { + "x": [x_center, x_center + 10], + "y": [y_center, y_center + 10], + "height": [12.0, 8.5], + "dbh": [25.0, 18.0], + "crown_ratio": [0.4, 0.5], + "fia_species_code": [122, 122], + "fia_status_code": [1, 1], + } + ) + path = tmp_path / "trees.csv" + trees.to_csv(path, index=False) + + inventory = create_tree_inventory_from_file( + test_domain, str(path), name="throwaway_upload" + ) + assert len(inventory.id) > 0 + assert inventory.domain_id == test_domain.id + # Processing the uploaded file runs as a background job + inventory.wait() + assert inventory.status == JobStatus.COMPLETED + assert len(inventory.to_dataframe()) == 2 + inventory.delete() + + +class TestCreateTreeInventoryFromGdam: + def test_create_returns_new_pending_inventory( + self, test_domain, completed_tree_inventory + ): + imputed = create_tree_inventory_from_gdam( + test_domain, + completed_tree_inventory, + impute_columns=["dbh", "crown_ratio"], + name="throwaway_gdam", + ) + assert isinstance(imputed, Inventory) + assert imputed.id != completed_tree_inventory.id + assert imputed.domain_id == test_domain.id + assert imputed.status in (JobStatus.PENDING, JobStatus.RUNNING) + imputed.delete() + + def test_accepts_inventory_id_string(self, test_domain, completed_tree_inventory): + imputed = create_tree_inventory_from_gdam( + test_domain, completed_tree_inventory.id + ) + assert imputed.domain_id == test_domain.id + imputed.delete() + + +class TestFromId: + def test_success(self, test_domain, completed_tree_inventory): + inventory = Inventory.from_id(test_domain.id, completed_tree_inventory.id) + assert inventory.id == completed_tree_inventory.id + assert inventory.domain_id == test_domain.id + + def test_not_found(self, test_domain): + with pytest.raises(NotFoundException): + Inventory.from_id(test_domain.id, uuid4().hex) + + +class TestGetInventory: + def test_get_inventory_returns_new_instance( + self, test_domain, completed_tree_inventory + ): + inventory = get_inventory(test_domain, completed_tree_inventory.id) + assert inventory.id == completed_tree_inventory.id + assert inventory is not completed_tree_inventory + + +class TestRefreshInventory: + def test_refresh_returns_self(self, completed_tree_inventory): + refreshed = completed_tree_inventory.refresh() + assert refreshed is completed_tree_inventory + assert refreshed.id == completed_tree_inventory.id + + +class TestUpdateInventory: + def test_update_name(self, test_domain, throwaway_inventory): + # update() mutates in place and returns self (chains) + updated = throwaway_inventory.update(name="updated_name") + assert updated is throwaway_inventory + assert throwaway_inventory.name == "updated_name" + assert get_inventory(test_domain, throwaway_inventory.id).name == "updated_name" + + def test_update_tags(self, throwaway_inventory): + throwaway_inventory.update(tags=["updated"]) + assert throwaway_inventory.tags == ["updated"] + + def test_update_no_fields_makes_no_api_call(self, throwaway_inventory): + assert throwaway_inventory.update() is throwaway_inventory + + +class TestDuplicate: + def test_duplicate_is_a_clone(self, completed_tree_inventory): + copy = completed_tree_inventory.duplicate(name="duplicate_test") + assert copy.id != completed_tree_inventory.id + assert copy.name == "duplicate_test" + # The copy job byte-copies the data rather than re-deriving it, so + # the finished copy carries the source's checksum verbatim + copy.wait() + assert copy.status == JobStatus.COMPLETED + assert copy.checksum == completed_tree_inventory.checksum + copy.delete() + + +class TestApplyModifications: + def test_apply_modifications_rederives_in_place(self, throwaway_inventory): + copy = throwaway_inventory + # Rules need at least one condition; height > 0 matches every tree + modification = InventoryModification( + conditions=[ + InventoryModificationCondition( + attribute=InventoryAttribute.HEIGHT, + operator=Operator.GT, + value=0, + ) + ], + actions=[ + InventoryModificationAction( + attribute=InventoryAttribute.HEIGHT, + modifier=Modifier.MULTIPLY, + value=0.9, + ) + ], + ) + original_checksum = copy.checksum + + modified = copy.apply_modifications([modification]) + + assert modified is copy # in place: same object, same id + assert copy.status == JobStatus.PENDING + assert copy.modifications == [] # ledger grows only after completion + assert copy.checksum != original_checksum # rotates at dispatch + copy.wait() + assert copy.status == JobStatus.COMPLETED + assert len(copy.modifications) == 1 + assert copy.checksum != original_checksum # data was re-derived + + def test_requires_completed_source(self, test_domain, completed_pim_grid): + inventory = create_tree_inventory_from_pim_grid( + test_domain, completed_pim_grid, seed=42 + ) + if inventory.status == JobStatus.COMPLETED: + inventory.delete() + pytest.skip("inventory completed too quickly to test the guard") + with pytest.raises(ValueError, match="apply modifications"): + inventory.apply_modifications([]) + inventory.delete() + + +class TestApplyTreatments: + def test_apply_treatments_rederives_in_place(self, throwaway_inventory): + copy = throwaway_inventory + original_checksum = copy.checksum + + treated = copy.apply_treatments([basal_area_treatment("from_below", 25.0)]) + + assert treated is copy # in place: same object, same id + assert copy.status == JobStatus.PENDING + assert copy.treatments == [] # ledger grows only after completion + assert copy.checksum != original_checksum # rotates at dispatch + copy.wait() + assert copy.status == JobStatus.COMPLETED + assert len(copy.treatments) == 1 + assert copy.checksum != original_checksum # data was re-derived + + def test_requires_completed_source(self, test_domain, completed_pim_grid): + inventory = create_tree_inventory_from_pim_grid( + test_domain, completed_pim_grid, seed=42 + ) + if inventory.status == JobStatus.COMPLETED: + inventory.delete() + pytest.skip("inventory completed too quickly to test the guard") + with pytest.raises(ValueError, match="apply treatments"): + inventory.apply_treatments([diameter_treatment("from_below", 10.0)]) + inventory.delete() + + +class TestModificationBuilders: + def test_remove_trees_at_creation( + self, test_domain, completed_pim_grid, completed_tree_inventory + ): + # Verify the builders independently through the create-time path. Use + # the same seed as the unmodified fixture so removing dbh < 10 must + # yield strictly fewer trees. + modified = create_tree_inventory_from_pim_grid( + test_domain, + completed_pim_grid, + seed=42, + modifications=[remove_trees(tree_attribute("dbh", "<", 10))], + name="throwaway_modified", + ) + modified.wait() + assert modified.status == JobStatus.COMPLETED + assert ( + 0 + < len(modified.to_dataframe()) + < len(completed_tree_inventory.to_dataframe()) + ) + modified.delete() + + +class TestVoxelize: + def test_voxelize_returns_new_pending_grid(self, completed_tree_inventory): + voxels = completed_tree_inventory.voxelize( + horizontal_resolution_m=2.0, + vertical_resolution_m=1.0, + name="throwaway_voxels", + ) + assert isinstance(voxels, Grid) + assert voxels.domain_id == completed_tree_inventory.domain_id + assert voxels.status in (JobStatus.PENDING, JobStatus.RUNNING) + voxels.delete() + + def test_resolution_args_must_be_given_together(self, completed_tree_inventory): + with pytest.raises(ValueError, match="together"): + completed_tree_inventory.voxelize(horizontal_resolution_m=2.0) + + def test_requires_completed_source(self, test_domain, completed_pim_grid): + inventory = create_tree_inventory_from_pim_grid( + test_domain, completed_pim_grid, seed=42 + ) + if inventory.status == JobStatus.COMPLETED: + inventory.delete() + pytest.skip("inventory completed too quickly to test the guard") + with pytest.raises(ValueError, match="voxelize"): + inventory.voxelize() + inventory.delete() + + +class TestExport: + def test_export_returns_pending_export(self, completed_tree_inventory): + export = completed_tree_inventory.export(format="csv") + assert len(export.id) > 0 + assert export.domain_id == completed_tree_inventory.domain_id + assert export.status in ( + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.COMPLETED, + ) + + def test_invalid_format(self, completed_tree_inventory): + with pytest.raises(ValueError): + completed_tree_inventory.export(format="shapefile") + + +class TestInventoryData: + @staticmethod + def _inventory(): + return Inventory( + id="inventory-id", + domain_id="domain-id", + type_=InventoryType.TREE, + status=JobStatus.COMPLETED, + source=InventorySource(), + ) + + def test_get_data_partition_passes_json_orientation(self, monkeypatch): + captured = {} + + def fake_get(domain_id, inventory_id, partition_index, **kwargs): + captured.update( + domain_id=domain_id, + inventory_id=inventory_id, + partition_index=partition_index, + **kwargs, + ) + return Response( + status_code=HTTPStatus.OK, + content=b"", + headers={}, + parsed=InventoryDataResponse( + partition=partition_index, + num_rows=1, + columns=["height"], + data=[{"height": 12.0}], + ), + ) + + client = object() + monkeypatch.setattr( + "fastfuels_sdk.v2.inventories.ensure_client", lambda: client + ) + monkeypatch.setattr( + "fastfuels_sdk.v2.inventories.get_inventory_data_json.sync_detailed", + fake_get, + ) + + partition = self._inventory().get_data_partition( + 2, columns=["height"], json_orientation="records" + ) + + assert partition.data == [{"height": 12.0}] + assert captured == { + "domain_id": "domain-id", + "inventory_id": "inventory-id", + "partition_index": 2, + "client": client, + "json_orientation": InventoryJsonOrientation.RECORDS, + "columns": "height", + } + + def test_get_data_partition_rejects_invalid_orientation(self): + with pytest.raises(ValueError, match="not a valid InventoryJsonOrientation"): + self._inventory().get_data_partition(0, json_orientation="columns") + + def test_to_dataframe_uses_csv_partitions(self, monkeypatch): + inventory = self._inventory() + metadata = SimpleNamespace( + num_partitions=2, total_rows=3, columns=["x", "height"] + ) + csv_partitions = [ + "x,height\n1.0,10.0\n2.0,20.0\n", + "x,height\n3.0,30.0\n", + ] + captured = [] + + def fake_csv(domain_id, inventory_id, partition_index, **kwargs): + captured.append((domain_id, inventory_id, partition_index, kwargs)) + return Response( + status_code=HTTPStatus.OK, + content=csv_partitions[partition_index].encode(), + headers={}, + parsed=csv_partitions[partition_index], + ) + + def fail_json(*args, **kwargs): + raise AssertionError("to_dataframe must use the CSV endpoint") + + client = object() + monkeypatch.setattr(inventory, "get_data_metadata", lambda: metadata) + monkeypatch.setattr( + "fastfuels_sdk.v2.inventories.ensure_client", lambda: client + ) + monkeypatch.setattr( + "fastfuels_sdk.v2.inventories.get_inventory_data_csv.sync_detailed", + fake_csv, + ) + monkeypatch.setattr( + "fastfuels_sdk.v2.inventories.get_inventory_data_json.sync_detailed", + fail_json, + ) + + trees = inventory.to_dataframe(columns=["x", "height"]) + + assert trees.to_dict(orient="list") == { + "x": [1.0, 2.0, 3.0], + "height": [10.0, 20.0, 30.0], + } + assert [call[2] for call in captured] == [0, 1] + assert all( + call[3] == {"client": client, "columns": "x,height"} for call in captured + ) + + def test_get_data_metadata(self, completed_tree_inventory): + metadata = completed_tree_inventory.get_data_metadata() + assert metadata.inventory_id == completed_tree_inventory.id + assert metadata.num_partitions >= 1 + assert metadata.total_rows > 0 + assert len(metadata.columns) > 0 + + def test_get_data_partition(self, completed_tree_inventory): + # metadata.columns can carry a __null_dask_index__ backend artifact + # that the partition responses correctly omit, so compare as subset + metadata = completed_tree_inventory.get_data_metadata() + partition = completed_tree_inventory.get_data_partition(0) + assert partition.partition == 0 + assert partition.num_rows == metadata.partitions[0].num_rows + assert set(partition.columns) <= set(metadata.columns) + assert "height" in partition.columns + assert len(partition.data) == partition.num_rows + + def test_get_data_partition_records(self, completed_tree_inventory): + partition = completed_tree_inventory.get_data_partition( + 0, columns=["height"], json_orientation="records" + ) + + assert partition.columns == ["height"] + assert len(partition.data) == partition.num_rows + assert set(partition.data[0]) == {"height"} + + def test_to_dataframe(self, completed_tree_inventory): + metadata = completed_tree_inventory.get_data_metadata() + trees = completed_tree_inventory.to_dataframe() + assert isinstance(trees, pd.DataFrame) + assert len(trees) == metadata.total_rows + assert set(trees.columns) <= set(metadata.columns) + assert {"x", "y", "height"} <= set(trees.columns) + + def test_to_dataframe_column_subset(self, completed_tree_inventory): + metadata = completed_tree_inventory.get_data_metadata() + subset = metadata.columns[:2] + trees = completed_tree_inventory.to_dataframe(columns=subset) + assert list(trees.columns) == subset + + @pytest.fixture(scope="class") + def completed_csv_export(self, completed_tree_inventory): + """A completed CSV export of the shared tree inventory.""" + export = completed_tree_inventory.export(format="csv", tags=["test"]) + export.wait() + return export + + def test_to_dataframe_matches_csv_export( + self, completed_tree_inventory, completed_csv_export, tmp_path + ): + # Ground-truth check for to_dataframe: the records it loads from the + # data partitions must match the same inventory the server renders to + # a CSV and we read back with pandas. Row order is not guaranteed and + # the CSV round-trips floats through text, so compare order- and + # precision-independently: equal row count, shared columns, and the + # same multiset of values per column. Reuses the export fixture. + path = completed_csv_export.to_file(tmp_path / "trees.csv") + from_csv = pd.read_csv(path) + from_api = completed_tree_inventory.to_dataframe() + + shared = sorted(set(from_api.columns) & set(from_csv.columns)) + assert {"x", "y", "height"} <= set(shared) + assert len(from_api) == len(from_csv) + + for column in shared: + api_values = from_api[column].to_numpy() + if not np.issubdtype(api_values.dtype, np.number): + continue # presence is already asserted via `shared` + assert np.allclose( + np.sort(api_values.astype(float)), + np.sort(from_csv[column].to_numpy(dtype=float)), + rtol=1e-4, + atol=1e-4, + equal_nan=True, + ) + + +class TestListInventories: + def test_list_in_domain(self, test_domain, completed_tree_inventory): + inventory_ids = [i.id for i in list_inventories(test_domain)] + assert completed_tree_inventory.id in inventory_ids + + def test_list_cross_domain(self, completed_tree_inventory): + # No domain: list inventories across all the user's domains + inventory_ids = [i.id for i in list_inventories()] + assert completed_tree_inventory.id in inventory_ids + + def test_filter_by_tag(self, test_domain, completed_tree_inventory): + tagged = list_inventories(test_domain, tag="test") + assert completed_tree_inventory.id in [i.id for i in tagged] + + def test_invalid_sort_field(self): + with pytest.raises(ValueError): + list_inventories(sort_by="not_a_field") + + +class TestToJson: + def test_to_json(self, completed_tree_inventory): + inventory_dict = json.loads(completed_tree_inventory.to_json()) + assert inventory_dict["id"] == completed_tree_inventory.id + assert inventory_dict["domain_id"] == completed_tree_inventory.domain_id + + +class TestDeleteInventory: + def test_delete(self, test_domain, completed_tree_inventory): + inventory = completed_tree_inventory.duplicate(name="delete_test") + inventory.delete() + + with pytest.raises(NotFoundException): + Inventory.from_id(test_domain.id, inventory.id) + + with pytest.raises(NotFoundException): + inventory.delete() diff --git a/tests/v2/test_modifications.py b/tests/v2/test_modifications.py new file mode 100644 index 0000000..1688a10 --- /dev/null +++ b/tests/v2/test_modifications.py @@ -0,0 +1,109 @@ +""" +tests/v2/test_modifications.py + +Unit tests for the inventory modification builders (no API). The grid `mask` +builder is exercised in tests/v2/test_grids.py. +""" + +# Core imports +from types import SimpleNamespace + +# Internal imports +from fastfuels_sdk.v2.modifications import ( + modify_trees, + remove_trees, + tree_attribute, + tree_within, +) +from fastfuels_sdk.v2.client_library.models import ( + InventoryAttribute, + InventoryFeatureSpatialCondition, + InventoryModification, + InventoryModificationAction, + InventoryModificationCondition, + Modifier, + Operator, + RemoveAction, + SpatialOperator, +) + +# External imports +import pytest + + +class TestTreeAttribute: + def test_symbolic_operator(self): + condition = tree_attribute("dbh", "<", 10) + assert isinstance(condition, InventoryModificationCondition) + assert condition.attribute == InventoryAttribute.DBH + assert condition.operator == Operator.LT + assert condition.value == 10 + + def test_enum_name_operator(self): + assert tree_attribute("height", "ge", 2).operator == Operator.GE + + def test_operator_member_passes_through(self): + assert tree_attribute("dbh", Operator.NE, 0).operator is Operator.NE + + def test_invalid_operator_raises(self): + with pytest.raises(ValueError): + tree_attribute("dbh", "~", 1) + + def test_invalid_attribute_raises(self): + with pytest.raises(ValueError): + tree_attribute("girth", "<", 1) + + +class TestTreeWithin: + def test_builds_feature_condition(self): + condition = tree_within("feat123", buffer_m=5) + assert isinstance(condition, InventoryFeatureSpatialCondition) + assert condition.source == "feature" + assert condition.feature_id == "feat123" + assert condition.operator == SpatialOperator.WITHIN + assert condition.buffer_m == 5 + + def test_accepts_feature_object(self): + assert tree_within(SimpleNamespace(id="f1")).feature_id == "f1" + + def test_operator(self): + assert tree_within("f", operator="outside").operator == SpatialOperator.OUTSIDE + + +class TestRemoveTrees: + def test_builds_modification(self): + modification = remove_trees(tree_attribute("dbh", "<", 10)) + assert isinstance(modification, InventoryModification) + assert len(modification.conditions) == 1 + assert len(modification.actions) == 1 + assert isinstance(modification.actions[0], RemoveAction) + + def test_requires_at_least_one_condition(self): + with pytest.raises(ValueError, match="condition"): + remove_trees() + + def test_multiple_conditions_anded(self): + modification = remove_trees(tree_attribute("dbh", "<", 10), tree_within("f")) + assert len(modification.conditions) == 2 + + +class TestModifyTrees: + def test_builds_action(self): + modification = modify_trees( + "height", "multiply", 0.9, tree_attribute("dbh", ">", 0) + ) + action = modification.actions[0] + assert isinstance(action, InventoryModificationAction) + assert action.attribute == InventoryAttribute.HEIGHT + assert action.modifier == Modifier.MULTIPLY + assert action.value == 0.9 + assert len(modification.conditions) == 1 + + def test_requires_at_least_one_condition(self): + with pytest.raises(ValueError, match="condition"): + modify_trees("height", "multiply", 0.9) + + def test_remove_is_not_a_modifier(self): + # "remove" is RemoveAction, not a modifier value + with pytest.raises(ValueError): + modify_trees("height", "remove", 0, tree_attribute("dbh", ">", 0)) diff --git a/tests/v2/test_point_clouds.py b/tests/v2/test_point_clouds.py new file mode 100644 index 0000000..6de60a7 --- /dev/null +++ b/tests/v2/test_point_clouds.py @@ -0,0 +1,202 @@ +""" +tests/v2/test_point_clouds.py +""" + +# Core imports +import json +from uuid import uuid4 + +# Internal imports +from fastfuels_sdk.v2.point_clouds import ( + PointCloud, + _point_cloud_type, + check_3dep_coverage, + create_point_cloud_from_3dep, + create_point_cloud_from_file, + get_point_cloud, + list_point_clouds, +) +from fastfuels_sdk.v2.client_library.models import JobStatus, PointCloudType +from fastfuels_sdk.v2.domains import Domain +from fastfuels_sdk.v2.exceptions import NotFoundException + +# External imports +import pytest + +# The test_domain fixture is session-scoped and shared (tests/v2/conftest.py). +# +# There is no laspy in the test environment to author a valid LAS/LAZ, so the +# upload tests send a tiny placeholder file: the create call and the signed PUT +# both succeed and the point cloud comes back pending. These tests exercise the +# SDK surface (create + lifecycle), not the server-side LiDAR processing, so +# they never call wait() -- the placeholder would fail parsing. + + +def _placeholder_laz(directory) -> str: + path = directory / "scan.laz" + path.write_bytes(b"\x00" * 64) + return str(path) + + +@pytest.fixture(scope="module") +def covered_3dep_domain(): + """A small Bondurant, WY domain with stable 3DEP LiDAR coverage.""" + geojson = { + "type": "FeatureCollection", + "crs": {"type": "name", "properties": {"name": "EPSG:32612"}}, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [522800, 4720400], + [523300, 4720400], + [523300, 4720900], + [522800, 4720900], + [522800, 4720400], + ] + ], + }, + } + ], + } + domain = Domain.from_geojson( + geojson, + name="test_3dep_point_cloud_domain", + tags=["sdk-test"], + ) + yield domain + domain.delete(force=True) + + +class TestPointCloudType: + """Pure unit tests for the scan-type coercion (no API).""" + + def test_string_coerces_to_enum(self): + assert _point_cloud_type("als") == PointCloudType.ALS + assert _point_cloud_type("tls") == PointCloudType.TLS + + def test_enum_passes_through(self): + assert _point_cloud_type(PointCloudType.ALS) is PointCloudType.ALS + + def test_invalid_raises(self): + with pytest.raises(ValueError): + _point_cloud_type("mls") + + +class TestCreateFrom3dep: + def test_coverage_preflight(self, covered_3dep_domain): + coverage = check_3dep_coverage(covered_3dep_domain) + + assert coverage.available is True + assert coverage.coverage_fraction == pytest.approx(1.0, abs=1e-3) + assert coverage.estimated_point_count > 0 + assert coverage.point_budget > 0 + assert coverage.exceeds_point_budget is False + assert coverage.datasets + + def test_create_with_pinned_dataset(self, covered_3dep_domain): + coverage = check_3dep_coverage(covered_3dep_domain) + dataset = coverage.datasets[0].name + point_cloud = create_point_cloud_from_3dep( + covered_3dep_domain, + datasets=[dataset], + name="throwaway_3dep_pc", + ) + try: + assert isinstance(point_cloud, PointCloud) + assert point_cloud.domain_id == covered_3dep_domain.id + assert point_cloud.type_ == PointCloudType.ALS + assert point_cloud.source["name"] == "3dep" + assert point_cloud.source["datasets"] == [dataset] + assert point_cloud.status in ( + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.COMPLETED, + ) + finally: + try: + point_cloud.delete() + except NotFoundException: + pass + + +class TestCreateAndLifecycle: + @pytest.fixture(scope="class") + def uploaded_point_cloud(self, test_domain, tmp_path_factory): + path = _placeholder_laz(tmp_path_factory.mktemp("pc")) + pc = create_point_cloud_from_file( + test_domain, path, point_cloud_type="als", name="throwaway_pc" + ) + yield pc + try: + pc.delete() + except NotFoundException: + pass + + def test_create_returns_pending_record(self, uploaded_point_cloud, test_domain): + pc = uploaded_point_cloud + assert isinstance(pc, PointCloud) + assert len(pc.id) > 0 + assert pc.domain_id == test_domain.id + assert pc.type_ == PointCloudType.ALS + # Pending at create; the placeholder may later fail processing -- either + # is fine, we only assert the record was created as a real job. + assert pc.status in ( + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.FAILED, + ) + + def test_from_id_and_get_point_cloud(self, uploaded_point_cloud, test_domain): + fetched = PointCloud.from_id(test_domain.id, uploaded_point_cloud.id) + assert fetched.id == uploaded_point_cloud.id + assert ( + get_point_cloud(test_domain, uploaded_point_cloud.id).id + == uploaded_point_cloud.id + ) + + def test_refresh_returns_self(self, uploaded_point_cloud): + assert uploaded_point_cloud.refresh() is uploaded_point_cloud + + def test_update_name(self, uploaded_point_cloud, test_domain): + updated = uploaded_point_cloud.update(name="renamed_pc") + assert updated is uploaded_point_cloud + assert ( + get_point_cloud(test_domain, uploaded_point_cloud.id).name == "renamed_pc" + ) + + def test_update_no_fields_makes_no_api_call(self, uploaded_point_cloud): + assert uploaded_point_cloud.update() is uploaded_point_cloud + + def test_list_membership(self, uploaded_point_cloud, test_domain): + ids = [pc.id for pc in list_point_clouds(test_domain)] + assert uploaded_point_cloud.id in ids + + def test_list_cross_domain_membership(self, uploaded_point_cloud): + ids = [pc.id for pc in list_point_clouds()] + assert uploaded_point_cloud.id in ids + + def test_to_json(self, uploaded_point_cloud): + assert ( + json.loads(uploaded_point_cloud.to_json())["id"] == uploaded_point_cloud.id + ) + + +class TestNotFound: + def test_from_id_not_found(self, test_domain): + with pytest.raises(NotFoundException): + PointCloud.from_id(test_domain.id, uuid4().hex) + + +class TestDelete: + def test_delete_then_not_found(self, test_domain, tmp_path): + pc = create_point_cloud_from_file( + test_domain, _placeholder_laz(tmp_path), point_cloud_type="tls" + ) + pc.delete() + with pytest.raises(NotFoundException): + PointCloud.from_id(test_domain.id, pc.id) diff --git a/tests/v2/test_treatments.py b/tests/v2/test_treatments.py new file mode 100644 index 0000000..d6c1af8 --- /dev/null +++ b/tests/v2/test_treatments.py @@ -0,0 +1,59 @@ +""" +tests/v2/test_treatments.py + +Unit tests for the inventory treatment builders (no API). +""" + +# Internal imports +from fastfuels_sdk.v2.treatments import basal_area_treatment, diameter_treatment +from fastfuels_sdk.v2.client_library.models import ( + InventoryBasalAreaTreatment, + InventoryDiameterTreatment, + InventoryDiameterTreatmentMethod, + InventoryTreatmentMethod, +) +from fastfuels_sdk.v2.client_library.types import UNSET + +# External imports +import pytest + + +class TestBasalAreaTreatment: + def test_builds_model(self): + t = basal_area_treatment("from_below", 25.0) + assert isinstance(t, InventoryBasalAreaTreatment) + assert t.method == InventoryTreatmentMethod.FROM_BELOW + assert t.value == 25.0 + assert t.metric == "basal_area" + assert t.unit is UNSET + assert t.conditions is UNSET + + def test_method_enum_passes_through(self): + t = basal_area_treatment(InventoryTreatmentMethod.PROPORTIONAL, 0.5) + assert t.method is InventoryTreatmentMethod.PROPORTIONAL + + def test_unit_and_conditions_pass_through(self): + sentinel = object() + t = basal_area_treatment( + "from_below", 25.0, unit="m**2/ha", conditions=[sentinel] + ) + assert t.unit == "m**2/ha" + assert t.conditions == [sentinel] + + def test_invalid_method_raises(self): + with pytest.raises(ValueError): + basal_area_treatment("sideways", 25.0) + + +class TestDiameterTreatment: + def test_builds_model(self): + t = diameter_treatment("from_below", 10.0) + assert isinstance(t, InventoryDiameterTreatment) + assert t.method == InventoryDiameterTreatmentMethod.FROM_BELOW + assert t.value == 10.0 + assert t.metric == "diameter" + + def test_proportional_not_valid_for_diameter(self): + # Diameter treatments support only from_below / from_above. + with pytest.raises(ValueError): + diameter_treatment("proportional", 10.0) diff --git a/tests/v2/test_uploads.py b/tests/v2/test_uploads.py new file mode 100644 index 0000000..7dc5388 --- /dev/null +++ b/tests/v2/test_uploads.py @@ -0,0 +1,75 @@ +""" +tests/v2/test_uploads.py + +Unit tests for the shared signed-upload helper (no API). +""" + +# Core imports +from types import SimpleNamespace + +# Internal imports +from fastfuels_sdk.v2._uploads import put_upload +from fastfuels_sdk.v2.client_library.models import GridUploadSpecHeaders + +# External imports +import pytest +import requests + + +def _spec(url, header_dict): + """A minimal upload spec: a signed URL plus the server-provided headers.""" + headers = GridUploadSpecHeaders() + for key, value in header_dict.items(): + headers[key] = value + return SimpleNamespace( + url=url, headers=headers, content_type=header_dict.get("Content-Type") + ) + + +class _FakeResponse: + def __init__(self, status_code=200): + self.status_code = status_code + + def raise_for_status(self): + if self.status_code >= 400: + raise requests.HTTPError(str(self.status_code)) + + +def test_put_upload_echoes_server_headers(tmp_path, monkeypatch): + # The signed URL covers BOTH Content-Type and the GCS content-length-range; + # the PUT must send exactly the server-provided header set -- no more, no + # less -- or GCS rejects it with 403. + server_headers = { + "Content-Type": "image/tiff", + "x-goog-content-length-range": "0,1073741824", + } + spec = _spec("https://upload.example/signed", server_headers) + path = tmp_path / "raster.tif" + path.write_bytes(b"\x00\x01\x02data") + + captured = {} + + def fake_put(url, data=None, headers=None): + captured["url"] = url + captured["headers"] = headers + captured["data"] = data.read() + return _FakeResponse(200) + + monkeypatch.setattr(requests, "put", fake_put) + + put_upload(spec, str(path)) + + assert captured["url"] == "https://upload.example/signed" + assert captured["headers"] == server_headers # incl. x-goog-content-length-range + assert captured["data"] == b"\x00\x01\x02data" # file streamed to the PUT + + +def test_put_upload_raises_on_http_error(tmp_path, monkeypatch): + spec = _spec("https://upload.example/signed", {"Content-Type": "text/csv"}) + path = tmp_path / "trees.csv" + path.write_text("x,y\n") + + monkeypatch.setattr(requests, "put", lambda *a, **k: _FakeResponse(403)) + + with pytest.raises(requests.HTTPError): + put_upload(spec, str(path)) diff --git a/tests/v2/utils.py b/tests/v2/utils.py new file mode 100644 index 0000000..cd16801 --- /dev/null +++ b/tests/v2/utils.py @@ -0,0 +1,69 @@ +""" +tests/v2/utils.py +""" + +import json +from datetime import datetime, timedelta, timezone + +from tests import TEST_DATA_DIR +from fastfuels_sdk.v2.domains import Domain, list_domains + +# Fingerprint tag for test-created domains, so the session-start sweep +# can find resources leaked by crashed runs +SWEEP_TAG = "sdk-test" + +# Required fuelbed input columns for layerset features +DEFAULT_LAYERSET_PROPERTIES = { + "fuel_type": "grass", + "fuel_loading": 0.5, + "fuel_height": 0.3, + "percent_cover": 80.0, + "distribution": "homogeneous", +} + + +def create_default_domain() -> Domain: + """Creates a default v2 Domain resource for testing.""" + # Load test GeoJSON data + with open(TEST_DATA_DIR / "blue_mtn.geojson") as f: + geojson = json.load(f) + + # Create a domain using the GeoJSON + domain = Domain.from_geojson( + geojson, + name="test_domain", + description="Domain for testing v2 domain operations", + tags=[SWEEP_TAG], + pad_to_resolution=2.0, + ) + + return domain + + +def sweep_leftover_domains(max_age: timedelta = timedelta(hours=2)) -> None: + """Delete test domains leaked by crashed or interrupted runs. + + Teardown never runs when a session is killed mid-flight, so test + domains can accumulate on the live account. Deletes domains carrying + SWEEP_TAG that are older than ``max_age``; the age gate keeps + concurrent runs (e.g. local + CI) from sweeping each other's live + resources. + """ + cutoff = datetime.now(timezone.utc) - max_age + for domain in list_domains(size=100): + if SWEEP_TAG in (domain.tags or []) and domain.created_on < cutoff: + domain.delete(force=True) + + +def create_default_layerset_geojson() -> dict: + """Build a valid layerset FeatureCollection for testing. + + Layersets require a projected CRS, so this uses the EPSG:5070 variant + of the blue_mtn geometry with the required fuelbed properties on each + feature. + """ + with open(TEST_DATA_DIR / "blue_mtn_5070.geojson") as f: + geojson = json.load(f) + for feature in geojson["features"]: + feature["properties"] = dict(DEFAULT_LAYERSET_PROPERTIES) + return geojson diff --git a/uv.lock b/uv.lock index d5df030..1a5f94f 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,15 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[[package]] +name = "affine" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/98/d2f0bb06385069e799fc7d2870d9e078cfa0fa396dc8a2b81227d0da08b9/affine-2.4.0.tar.gz", hash = "sha256:a24d818d6a836c131976d22f8c27b8d3ca32d0af64c1d8d29deb7bafa4da1eea", size = 17132, upload-time = "2023-01-19T23:44:30.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -250,6 +259,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "cligj" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/0d/837dbd5d8430fd0f01ed72c4cfb2f548180f4c68c635df84ce87956cff32/cligj-0.7.2.tar.gz", hash = "sha256:a4bc13d623356b373c2c27c53dbd9c68cae5d526270bfa71f6c6fa69669c6b27", size = 9803, upload-time = "2021-05-28T21:23:27.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/86/43fa9f15c5b9fb6e82620428827cd3c284aa933431405d1bcf5231ae3d3e/cligj-0.7.2-py3-none-any.whl", hash = "sha256:c1ca117dbce1fe20a5809dc96f01e1c2840f6dcc939b3ddbb1111bf330ba82df", size = 7069, upload-time = "2021-05-28T21:23:26.877Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -336,13 +369,17 @@ wheels = [ name = "fastfuels-sdk" source = { editable = "." } dependencies = [ + { name = "attrs" }, { name = "geopandas" }, + { name = "httpx" }, { name = "numpy" }, { name = "pandas" }, { name = "pydantic" }, + { name = "python-dateutil" }, { name = "requests" }, { name = "scipy" }, { name = "urllib3" }, + { name = "xarray" }, { name = "zarr" }, ] @@ -354,9 +391,13 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "python-dateutil" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] docs = [ + { name = "mike" }, { name = "mkdocs" }, + { name = "mkdocs-literate-nav" }, { name = "mkdocs-material" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, @@ -364,28 +405,35 @@ docs = [ [package.metadata] requires-dist = [ + { name = "attrs", specifier = ">=22.2.0" }, { name = "geopandas" }, + { name = "httpx", specifier = ">=0.23.0" }, { name = "numpy" }, { name = "pandas" }, { name = "pydantic", specifier = ">=2" }, + { name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "requests" }, { name = "scipy" }, { name = "urllib3", specifier = ">=2.1.0" }, + { name = "xarray" }, { name = "zarr" }, ] [package.metadata.requires-dev] dev = [ { name = "attrs", specifier = ">=22.2.0" }, - { name = "httpx", specifier = ">=0.23.0,<0.29.0" }, + { name = "httpx", specifier = ">=0.23.0" }, { name = "ipykernel" }, { name = "pre-commit" }, { name = "pytest" }, { name = "python-dateutil", specifier = ">=2.9.0.post0" }, + { name = "rasterio" }, ] docs = [ + { name = "mike", specifier = ">=2.2.0" }, { name = "mkdocs" }, - { name = "mkdocs-material" }, + { name = "mkdocs-literate-nav", specifier = "==0.6.2" }, + { name = "mkdocs-material", specifier = ">=9.7,<10" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, ] @@ -743,6 +791,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] +[[package]] +name = "mike" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "verspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, +] + [[package]] name = "mkdocs" version = "1.6.1" @@ -795,6 +860,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" }, +] + [[package]] name = "mkdocs-material" version = "9.7.6" @@ -1372,6 +1449,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996, upload-time = "2025-11-28T19:04:51.132Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pyproj" version = "3.7.2" @@ -1612,6 +1698,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "rasterio" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "affine", marker = "python_full_version < '3.12'" }, + { name = "attrs", marker = "python_full_version < '3.12'" }, + { name = "certifi", marker = "python_full_version < '3.12'" }, + { name = "click", marker = "python_full_version < '3.12'" }, + { name = "click-plugins", marker = "python_full_version < '3.12'" }, + { name = "cligj", marker = "python_full_version < '3.12'" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "pyparsing", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/fa/fce8dc9f09e5bc6520b6fc1b4ecfa510af9ca06eb42ad7bdff9c9b8989d0/rasterio-1.4.4.tar.gz", hash = "sha256:c95424e2c7f009b8f7df1095d645c52895cd332c0c2e1b4c2e073ea28b930320", size = 445004, upload-time = "2025-12-12T18:01:08.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/0d/d3859e49ab94464de2623fec82c6798d8d7c8bea2473cd2696fc5e09f717/rasterio-1.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b8eea428b5f0c78a963f6003a19b60777df83a0aba8c28231d65431e32ac160e", size = 21144125, upload-time = "2025-12-12T17:58:59.511Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3c/97ba4b146309cdc0e36f289b02ac69465b026a21afc828e4e4e1dc39466a/rasterio-1.4.4-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:1cc0ea5aa0d22f5f349aa221674481de689b7b3a99607ce6bb58a29e5be54d17", size = 25746406, upload-time = "2025-12-12T17:59:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/ce/33/75f81bd837ac2336b24456fdb249597a4b9af2a212b7151f64d09022be36/rasterio-1.4.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7eb25b23666b29dadfc49a59206cead62c99190584b61771bba0e95f7da06801", size = 34587242, upload-time = "2025-12-12T17:59:05.848Z" }, + { url = "https://files.pythonhosted.org/packages/f9/77/3869a426f6e752dde13f3868cdf16253ca0214f92107db79c1583c9aa07b/rasterio-1.4.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e24b7b8c2df801dde2a1dffb44c58902bd76b5cab740dc11de4ff9963992a71a", size = 35881871, upload-time = "2025-12-12T17:59:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/66/d0/3818859ddbd3750d0ef5a6580a3272e81764286d943c689dd41e49b8b786/rasterio-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:0718630f607be2f5742d8e4b34b434746fd788a192d77eefc9bb924399fea802", size = 25716477, upload-time = "2025-12-12T17:59:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/02/039eb4970c93aaef4c9eb1ee159abad18e6e7f932c2eed575c95f78d94f6/rasterio-1.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:0308ff4762ae9eb40a991f12d758626b59af4376b13675480391dd7295d17bbf", size = 24075993, upload-time = "2025-12-12T17:59:16.407Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/63d89ddfcb4643730553683ee322566b9b15fe56d026e4c21c4f4f5d9d26/rasterio-1.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3c4f0cbd188f893011f2a0a6dc2852b3892799b3a0d79eddf92f2b115ec7ed7", size = 21120715, upload-time = "2025-12-12T17:59:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/2c003f76a23dbb078fdee35c8e2ec490d2ad8982f4dc956ba08b56027b87/rasterio-1.4.4-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:6fce26090b9f509eab337228420145947c491a13628965410f25bc3e6e05cf75", size = 25732944, upload-time = "2025-12-12T17:59:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cc/4a8e92362c0ff496dd1007c3dcba66e9ededf1a45eca8ad1db302b071c49/rasterio-1.4.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c1c722da390dc264aeccdc0dc200ca37923875d910ca4cd5bec0fec351bb818e", size = 34295209, upload-time = "2025-12-12T17:59:26.035Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/717d2dec47fbefad33ca0d27bd5f0d543b1d1bc9fcab5ef82a13adaaf38d/rasterio-1.4.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98b6dfb8282b2a54b9d75c3dc8d2520a69bbc66916c7d43de8e0bbf6e0240ca1", size = 35661866, upload-time = "2025-12-12T17:59:29.928Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/ae3351fba2726ec0976974ce2eb030c159edd3363b8771e832b8db571c24/rasterio-1.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:9513f4c7a6d93b45098f8dff2421fa9516604e3bfbf35aa144484a88d36a321f", size = 25682853, upload-time = "2025-12-12T17:59:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/38/ee/35387296bbacfc5cbbb4273228b1b959793d3ce38b0402a07f11a248420b/rasterio-1.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:60b49a482e0f12f12ce9d2cc3090add02f89f3d422e85f2cffaa9207adb83c04", size = 24043249, upload-time = "2025-12-12T17:59:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fe/e3e37041c49956f4f4cbe473c3fe290aaba96ed20e9c07da304e0cad2015/rasterio-1.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:df26c96aa81ffbd0b33189680859211eadf9950123c21579f84de73bb0f91d81", size = 21107336, upload-time = "2025-12-12T17:59:43.585Z" }, + { url = "https://files.pythonhosted.org/packages/f3/02/c217fdcc8e80a4b7d1b1bc4529d78f98452816e9add53ff8742049a77ae7/rasterio-1.4.4-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:b3af0ecc922a80f3755516629f7948e37bade9077b5f5c12a3869a5e7f01619b", size = 25719929, upload-time = "2025-12-12T17:59:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d0/7f177f37bc9595d809dabb0073abd0c42358469f6b10875192b46331c652/rasterio-1.4.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7ce3b0f9a22e95a27790087908753973644d7c3877d495ec9bd6e04a25233ca4", size = 34198845, upload-time = "2025-12-12T17:59:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/84/66c0d9cca2a09074ec2ce6fffa87709ca51b0d197ae742d835e841bac660/rasterio-1.4.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c072450caa96428b1218b030500bb908fd6f09bc013a88969ff81a124b6a112a", size = 35576074, upload-time = "2025-12-12T17:59:56.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/68/f7df5478458ace2fa50be43e9fab1a39957a0e71afaa3e6147ec289e0fc8/rasterio-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ee92ef10c0ba89f45f9c2b40fca9f971f357385f04ee9b716fb09cbd9ce20c", size = 25680573, upload-time = "2025-12-12T18:00:00.45Z" }, + { url = "https://files.pythonhosted.org/packages/34/e5/1bdaccb658430dfd391ad4a63d206546f36639d7e4130bf31f125c6525b4/rasterio-1.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:65c10afe64b5e488185aaff0b659e08eda22c89285b54a3e433b80e6c6621770", size = 24040367, upload-time = "2025-12-12T18:00:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/32/76/54643a7d1d650fd7f1acea9093c298603e4c01bba6f90be2254310b48507/rasterio-1.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:18c2c1130e789dc2771d0aa5ec4b56d5b8a0097c648ccb94882d5ff3ab55c928", size = 21247203, upload-time = "2025-12-12T18:00:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/434b4849ccd6a3e03a0b1ac37c963c1771564945745613d15c5d96ce768d/rasterio-1.4.4-cp313-cp313t-macosx_15_0_x86_64.whl", hash = "sha256:2d1654b7ffa6f3dde42c5fd27159ae45148c11e352de26f12fe7313a3236aeed", size = 25822050, upload-time = "2025-12-12T18:00:11.081Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fa/fe9a478aa0cde246da58baeb0df3248c7ca174e4d9c9b27e81b504e40a76/rasterio-1.4.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c4022cbddb659856e120603b12233cec8913ae760fff220657ce888c3c6b9f9d", size = 34833783, upload-time = "2025-12-12T18:00:14.525Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/ed4716590dbcd4b8ae633417d758564e510bee4d6aaac5050a0f6d5179c5/rasterio-1.4.4-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:96b88880551a07b7a3b50439483cefbd9af91a09e19ff2b736815994e5671314", size = 35738114, upload-time = "2025-12-12T18:00:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/7e/29/da7050d11ba1d041e0333ac14768e6e9ca1aa2b9fa8416f317d2650ed276/rasterio-1.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:def75d486d0ab8f306f918a913c425ed57159495518c54efe8e18d5164d37d90", size = 25896835, upload-time = "2025-12-12T18:00:21.411Z" }, + { url = "https://files.pythonhosted.org/packages/88/80/304dbe5434c4aa8dfaf90480c16d770161796a6a61fa88e72e8a402153df/rasterio-1.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:770b7e86f6c565e6f9cf30f6fa4479a5a2bab4e10ff44fe7acfd518ca4a71d1b", size = 24128074, upload-time = "2025-12-12T18:00:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/d5a3dc51cd5fef62b76ecc77d33c1ca20de305fed7e16c71bcdf4858e466/rasterio-1.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:019693f14a83ae9225cb57c16e466901d0e6284962dcf13a9f4bb1175b979011", size = 21120237, upload-time = "2025-12-12T18:00:27.723Z" }, + { url = "https://files.pythonhosted.org/packages/50/da/db18362602b17327c0e00c9e9c0847c1c4ac657c1a289169ca06a26faccb/rasterio-1.4.4-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:87d7c3e97e3b40c9041d1602e2dcb4fc2d88abe6c645fccb4939dec297a91cf8", size = 25720506, upload-time = "2025-12-12T18:00:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8f/a15d66c9c05bffb176c9707ef1f2bfcf9c0b835272937c80ac7207a20b5c/rasterio-1.4.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a2401e4c43a31c7382154d4042b60a63b9bca5886802983c5c9362cdc5b09548", size = 34153931, upload-time = "2025-12-12T18:00:33.852Z" }, + { url = "https://files.pythonhosted.org/packages/05/2d/cd778286b910db7a3f0bc1743ca362173f1fbb7365137e4982ca857b6d26/rasterio-1.4.4-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6c4287d8934d953f7870b8e2a1df1096fbf47eba39ad0f777a31ea500f4e5010", size = 35421139, upload-time = "2025-12-12T18:00:37.482Z" }, + { url = "https://files.pythonhosted.org/packages/70/97/13a2e33aede8d7a42178c696a6a93868d1f9560f73de05033a1675f0806a/rasterio-1.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:c3ba1871549221140661227dd4fa1f9a472ded4a6d2f2c2e367b0648bb15b99d", size = 26419132, upload-time = "2025-12-12T18:00:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/27/d8/2dcfcb362d6a2fd07c14cfb803a345a7926d4d9fb6243e196df105671e97/rasterio-1.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:7c9d7dc824cb8d222808be153643cd4e65ea3e1f66019ada1ccd630221edfe30", size = 24800998, upload-time = "2025-12-12T18:00:45.332Z" }, + { url = "https://files.pythonhosted.org/packages/13/f8/16e9b648e7f16cadb41df7c0116dbab26b4a2ba02c85cbe3f744065bdf56/rasterio-1.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98e17bded830a59992d9f8f8d9f227ce1c4be0694930afcc4360358f5cb1a5db", size = 21247046, upload-time = "2025-12-12T18:00:49.429Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ea/f3dc3a25d7591821d488f5c5eb89f6abcd1f5c8e2ef4bd2792f965cbc9c8/rasterio-1.4.4-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:56134ca203f952855e60774b06672033cf65057eb9810fcc5c1a75f1921053a3", size = 25821677, upload-time = "2025-12-12T18:00:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d3/1e038350218e852f904c8dc4ab751aa023a2e82e68998767b7b42e33832c/rasterio-1.4.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:52edde65515b33fe4314c8a44a9ee2fc00b550deed6d56e1a8d085d42bbca3e6", size = 34829572, upload-time = "2025-12-12T18:00:56.294Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/28abf7a5f5d9cb014c2e14cc396bebe953b3deefbf604d49f4322e73fa35/rasterio-1.4.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d61d3f2c171c64050bd75e54a5d964ff7f165b3f5d2b92c9ee09b9716aa1b8bf", size = 35735171, upload-time = "2025-12-12T18:00:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/54/91/1ce35cfda2d56dacd6395faf20a5290268bd9009c53393ac42b5f9bb2c4c/rasterio-1.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:40137fe512c0d6e96c0167a0ae4e56d82c488f244163c45494b7392e51c844de", size = 26700712, upload-time = "2025-12-12T18:01:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/3b/33/4d13f48a8f01d782ffc1eece20821586518f3f515dca7cf152bca9fd22d4/rasterio-1.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:29ec3a794454b5bb255c9c0374cc380030a8a1e295c81eee7feb036802d2a9e3", size = 24875933, upload-time = "2025-12-12T18:01:06.134Z" }, +] + +[[package]] +name = "rasterio" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "affine", marker = "python_full_version >= '3.12'" }, + { name = "attrs", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "click", marker = "python_full_version >= '3.12'" }, + { name = "cligj", marker = "python_full_version >= '3.12'" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "pyparsing", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/88/edb4b66b6cb2c13f123af5a3896bf70c0cbe73ab3cd4243cb4eb0212a0f6/rasterio-1.5.0.tar.gz", hash = "sha256:1e0ea56b02eea4989b36edf8e58a5a3ef40e1b7edcb04def2603accd5ab3ee7b", size = 452184, upload-time = "2026-01-05T16:06:47.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/de/ba1cd11d7d1182bfb26e758bf07016d04e5442f4f5fea35b0d7279b72399/rasterio-1.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:420656074897a460f5ef46f657b3061d2e004f9d99e613914b0671643e69d92c", size = 22787192, upload-time = "2026-01-05T16:05:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/efaeb6dc531dbcd02fec01c791a853bb5a139a5126ecec579ac0f735eeb9/rasterio-1.5.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:c5c3597a783857e760550e8f26365d928b0377ac5ffc3e12ba447ac65ca5406d", size = 24412221, upload-time = "2026-01-05T16:05:22.526Z" }, + { url = "https://files.pythonhosted.org/packages/a2/14/89645988424c40cbcb8334f94305ffe094dd28d85c643341d9690704c9f0/rasterio-1.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e14d07a09833b6df6024ce7a57aee1e1977b3aec682e30b1e58ce773462f2382", size = 36128020, upload-time = "2026-01-05T16:05:25.556Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/5a52319a98451ff910f42e5f7f4804bfb39f9327933a89daab685d1ce2dd/rasterio-1.5.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26dbcffcf0d01fc121cbb92186bc1cb78e16efe62b17be45ad7494446b325cf8", size = 37634010, upload-time = "2026-01-05T16:05:28.673Z" }, + { url = "https://files.pythonhosted.org/packages/57/d6/fe8826f813c98b046d8d4c3bc83053c89c71f367f89257d211fe5dd0b0ba/rasterio-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac8d04eee66ca8060763ead607800e5611d857dd005905d920365e24a16ba20a", size = 30142328, upload-time = "2026-01-05T16:05:31.357Z" }, + { url = "https://files.pythonhosted.org/packages/af/62/6397379271d5628ed65ef781bf2d3a8f56094a86e6d8479c6ca506a1b960/rasterio-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:31f1edc45c781ebd087e60cc00a4fc37028dd3fe25cff4098e4139fc9d0565be", size = 28500710, upload-time = "2026-01-05T16:05:33.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/87/42865a77cebf2e524d27b6afc71db48984799ecd1dbe6a213d4713f42f5f/rasterio-1.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e7b25b0a19975ccd511e507e6de45b0a2d8fb6802abe49bb726cf48588e34833", size = 22776107, upload-time = "2026-01-05T16:05:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e81683fbbfdf04e019e68b042d9cff8524b0571aa80e4f4d81c373c31a49/rasterio-1.5.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1162c18eaece9f6d2aa1c2ff6b373b99651d93f113f24120a991eaebf28aa4f4", size = 24401477, upload-time = "2026-01-05T16:05:39.702Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3c/6aa6e0690b18eea02a61739cb362a47c5df66138f0a02cc69e1181b964e5/rasterio-1.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8eb87fd6f843eea109f3df9bef83f741b053b716b0465932276e2c0577dfb929", size = 36018214, upload-time = "2026-01-05T16:05:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/1af9aa9810fb30668568f2c4dd3eec2412c8e9762b69201d971c509b295e/rasterio-1.5.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:08a7580cbb9b3bd320bdf827e10c9b2424d0df066d8eef6f2feb37e154ce0c17", size = 37544972, upload-time = "2026-01-05T16:05:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/01/62/bfe3408743c9837919ff232474a09ece9eaa88d4ee8c040711fa3dff6dad/rasterio-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:d7d6729c0739b5ec48c33686668a30e27f5bdb361093f180ee7818ff19665547", size = 30140141, upload-time = "2026-01-05T16:05:48.751Z" }, + { url = "https://files.pythonhosted.org/packages/63/ca/e90e19a6d065a718cc3d468a12b9f015289ad17017656dea8c76f7318d1f/rasterio-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:8af7c368c22f0a99d1259ccc5a5cd96c432c2bde6f132c1ac78508cd7445a745", size = 28498556, upload-time = "2026-01-05T16:05:51.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ba/e37462d8c33bbbd6c152a0390ec6911a3d9614ded3d2bc6f6a48e147e833/rasterio-1.5.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b4ccfcc8ed9400e4f14efdf2005533fcf72048748b727f85ff89b9291ecdf98a", size = 22920107, upload-time = "2026-01-05T16:05:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/7bfa9cf96ac39b451b2f94dfc584c223ec584c52c148df2e4bab60c3341b/rasterio-1.5.0-cp313-cp313t-macosx_15_0_x86_64.whl", hash = "sha256:2f57c36ca4d3c896f7024226bd71eeb5cd10c8183c2a94508534d78cc05ff9e7", size = 24508993, upload-time = "2026-01-05T16:05:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/e5/55/7293743f3b69de4b726c67b8dc9da01fc194070b6becc51add4ca8a20a27/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cc1395475e4bb7032cd81dda4d5558061c4c7d5a50b1b5e146bdf9716d0b9353", size = 36565784, upload-time = "2026-01-05T16:06:00.019Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ef/5354c47de16c6e289728c3a3d6961ffcf7a9ad6313aef7e8db5d6a40c46e/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:592a485e2057b1aaeab4f843c9897628e60e3ff45e2509325c3e1479116599cb", size = 37686456, upload-time = "2026-01-05T16:06:02.772Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fc/fe1f034b1acd1900d9fbd616826d001a3d5811f1d0c97c785f88f525853e/rasterio-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0c739e70a72fb080f039ee1570c5d02b974dde32ded1a3216e1f13fe38ac4844", size = 30355842, upload-time = "2026-01-05T16:06:06.359Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cb/4dee9697891c9c6474b240d00e27688e03ecd882d3c83cc97eb25c2266ff/rasterio-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a3539a2f401a7b4b2e94ff2db334878c0e15a2d1c9fe90bb0879c52f89367ae5", size = 28589538, upload-time = "2026-01-05T16:06:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/77/9f/f84dfa54110c1c82f9f4fd929465d12519569b6f5d015273aa0957013b2e/rasterio-1.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:597be8df418d5ba7b6a927b6b9febfcb42b192882448a8d5b2e2e75a1296631f", size = 22788832, upload-time = "2026-01-05T16:06:12.247Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/de55255c918b17afd7292f793a3500c4aea7e9530b2b3f5b3a57836c7d49/rasterio-1.5.0-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:dd292030d39d685c0b35eddef233e7f1cb8b43052578a3ec97a2da57799693be", size = 24405917, upload-time = "2026-01-05T16:06:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/a9/57/054087a9d5011ad5dfa799277ba8814e41775e1967d37a59ab7b8e2f1876/rasterio-1.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:62c3f97a3c72643c74f2d0f310621a09c35c0c412229c327ae6bcc1ee4b9c3bc", size = 35987536, upload-time = "2026-01-05T16:06:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/5fbe5f67ae75d7e89ffb718c500d5fecbaa84f6ba354db306de689faf961/rasterio-1.5.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:19577f0f0c5f1158af47b57f73356961cbd1782a5f6ae6f3adf6f2650f4eb369", size = 37408048, upload-time = "2026-01-05T16:06:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/0c4ef19980204bdcbc8f9e084056adebc97916ff4edcc718750ef34e5bf9/rasterio-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:015c1ab6e5453312c5e29692752e7ad73568fe4d13567cbd448d7893128cbd2d", size = 30949590, upload-time = "2026-01-05T16:06:23.425Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d8/2e6b81505408926c00e629d7d3d73fd0454213201bd9907450e0fe82f3dd/rasterio-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:ff677c0a9d3ba667c067227ef2b76872488b37ff29b061bc3e576fad9baa3286", size = 29337287, upload-time = "2026-01-05T16:06:26.599Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/7b6e6afb28d4e3f69f2229f990ed87dfdc21a3e15ca63b96b2fd9ba17d89/rasterio-1.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:508251b9c746d8d008771a30c2160ff321bfc3b41f6a1aa8e8ef1dd4a00d97ba", size = 22926149, upload-time = "2026-01-05T16:06:29.617Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/19345d8bc7d2b96c1172594026b9009702e9ab9f0baf07079d3612aaadae/rasterio-1.5.0-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:742841ed48bc70f6ef517b8fa3521f231780bf408fde0aa6d73770337a36374e", size = 24516040, upload-time = "2026-01-05T16:06:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/dc7a4518fa78904bc41952cbf346c3c2a88a20e61b479154058392914c0b/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c9a9eee49ce9410c2f352b34c370bb3a96bb518b6a7f97b3a72ee4c835fd4b5c", size = 36589519, upload-time = "2026-01-05T16:06:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/8f706083c6c163054d12c7ed6d5ac4e4ed02252b761288d74e6158871b34/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b9fd87a0b63ab5c6267dfb0bc96f54fdf49d000651b9ee85ed37798141cff046", size = 37714599, upload-time = "2026-01-05T16:06:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d5/bbca726d5fea5864f7e4bcf3ee893095369e93ad51120495e8c40e2aa1a0/rasterio-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f459db8953ba30ca04fcef2b5e1260eeeff0eae8158bd9c3d6adbe56289765cc", size = 31233931, upload-time = "2026-01-05T16:06:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d1/8b017856e63ccaff3cbd0e82490dbb01363a42f3a462a41b1d8a391e1443/rasterio-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f4b9c2c3b5f10469eb9588f105086e68f0279e62cc9095c4edd245e3f9b88c8a", size = 29418321, upload-time = "2026-01-05T16:06:44.758Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1847,6 +2040,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" }, ] +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + [[package]] name = "virtualenv" version = "21.4.2" @@ -1898,6 +2100,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] +[[package]] +name = "xarray" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/a6/6fe936a798a3a38a79c7422d1a31afd2e9a14690fcb0ccff96bc01f04bf2/xarray-2026.4.0.tar.gz", hash = "sha256:c4ac9a01a945d90d5b1628e2af045099a9d4943536d4f2ee3ae963c3b222d15b", size = 3132311, upload-time = "2026-04-13T19:45:36.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl", hash = "sha256:d43751d9fb4a90f9249c30431684f00c41bc874f1edccd862631a40cbc0edf08", size = 1414326, upload-time = "2026-04-13T19:45:34.659Z" }, +] + [[package]] name = "zarr" version = "3.1.5"