Skip to content
23 changes: 22 additions & 1 deletion services/api/api/resources/grids/fbfm13/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
domain_id comes from the URL path parameter, not the request body.
"""

from lib.landfire import LANDFIRE_VERSIONS

EXAMPLE_FBFM13_MINIMAL = {}

EXAMPLE_FBFM13_WITH_METADATA = {
Expand Down Expand Up @@ -37,6 +39,11 @@
"alignment": {"target": "native"},
}

EXAMPLE_FBFM13_LATEST_RELEASE = {
"name": "FBFM13 latest release",
"version": LANDFIRE_VERSIONS["fbfm13"]["lfps_available"][0],
}

CREATE_LANDFIRE_FBFM13_OPENAPI_EXAMPLES = {
"minimal": {
"value": EXAMPLE_FBFM13_MINIMAL,
Expand Down Expand Up @@ -94,13 +101,27 @@
"domain-anchored grids without further alignment."
),
},
"latest_release": {
"value": EXAMPLE_FBFM13_LATEST_RELEASE,
"summary": "Latest release",
"description": (
"Fetches the latest FBFM13 release on demand from LANDFIRE "
"Product Service as it becomes available region by region — "
"more current landscape conditions than the staged national release. "
"See https://landfire.gov/data for the delivery schedule."
),
},
Comment thread
lindsaywiard marked this conversation as resolved.
}

ALL_FBFM13_EXAMPLE_VALUES = [
STAGED_FBFM13_EXAMPLE_VALUES = [
("minimal", EXAMPLE_FBFM13_MINIMAL),
("with_metadata", EXAMPLE_FBFM13_WITH_METADATA),
("remove_non_burnable", EXAMPLE_FBFM13_REMOVE_NON_BURNABLE),
("with_buffer", EXAMPLE_FBFM13_WITH_BUFFER),
("domain_aligned_2m", EXAMPLE_FBFM13_DOMAIN_2M),
("native_anchor", EXAMPLE_FBFM13_NATIVE),
]

LFPS_FBFM13_EXAMPLE_VALUES = [
("latest_release", EXAMPLE_FBFM13_LATEST_RELEASE),
]
11 changes: 11 additions & 0 deletions services/api/api/resources/grids/fbfm13/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Router for FBFM13 grid product endpoints.
"""

import asyncio
import uuid
from datetime import datetime
from typing import Annotated
Expand All @@ -25,11 +26,13 @@
from api.resources.grids.utils import (
dump_modifications_for_firestore,
validate_feature_modifications,
validate_lfps_coverage,
validate_target_grid_alignment,
)
from api.schema import JobStatus
from api.tasks import create_http_task_async
from lib.config import GRIDDLE_QUEUE, GRIDDLE_SERVICE, GRIDS_COLLECTION
from lib.landfire import LANDFIRE_VERSIONS

router = APIRouter()

