Spaces:
Sleeping
Sleeping
| """FastAPI entry for PRO materialization (IMP-113 P0–P2).""" | |
| from __future__ import annotations | |
| from contextlib import asynccontextmanager | |
| from typing import NoReturn | |
| from fastapi import FastAPI, HTTPException | |
| from pro_materialization_service import __version__ | |
| from pro_materialization_service.geospatial.asset_version import s2_asset_mapping_version | |
| from pro_materialization_service.geospatial.pipeline import materialize | |
| from pro_materialization_service.inference_hmac import hmac_secret, install_hmac_middleware, require_inbound_hmac | |
| from pro_materialization_service.models import ( | |
| MaterializeRequest, | |
| MaterializeResult, | |
| MaterializeStubIn, | |
| MaterializeStubOut, | |
| ) | |
| async def _lifespan(app: FastAPI): | |
| if require_inbound_hmac() and not hmac_secret(): | |
| raise RuntimeError( | |
| "NUTONIC_INFERENCE_REQUIRE_INBOUND_HMAC is enabled but " | |
| "NUTONIC_INFERENCE_HMAC_SECRET / INFERENCE_HMAC_SECRET is empty", | |
| ) | |
| yield | |
| app = FastAPI(title="NU:TONIC PRO materialization service", version=__version__, lifespan=_lifespan) | |
| install_hmac_middleware(app) | |
| def health() -> dict[str, str]: | |
| return { | |
| "status": "ok", | |
| "service": "pro_materialization_service", | |
| "version": __version__, | |
| } | |
| def internal_healthz() -> dict[str, object]: | |
| """Control-plane probe (`plans/2026-04-12-...` §7.2).""" | |
| return { | |
| "ok": True, | |
| "version": __version__, | |
| "s2_asset_mapping_version": s2_asset_mapping_version(), | |
| } | |
| def _materialize_http_errors(e: ValueError) -> NoReturn: | |
| code = str(e) | |
| if code == "VLM_CONTRACT_INCLUDES_MAPBOX_RGB": | |
| raise HTTPException( | |
| status_code=422, | |
| detail={ | |
| "code": code, | |
| "message": "vlm_contract_id must not include mapbox_rgb; use nutonic.pro.vlm.v1_512_s2_only.", | |
| }, | |
| ) from e | |
| if code == "MINIMAL_RGB_UNSUPPORTED_USE_TERRAMIND_SPECTRAL": | |
| raise HTTPException( | |
| status_code=422, | |
| detail={ | |
| "code": code, | |
| "message": "sentinel_fetch_mode MINIMAL_RGB is not supported; use TERRAMIND_SPECTRAL or FULL_STAC.", | |
| }, | |
| ) from e | |
| if code.startswith("MAPBOX_HTTP_") or code == "MAPBOX_TRANSPORT_ERROR": | |
| raise HTTPException(status_code=502, detail={"code": code}) from e | |
| if code == "MAPBOX_TOKEN_MISSING": | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code == "UNKNOWN_VLM_CONTRACT": | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code == "VLM_CONTRACT_REQUIRES_SENTINEL_STACK": | |
| raise HTTPException( | |
| status_code=422, | |
| detail={ | |
| "code": code, | |
| "message": "This vlm_contract_id includes sentinel_fc / cloud_mask_thumb; use TERRAMIND_SPECTRAL or FULL_STAC.", | |
| }, | |
| ) from e | |
| if code in ("VLM_ROLE_SENTINEL_FC_WITHOUT_STACK", "VLM_ROLE_CLOUD_MASK_WITHOUT_SCL"): | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code.startswith("UNSUPPORTED_VLM_ROLE_"): | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code in ( | |
| "TIM_BRANCH_REQUIRES_RGB_MAPBOX", | |
| "TIM_BRANCH_REQUIRES_S2L2A_FULL", | |
| "TIM_BRANCH_INVALID", | |
| "TIM_RGB_REQUIRES_MAPBOX_VLM_CONTRACT", | |
| "PROFILE_REQUIRES_TIM", | |
| "PROFILE_REQUIRES_SENTINEL_STACK", | |
| "PROFILE_REQUIRES_S2L2A_FULL", | |
| ): | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code in ("STAC_NO_ITEMS", "S2L2A_INCOMPLETE"): | |
| raise HTTPException(status_code=422, detail={"code": code}) from e | |
| if code == "S2_DEPENDENCIES_MISSING": | |
| raise HTTPException( | |
| status_code=503, | |
| detail={ | |
| "code": code, | |
| "message": ( | |
| "Sentinel/STAC imports failed (need pystac-client and rasterio). " | |
| "They are package dependencies; check the container/runtime environment." | |
| ), | |
| }, | |
| ) from e | |
| if code == "SENTINEL_PIPELINE_NOT_AVAILABLE": | |
| raise HTTPException( | |
| status_code=422, | |
| detail={ | |
| "code": code, | |
| "message": "Deprecated error code — use a supported sentinel_fetch_mode.", | |
| }, | |
| ) from e | |
| raise HTTPException(status_code=400, detail={"code": code}) from e | |
| def internal_materialize(body: MaterializeRequest) -> MaterializeResult: | |
| """ | |
| Sentinel-2–backed materialization (``TERRAMIND_SPECTRAL`` / ``FULL_STAC``); Mapbox VLM contracts are rejected. | |
| """ | |
| try: | |
| return materialize(body) | |
| except ValueError as e: | |
| _materialize_http_errors(e) | |
| def materialize_stub(body: MaterializeStubIn) -> MaterializeStubOut: | |
| """ | |
| Back-compat alias: same as ``POST /internal/v1/materialize`` with defaults. | |
| Prefer ``/internal/v1/materialize`` for orchestration (`docs/PRO-TAB-VLM-ORCHESTRATION-SPEC.md`). | |
| """ | |
| req = MaterializeRequest( | |
| latitude=body.latitude, | |
| longitude=body.longitude, | |
| tim_branch="S2L2A_full", | |
| sentinel_fetch_mode="TERRAMIND_SPECTRAL", | |
| enable_tim=False, | |
| vlm_contract_id="nutonic.pro.vlm.v1_512_s2_only", | |
| ) | |
| try: | |
| out = materialize(req) | |
| except ValueError as e: | |
| _materialize_http_errors(e) | |
| roles = [a.role for a in out.vlm_artifacts] | |
| return MaterializeStubOut( | |
| materialization_id=out.materialization_id, | |
| latitude=body.latitude, | |
| longitude=body.longitude, | |
| tim_input_branch=body.tim_input_branch, | |
| cache_key=out.cache_key, | |
| vlm_roles=roles, | |
| ) | |