Expand Down Expand Up @@ -82,6 +85,14 @@ async def create_landfire_fbfm13(
await validate_target_grid_alignment(body.alignment, owner_id, domain_id)
await validate_feature_modifications(body.modifications, owner_id, domain_id)

if body.version in LANDFIRE_VERSIONS["fbfm13"]["lfps_available"]:
await asyncio.to_thread(
validate_lfps_coverage,
"fbfm13",
body.version,
domain,
)

grid_id = uuid.uuid4().hex
request_time = datetime.now()
source = LandfireFbfm13Source(
Expand Down
23 changes: 18 additions & 5 deletions services/api/api/resources/grids/fbfm13/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from enum import StrEnum
from typing import Literal

from pydantic import field_validator
from pydantic import Field, field_validator

from api.resources.grids.providers.landfire import (
LandfireSource,
Expand All @@ -22,10 +22,18 @@
from api.resources.grids.schema import Band, BandType, CreateSourceGridRequestBase
from lib.landfire import LANDFIRE_VERSIONS

# Build the version enum class from LANDFIRE_VERSIONS
# Build the version enum class from LANDFIRE_VERSIONS, combining both
# "available" (staged annual) and "lfps_available" (current-year, fetched
# on demand from LANDFIRE Product Service) versions.
_FBFM13_ALL_VERSIONS = list(
dict.fromkeys(
LANDFIRE_VERSIONS["fbfm13"]["available"]
+ LANDFIRE_VERSIONS["fbfm13"]["lfps_available"]
)
)
LandfireFbfm13Version = StrEnum(
"LandfireFbfm13Version",
{f"v{version}": version for version in LANDFIRE_VERSIONS["fbfm13"]["available"]},
{f"v{version}": version for version in _FBFM13_ALL_VERSIONS},
)


Expand All @@ -50,8 +58,13 @@ class CreateLandfireFbfm13Request(CreateSourceGridRequestBase):
To convert codes to fuel parameters, use /grids/lookup/fbfm13.
"""

version: LandfireFbfm13Version = LandfireFbfm13Version(
LANDFIRE_VERSIONS["fbfm13"]["default"]
version: LandfireFbfm13Version = Field(
default=LandfireFbfm13Version(LANDFIRE_VERSIONS["fbfm13"]["default"]),
description=(
"LANDFIRE version. Most years are served from staged LANDFIRE data; "
f"{', '.join(LANDFIRE_VERSIONS['fbfm13']['lfps_available'])} "
"is instead fetched on demand from LANDFIRE Product Service."
),
)
remove_non_burnable: list[NonBurnableFuelModel] | None = None

Expand Down
18 changes: 17 additions & 1 deletion services/api/api/resources/grids/fbfm40/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@
}

EXAMPLE_FBFM40_SEASONAL = {
"name": "FBFM40 spring seasonal fuels",
"name": "FBFM40 early spring seasonal fuels",
"version": LANDFIRE_VERSIONS["fbfm40"]["lfps_available"][0],
"season": "ES",
}

EXAMPLE_FBFM40_LATEST_RELEASE = {
"name": "FBFM40 latest release",
"version": LANDFIRE_VERSIONS["fbfm40"]["lfps_available"][0],
}

CREATE_LANDFIRE_FBFM40_OPENAPI_EXAMPLES = {
"minimal": {
"value": EXAMPLE_FBFM40_MINIMAL,
Expand Down Expand Up @@ -112,6 +117,16 @@
"currently serves on-demand."
),
},
"latest_release": {
"value": EXAMPLE_FBFM40_LATEST_RELEASE,
"summary": "Latest release",
"description": (
"Fetches the latest FBFM40 release on demand from LANDFIRE "
"Product Service as it becomes available region by region — "
"more current landscape conditions than the staged national release. "
"See https://landfire.gov/data for the delivery schedule."
),
},
}

STAGED_FBFM40_EXAMPLE_VALUES = [
Expand All @@ -125,4 +140,5 @@

LFPS_FBFM40_EXAMPLE_VALUES = [
("seasonal", EXAMPLE_FBFM40_SEASONAL),
("latest_release", EXAMPLE_FBFM40_LATEST_RELEASE),
]
14 changes: 8 additions & 6 deletions services/api/api/resources/grids/fbfm40/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from api.schema import JobStatus
from api.tasks import create_http_task_async
from lib.config import GRIDDLE_QUEUE, GRIDDLE_SERVICE, GRIDS_COLLECTION
from lib.landfire import resolve_seasonal_product
from lib.landfire import LANDFIRE_VERSIONS, resolve_seasonal_product

router = APIRouter()

Expand Down Expand Up @@ -73,11 +73,8 @@ async def create_landfire_fbfm40(
- **description**: (optional) Description.
- **tags**: (optional) Tags for organizing grids.
- **version**: (optional) LANDFIRE version. Default: "2024".
Fetches data from a saved copy of the annual release, unless `season` is set.
- **season**: (optional) LANDFIRE Seasonal Fuels release: "ES" (early
spring), "SP" (spring), "SU" (summer), or "FA" (fall). Setting
`season` fetches data from the LANDFIRE Product Service on demand
rather than a saved annual copy.
spring), "SP" (spring), "SU" (summer), or "FA" (fall).

## Response

Expand All @@ -96,14 +93,19 @@ async def create_landfire_fbfm40(

await validate_target_grid_alignment(body.alignment, owner_id, domain_id)
await validate_feature_modifications(body.modifications, owner_id, domain_id)
if body.season is not None:
use_lfps = (
body.season is not None
or body.version in LANDFIRE_VERSIONS["fbfm40"]["lfps_available"]
)
if use_lfps:
await asyncio.to_thread(
validate_lfps_coverage,
"fbfm40",
body.version,
domain,
season=body.season,
)
if body.season is not None:
# Read the represented year off the live LFPS catalog entry rather
# than assuming it is `version + 1`. Coverage validation above already
# confirmed the product is live, so the match is present (cached call).
Expand Down
23 changes: 10 additions & 13 deletions services/api/api/resources/grids/fbfm40/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,13 @@ class CreateLandfireFbfm40Request(CreateSourceGridRequestBase):
version: LandfireFbfm40Version = Field(
default=LandfireFbfm40Version(LANDFIRE_VERSIONS["fbfm40"]["default"]),
description=(
"LANDFIRE landscape vintage year (e.g. '2024'). With `season` set, "
"this is the base vintage for LANDFIRE Seasonal Fuels; the calendar "
"year the data represents is reported back as `year`."
"LANDFIRE landscape vintage year (e.g. '2024'). Most years are "
"served from staged LANDFIRE data; version "
f"{', '.join(LANDFIRE_VERSIONS['fbfm40']['lfps_available'])} "
"is instead fetched on demand from LANDFIRE Product Service. "
"With `season` set, this is the base vintage for LANDFIRE "
"Seasonal Fuels; the calendar year the data represents is "
"reported back as `year`."
),
)
remove_non_burnable: list[NonBurnableFuelModel] | None = None
Expand Down Expand Up @@ -121,18 +125,11 @@ def check_version_matches_season(self):
"""

versions = LANDFIRE_VERSIONS["fbfm40"]
if self.season is None and self.version not in versions["available"]:
raise ValueError(
f"version {self.version} is only available for seasonal "
f"(season=...) requests. Available annual versions: "
f"{', '.join(versions['available'])}."
)

if self.season is not None and self.version not in versions["lfps_available"]:
raise ValueError(
f"version {self.version} is not available for LANDFIRE "
f"Seasonal Fuels. Available seasonal versions: "
f"{', '.join(versions['lfps_available'])}."
f"LANDFIRE Seasonal Fuels are only available for version "
f"{', '.join(versions['lfps_available'])}, not for version "
f"{self.version}."
)

return self
Expand Down
23 changes: 22 additions & 1 deletion services/api/api/resources/grids/fccs/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
domain_id comes from the URL path parameter, not the request body.
"""

from lib.landfire import LANDFIRE_VERSIONS

EXAMPLE_FCCS_MINIMAL = {}

EXAMPLE_FCCS_WITH_METADATA = {
Expand Down Expand Up @@ -37,6 +39,11 @@
"alignment": {"target": "native"},
}

EXAMPLE_FCCS_LATEST_RELEASE = {
"name": "FCCS latest release",
"version": LANDFIRE_VERSIONS["fccs"]["lfps_available"][0],
}

CREATE_LANDFIRE_FCCS_OPENAPI_EXAMPLES = {
"minimal": {
"value": EXAMPLE_FCCS_MINIMAL,
Expand Down Expand Up @@ -93,13 +100,27 @@
"domain-anchored grids without further alignment."
),
},
"latest_release": {
"value": EXAMPLE_FCCS_LATEST_RELEASE,
"summary": "Latest release",
"description": (
"Fetches the latest FCCS release on demand from LANDFIRE "
"Product Service as it becomes available region by region — "
"more current landscape conditions than the staged national release. "
"See https://landfire.gov/data for the delivery schedule."
),
},
}

ALL_FCCS_EXAMPLE_VALUES = [
STAGED_FCCS_EXAMPLE_VALUES = [
("minimal", EXAMPLE_FCCS_MINIMAL),
("with_metadata", EXAMPLE_FCCS_WITH_METADATA),
("remove_bare_ground", EXAMPLE_FCCS_REMOVE_BARE_GROUND),
("with_buffer", EXAMPLE_FCCS_WITH_BUFFER),
("domain_aligned_2m", EXAMPLE_FCCS_DOMAIN_2M),
("native_anchor", EXAMPLE_FCCS_NATIVE),
]

LFPS_FCCS_EXAMPLE_VALUES = [
("latest_release", EXAMPLE_FCCS_LATEST_RELEASE),
]
11 changes: 11 additions & 0 deletions services/api/api/resources/grids/fccs/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Router for FCCS grid product endpoints.
"""

import asyncio
import uuid
from datetime import datetime
from typing import Annotated
Expand All @@ -25,11 +26,13 @@
from api.resources.grids.utils import (
dump_modifications_for_firestore,
validate_feature_modifications,
validate_lfps_coverage,
validate_target_grid_alignment,
)
from api.schema import JobStatus
from api.tasks import create_http_task_async
from lib.config import GRIDDLE_QUEUE, GRIDDLE_SERVICE, GRIDS_COLLECTION
from lib.landfire import LANDFIRE_VERSIONS

router = APIRouter()

Expand Down Expand Up @@ -86,6 +89,14 @@ async def create_landfire_fccs(
await validate_target_grid_alignment(body.alignment, owner_id, domain_id)
await validate_feature_modifications(body.modifications, owner_id, domain_id)

if body.version in LANDFIRE_VERSIONS["fccs"]["lfps_available"]:
await asyncio.to_thread(
validate_lfps_coverage,
"fccs",
body.version,
domain,
)

grid_id = uuid.uuid4().hex
request_time = datetime.now()
source = LandfireFccsSource(
Expand Down
23 changes: 19 additions & 4 deletions services/api/api/resources/grids/fccs/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@
from enum import StrEnum
from typing import Literal

from pydantic import Field

from api.resources.grids.providers.landfire import LandfireSource
from api.resources.grids.schema import Band, BandType, CreateSourceGridRequestBase
from lib.landfire import LANDFIRE_VERSIONS

# Build the version enum class from LANDFIRE_VERSIONS
# Build the version enum class from LANDFIRE_VERSIONS, combining both
# "available" (staged annual) and "lfps_available" (current-year, fetched
# on demand from LANDFIRE Product Service) versions.
_FCCS_ALL_VERSIONS = list(
dict.fromkeys(
LANDFIRE_VERSIONS["fccs"]["available"]
+ LANDFIRE_VERSIONS["fccs"]["lfps_available"]
)
)
LandfireFccsVersion = StrEnum(
"LandfireFccsVersion",
{f"v{version}": version for version in LANDFIRE_VERSIONS["fccs"]["available"]},
{f"v{version}": version for version in _FCCS_ALL_VERSIONS},
)


Expand All @@ -41,8 +51,13 @@ class CreateLandfireFccsRequest(CreateSourceGridRequestBase):
To convert IDs to fuel parameters, use /grids/lookup/fccs.
"""

version: LandfireFccsVersion = LandfireFccsVersion(
LANDFIRE_VERSIONS["fccs"]["default"]
version: LandfireFccsVersion = Field(
default=LandfireFccsVersion(LANDFIRE_VERSIONS["fccs"]["default"]),
description=(
"LANDFIRE version. Most years are served from staged LANDFIRE "
f"data; {', '.join(LANDFIRE_VERSIONS['fccs']['lfps_available'])} "
"is instead fetched on demand from LANDFIRE Product Service."
),
)
remove_bare_ground: bool = False

Expand Down
Loading
Loading