Harden RC7 API response contracts
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-18 05:07:35 +02:00
parent 8e49b857dd
commit 0fae53a7de
36 changed files with 886 additions and 153 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from app.core.errors import AppError
from app.db.session import get_db
from app.models import Dataset
from app.schemas import Envelope, JobRead
from app.schemas.analysis import ChangeDetectionRequest
from app.services.change_detection_service import ChangeDetectionService
from app.services.job_service import JobService
@@ -14,7 +15,7 @@ from app.utils.response import envelope
router = APIRouter(prefix="/analysis", tags=["analysis"])
@router.post("/change-detection", response_model=dict)
@router.post("/change-detection", response_model=Envelope[JobRead])
def run_change_detection(
payload: ChangeDetectionRequest,
db: Session = Depends(get_db),
+6 -5
View File
@@ -8,14 +8,15 @@ from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models import Area
from app.schemas.area import AreaCreate, AreaUpdate
from app.schemas import Envelope
from app.schemas.area import AreaCreate, AreaList, AreaRead, AreaUpdate
from app.services.area_service import AreaService
from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}/areas", tags=["areas"])
@router.get("", response_model=dict)
@router.get("", response_model=Envelope[AreaList])
def list_areas(
project_id: UUID,
limit: int = Query(default=50, ge=1, le=200),
@@ -26,13 +27,13 @@ def list_areas(
return envelope({"items": [AreaService.serialize_area(area) for area in areas], "total": total, "limit": limit, "offset": offset})
@router.post("", status_code=201, response_model=dict)
@router.post("", status_code=201, response_model=Envelope[AreaRead])
def create_area(project_id: UUID, payload: AreaCreate, db: Session = Depends(get_db)):
area = AreaService.create_area(db, project_id, payload)
return envelope(AreaService.serialize_area(area))
@router.get("/{area_id}", response_model=dict)
@router.get("/{area_id}", response_model=Envelope[AreaRead])
def get_area(
project_id: UUID,
area_id: UUID,
@@ -44,7 +45,7 @@ def get_area(
return envelope(AreaService.serialize_area(area))
@router.patch("/{area_id}", response_model=dict)
@router.patch("/{area_id}", response_model=Envelope[AreaRead])
def update_area(
project_id: UUID,
area_id: UUID,
+13 -4
View File
@@ -6,7 +6,13 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.assistant import AssistantQueryRequest
from app.schemas import Envelope
from app.schemas.assistant import (
AssistantModelList,
AssistantQueryRequest,
AssistantQueryResponse,
AssistantStatus,
)
from app.services.geo_assistant_service import GeoAssistantService
from app.utils.response import envelope
@@ -14,12 +20,12 @@ from app.utils.response import envelope
router = APIRouter(tags=["assistant"])
@router.get("/assistant/status", response_model=dict)
@router.get("/assistant/status", response_model=Envelope[AssistantStatus])
def assistant_status() -> dict:
return envelope(GeoAssistantService().status().model_dump())
@router.get("/assistant/models", response_model=dict)
@router.get("/assistant/models", response_model=Envelope[AssistantModelList])
def assistant_models() -> dict:
service = GeoAssistantService()
models = service.list_models()
@@ -32,7 +38,10 @@ def assistant_models() -> dict:
)
@router.post("/projects/{project_id}/assistant/query", response_model=dict)
@router.post(
"/projects/{project_id}/assistant/query",
response_model=Envelope[AssistantQueryResponse],
)
def assistant_query(
project_id: UUID,
payload: AssistantQueryRequest,
+222 -52
View File
@@ -13,6 +13,24 @@ from app.models import Area, Project
from app.core.errors import AppError
from app.db.session import get_db
from app.schemas import (
BathymetryPartitionFinalizationResult,
BathymetrySourceProbeRead,
BathymetrySourceRead,
DatasetList,
DhmvProductRead,
Envelope,
FloodHazardProductRead,
FloodHazardSelectionResponse,
GeoJsonFeatureCollection,
GrbProductRead,
GrbRefreshPlan,
ItemList,
JobRead,
OfficialVectorProductRead,
OrthophotoProductRead,
RasterMetadataResponse,
RasterOperationResult,
RasterPreviewResponse,
RasterClipRequest,
RasterStatsResponse,
RasterReprojectRequest,
@@ -23,6 +41,7 @@ from app.schemas import (
OrthophotoAcquireRequest,
DhmvAcquireRequest,
TerrainPartitionSelectionRequest,
TerrainSelectionResponse,
TerrainSelectionRequest,
FloodHazardAcquireRequest,
FloodHazardPartitionSelectionRequest,
@@ -30,6 +49,8 @@ from app.schemas import (
BathymetryPartitionFinalizeRequest,
BathymetryProfileAcquireRequest,
ThematicRasterAcquireRequest,
ThematicRasterProductRead,
ThematicRasterSelectionResponse,
ThematicRasterSelectionRequest,
GrbAcquireRequest,
OfficialVectorAcquireRequest,
@@ -37,12 +58,21 @@ from app.schemas import (
VectorBufferRequest,
VectorClipRequest,
VectorIntersectRequest,
VectorOperationResult,
VectorSelectionBBox, # noqa: F401 - retained as a route-module compatibility export
VectorSelectionDeriveRequest,
VectorSelectionRequest,
VectorSelectionResponse,
VectorStatsResponse,
)
from app.schemas.dataset import DatasetCreateResponse, DatasetTemporalUpdate
from app.schemas.dataset import (
DatasetCreateResponse,
DatasetTemporalUpdate,
DatasetVectorSummary,
DatasetVersionRead,
)
from app.schemas.source_catalog import SourceCatalogProbeReport
from app.schemas.source_freshness import SourceFreshnessReport
from app.services.job_service import JobService
from app.services.raster_operations_service import RasterOperationsService
from app.services.vector_operations_service import VectorOperationsService
@@ -100,7 +130,11 @@ def _run_job_sync(
)
@router.post("/datasets/upload", status_code=201, response_model=dict)
@router.post(
"/datasets/upload",
status_code=201,
response_model=Envelope[DatasetCreateResponse],
)
async def upload_dataset(
project_id: UUID,
file: UploadFile = File(...),
@@ -149,7 +183,7 @@ async def upload_dataset(
return envelope(created.model_dump())
@router.post("/datasets/orthophoto/acquire", response_model=dict)
@router.post("/datasets/orthophoto/acquire", response_model=Envelope[JobRead])
def acquire_bounded_orthophoto(
project_id: UUID,
payload: OrthophotoAcquireRequest,
@@ -165,7 +199,10 @@ def acquire_bounded_orthophoto(
return envelope(job)
@router.get("/datasets/orthophoto/products", response_model=dict)
@router.get(
"/datasets/orthophoto/products",
response_model=Envelope[ItemList[OrthophotoProductRead]],
)
def list_orthophoto_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -173,7 +210,7 @@ def list_orthophoto_products(project_id: UUID, db: Session = Depends(get_db)):
return envelope({"items": items, "total": len(items)})
@router.post("/datasets/dhmv/acquire", response_model=dict)
@router.post("/datasets/dhmv/acquire", response_model=Envelope[JobRead])
def acquire_bounded_dhmv(
project_id: UUID,
payload: DhmvAcquireRequest,
@@ -189,7 +226,10 @@ def acquire_bounded_dhmv(
return envelope(job)
@router.get("/datasets/dhmv/products", response_model=dict)
@router.get(
"/datasets/dhmv/products",
response_model=Envelope[ItemList[DhmvProductRead]],
)
def list_dhmv_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -197,7 +237,7 @@ def list_dhmv_products(project_id: UUID, db: Session = Depends(get_db)):
return envelope({"items": items, "total": len(items)})
@router.post("/datasets/grb/acquire", response_model=dict)
@router.post("/datasets/grb/acquire", response_model=Envelope[JobRead])
def acquire_bounded_grb(
project_id: UUID,
payload: GrbAcquireRequest,
@@ -213,7 +253,10 @@ def acquire_bounded_grb(
return envelope(job)
@router.get("/datasets/grb/products", response_model=dict)
@router.get(
"/datasets/grb/products",
response_model=Envelope[ItemList[GrbProductRead]],
)
def list_grb_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -221,7 +264,7 @@ def list_grb_products(project_id: UUID, db: Session = Depends(get_db)):
return envelope({"items": items, "total": len(items)})
@router.post("/datasets/official-vector/acquire", response_model=dict)
@router.post("/datasets/official-vector/acquire", response_model=Envelope[JobRead])
def acquire_bounded_official_vector(
project_id: UUID,
payload: OfficialVectorAcquireRequest,
@@ -237,7 +280,10 @@ def acquire_bounded_official_vector(
return envelope(job)
@router.get("/datasets/official-vector/products", response_model=dict)
@router.get(
"/datasets/official-vector/products",
response_model=Envelope[ItemList[OfficialVectorProductRead]],
)
def list_official_vector_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -245,7 +291,7 @@ def list_official_vector_products(project_id: UUID, db: Session = Depends(get_db
return envelope({"items": items, "total": len(items)})
@router.post("/datasets/flood-hazard/acquire", response_model=dict)
@router.post("/datasets/flood-hazard/acquire", response_model=Envelope[JobRead])
def acquire_bounded_flood_hazard(
project_id: UUID,
payload: FloodHazardAcquireRequest,
@@ -261,7 +307,10 @@ def acquire_bounded_flood_hazard(
return envelope(job)
@router.get("/datasets/flood-hazard/products", response_model=dict)
@router.get(
"/datasets/flood-hazard/products",
response_model=Envelope[ItemList[FloodHazardProductRead]],
)
def list_flood_hazard_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -269,7 +318,10 @@ def list_flood_hazard_products(project_id: UUID, db: Session = Depends(get_db)):
return envelope({"items": items, "total": len(items)})
@router.get("/datasets/bathymetry/sources", response_model=dict)
@router.get(
"/datasets/bathymetry/sources",
response_model=Envelope[ItemList[BathymetrySourceRead]],
)
def list_bathymetry_sources(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -277,14 +329,20 @@ def list_bathymetry_sources(project_id: UUID, db: Session = Depends(get_db)):
return envelope({"items": items, "total": len(items)})
@router.get("/datasets/bathymetry/sources/mdk_bcp_bathymetry/readiness", response_model=dict)
@router.get(
"/datasets/bathymetry/sources/mdk_bcp_bathymetry/readiness",
response_model=Envelope[BathymetrySourceProbeRead],
)
def probe_mdk_bathymetry_readiness(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
return envelope(MdkBathymetryProbeService.probe())
@router.post("/datasets/bathymetry/profiles/acquire", response_model=dict)
@router.post(
"/datasets/bathymetry/profiles/acquire",
response_model=Envelope[JobRead],
)
def acquire_bounded_bathymetry_profiles(
project_id: UUID,
payload: BathymetryProfileAcquireRequest,
@@ -300,7 +358,10 @@ def acquire_bounded_bathymetry_profiles(
return envelope(job)
@router.post("/datasets/bathymetry/profiles/partitions/finalize", response_model=dict)
@router.post(
"/datasets/bathymetry/profiles/partitions/finalize",
response_model=Envelope[BathymetryPartitionFinalizationResult],
)
def finalize_bathymetry_profile_partitions(
project_id: UUID,
payload: BathymetryPartitionFinalizeRequest,
@@ -309,7 +370,10 @@ def finalize_bathymetry_profile_partitions(
return envelope(BathymetryProfileAcquisitionService.finalize_partitions(db, project_id, payload))
@router.post("/datasets/bathymetry/profiles/partitions/select", response_model=dict)
@router.post(
"/datasets/bathymetry/profiles/partitions/select",
response_model=Envelope[VectorSelectionResponse],
)
def select_bathymetry_profile_partitions(
project_id: UUID,
payload: VectorSelectionRequest,
@@ -344,7 +408,7 @@ def select_bathymetry_profile_partitions(
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
@router.post("/datasets/thematic-raster/acquire", response_model=dict)
@router.post("/datasets/thematic-raster/acquire", response_model=Envelope[JobRead])
def acquire_bounded_thematic_raster(
project_id: UUID,
payload: ThematicRasterAcquireRequest,
@@ -360,7 +424,10 @@ def acquire_bounded_thematic_raster(
return envelope(job)
@router.get("/datasets/thematic-raster/products", response_model=dict)
@router.get(
"/datasets/thematic-raster/products",
response_model=Envelope[ItemList[ThematicRasterProductRead]],
)
def list_thematic_raster_products(project_id: UUID, db: Session = Depends(get_db)):
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
@@ -368,7 +435,7 @@ def list_thematic_raster_products(project_id: UUID, db: Session = Depends(get_db
return envelope({"items": items, "total": len(items)})
@router.get("/datasets", response_model=dict)
@router.get("/datasets", response_model=Envelope[DatasetList])
def list_datasets(
project_id: UUID,
limit: int = Query(default=50, ge=1, le=200),
@@ -379,7 +446,10 @@ def list_datasets(
return envelope({"items": [item.model_dump() for item in datasets], "total": total, "limit": limit, "offset": offset})
@router.get("/datasets/source-freshness", response_model=dict)
@router.get(
"/datasets/source-freshness",
response_model=Envelope[SourceFreshnessReport],
)
def audit_dataset_source_freshness(
project_id: UUID,
db: Session = Depends(get_db),
@@ -388,7 +458,10 @@ def audit_dataset_source_freshness(
return envelope(report.model_dump())
@router.get("/datasets/source-catalog-probes", response_model=dict)
@router.get(
"/datasets/source-catalog-probes",
response_model=Envelope[SourceCatalogProbeReport],
)
def probe_dataset_source_catalogs(
project_id: UUID,
refresh: bool = Query(default=False),
@@ -398,7 +471,10 @@ def probe_dataset_source_catalogs(
return envelope(report.model_dump())
@router.get("/datasets/grb-refresh-plan", response_model=dict)
@router.get(
"/datasets/grb-refresh-plan",
response_model=Envelope[GrbRefreshPlan],
)
def plan_grb_dataset_refresh(
project_id: UUID,
scope: str = Query(default=GrbRefreshPlanService.SCOPE),
@@ -414,7 +490,7 @@ def plan_grb_dataset_refresh(
return envelope(report.model_dump())
@router.get("/datasets/{dataset_id}", response_model=dict)
@router.get("/datasets/{dataset_id}", response_model=Envelope[DatasetCreateResponse])
def get_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -426,7 +502,10 @@ def get_dataset(
return envelope(DatasetCreateResponse.model_validate(dataset).model_dump())
@router.patch("/datasets/{dataset_id}/temporal", response_model=dict)
@router.patch(
"/datasets/{dataset_id}/temporal",
response_model=Envelope[DatasetCreateResponse],
)
def update_dataset_temporal_metadata(
project_id: UUID,
dataset_id: UUID,
@@ -440,7 +519,10 @@ def update_dataset_temporal_metadata(
return envelope(updated.model_dump())
@router.get("/datasets/{dataset_id}/versions", response_model=dict)
@router.get(
"/datasets/{dataset_id}/versions",
response_model=Envelope[ItemList[DatasetVersionRead]],
)
def list_dataset_versions(
project_id: UUID,
dataset_id: UUID,
@@ -453,7 +535,10 @@ def list_dataset_versions(
return envelope({"items": [item.model_dump() for item in versions], "total": len(versions)})
@router.post("/datasets/{dataset_id}/metadata/refresh", response_model=dict)
@router.post(
"/datasets/{dataset_id}/metadata/refresh",
response_model=Envelope[DatasetCreateResponse],
)
def refresh_dataset_metadata(
project_id: UUID,
dataset_id: UUID,
@@ -466,7 +551,10 @@ def refresh_dataset_metadata(
return envelope(refreshed.model_dump())
@router.get("/datasets/{dataset_id}/vector/inspect", response_model=dict)
@router.get(
"/datasets/{dataset_id}/vector/inspect",
response_model=Envelope[VectorOperationResult],
)
def inspect_vector_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -478,7 +566,10 @@ def inspect_vector_dataset(
return envelope(VectorOperationsService.inspect(db, dataset_id).model_dump())
@router.get("/datasets/{dataset_id}/vector/bbox", response_model=dict)
@router.get(
"/datasets/{dataset_id}/vector/bbox",
response_model=Envelope[VectorBBoxResponse],
)
def vector_bbox(
project_id: UUID,
dataset_id: UUID,
@@ -491,7 +582,10 @@ def vector_bbox(
return envelope(VectorBBoxResponse(**payload).model_dump())
@router.get("/datasets/{dataset_id}/vector/stats", response_model=dict)
@router.get(
"/datasets/{dataset_id}/vector/stats",
response_model=Envelope[VectorStatsResponse],
)
def vector_stats(
project_id: UUID,
dataset_id: UUID,
@@ -503,7 +597,10 @@ def vector_stats(
return envelope(VectorOperationsService.stats(db, dataset_id))
@router.post("/datasets/{dataset_id}/vector/select", response_model=dict)
@router.post(
"/datasets/{dataset_id}/vector/select",
response_model=Envelope[VectorSelectionResponse],
)
def select_vector_features(
project_id: UUID,
dataset_id: UUID,
@@ -562,7 +659,11 @@ def select_vector_features(
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
@router.post("/datasets/{dataset_id}/vector/select/derive", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/vector/select/derive",
status_code=201,
response_model=Envelope[DatasetCreateResponse],
)
def derive_vector_selection_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -597,7 +698,11 @@ def derive_vector_selection_dataset(
return envelope(derived.model_dump())
@router.post("/datasets/{dataset_id}/vector/clip", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/vector/clip",
status_code=201,
response_model=Envelope[JobRead],
)
def clip_vector_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -623,7 +728,11 @@ def clip_vector_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/vector/buffer", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/vector/buffer",
status_code=201,
response_model=Envelope[JobRead],
)
def buffer_vector_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -650,7 +759,11 @@ def buffer_vector_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/vector/intersect", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/vector/intersect",
status_code=201,
response_model=Envelope[JobRead],
)
def intersect_vector_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -676,7 +789,10 @@ def intersect_vector_dataset(
return envelope(job)
@router.get("/datasets/{dataset_id}/vector/summary", response_model=dict)
@router.get(
"/datasets/{dataset_id}/vector/summary",
response_model=Envelope[DatasetVectorSummary],
)
def vector_dataset_summary(
project_id: UUID,
dataset_id: UUID,
@@ -688,7 +804,10 @@ def vector_dataset_summary(
return envelope(DatasetService.vector_summary(db, dataset_id))
@router.get("/datasets/{dataset_id}/raster/inspect", response_model=dict)
@router.get(
"/datasets/{dataset_id}/raster/inspect",
response_model=Envelope[RasterOperationResult],
)
def raster_dataset_inspect(
project_id: UUID,
dataset_id: UUID,
@@ -701,7 +820,10 @@ def raster_dataset_inspect(
return envelope(payload)
@router.get("/datasets/{dataset_id}/raster/preview", response_model=dict)
@router.get(
"/datasets/{dataset_id}/raster/preview",
response_model=Envelope[RasterPreviewResponse],
)
def raster_preview_readiness(
project_id: UUID,
dataset_id: UUID,
@@ -727,7 +849,10 @@ def raster_orthophoto_image(
)
@router.post("/datasets/{dataset_id}/raster/terrain/select", response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/terrain/select",
response_model=Envelope[TerrainSelectionResponse],
)
def raster_terrain_selection(
project_id: UUID,
dataset_id: UUID,
@@ -737,7 +862,10 @@ def raster_terrain_selection(
return envelope(TerrainAnalysisService.analyze(db, project_id, dataset_id, payload))
@router.post("/datasets/raster/terrain/select", response_model=dict)
@router.post(
"/datasets/raster/terrain/select",
response_model=Envelope[TerrainSelectionResponse],
)
def partitioned_raster_terrain_selection(
project_id: UUID,
payload: TerrainPartitionSelectionRequest,
@@ -760,7 +888,10 @@ def raster_terrain_image(
)
@router.post("/datasets/{dataset_id}/raster/flood-hazard/select", response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/flood-hazard/select",
response_model=Envelope[FloodHazardSelectionResponse],
)
def raster_flood_hazard_selection(
project_id: UUID,
dataset_id: UUID,
@@ -770,7 +901,10 @@ def raster_flood_hazard_selection(
return envelope(FloodHazardAnalysisService.analyze(db, project_id, dataset_id, payload))
@router.post("/datasets/raster/flood-hazard/select", response_model=dict)
@router.post(
"/datasets/raster/flood-hazard/select",
response_model=Envelope[FloodHazardSelectionResponse],
)
def partitioned_raster_flood_hazard_selection(
project_id: UUID,
payload: FloodHazardPartitionSelectionRequest,
@@ -793,7 +927,10 @@ def raster_flood_hazard_image(
)
@router.post("/datasets/{dataset_id}/raster/thematic/select", response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/thematic/select",
response_model=Envelope[ThematicRasterSelectionResponse],
)
def raster_thematic_selection(
project_id: UUID,
dataset_id: UUID,
@@ -817,7 +954,10 @@ def raster_thematic_image(
)
@router.get("/datasets/{dataset_id}/raster/stats", response_model=dict)
@router.get(
"/datasets/{dataset_id}/raster/stats",
response_model=Envelope[RasterStatsResponse],
)
def raster_stats(
project_id: UUID,
dataset_id: UUID,
@@ -830,7 +970,11 @@ def raster_stats(
return envelope(RasterStatsResponse(**payload).model_dump())
@router.post("/datasets/{dataset_id}/raster/reproject", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/reproject",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_reproject_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -857,7 +1001,11 @@ def raster_reproject_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/clip", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/clip",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_clip_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -878,7 +1026,11 @@ def raster_clip_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/tile", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/tile",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_tile_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -905,7 +1057,11 @@ def raster_tile_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndvi", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/indices/ndvi",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_ndvi_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -932,7 +1088,11 @@ def raster_ndvi_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndwi", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/indices/ndwi",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_ndwi_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -959,7 +1119,11 @@ def raster_ndwi_dataset(
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndbi", status_code=201, response_model=dict)
@router.post(
"/datasets/{dataset_id}/raster/indices/ndbi",
status_code=201,
response_model=Envelope[JobRead],
)
def raster_ndbi_dataset(
project_id: UUID,
dataset_id: UUID,
@@ -986,7 +1150,10 @@ def raster_ndbi_dataset(
return envelope(job)
@router.get("/datasets/{dataset_id}/raster/metadata", response_model=dict)
@router.get(
"/datasets/{dataset_id}/raster/metadata",
response_model=Envelope[RasterMetadataResponse],
)
def raster_dataset_metadata(
project_id: UUID,
dataset_id: UUID,
@@ -998,7 +1165,10 @@ def raster_dataset_metadata(
return envelope(RasterOperationsService.metadata(db, dataset_id))
@router.get("/datasets/{dataset_id}/content", response_model=dict)
@router.get(
"/datasets/{dataset_id}/content",
response_model=Envelope[GeoJsonFeatureCollection],
)
def dataset_content(
project_id: UUID,
dataset_id: UUID,
+6 -1
View File
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas import Envelope
from app.schemas.demo import DemoWorkflowResponse
from app.services.demo_workflow_service import DemoWorkflowService
from app.utils.response import envelope
@@ -11,7 +12,11 @@ from app.utils.response import envelope
router = APIRouter(prefix="/demo", tags=["demo"])
@router.post("/workflow", status_code=status.HTTP_201_CREATED, response_model=dict)
@router.post(
"/workflow",
status_code=status.HTTP_201_CREATED,
response_model=Envelope[DemoWorkflowResponse],
)
def seed_demo_workflow(db: Session = Depends(get_db)) -> dict:
result: DemoWorkflowResponse = DemoWorkflowService.seed(db)
return envelope(result.model_dump())
+42 -13
View File
@@ -6,7 +6,21 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas import DetectionQaRequest, DetectionRunRequest
from app.schemas import (
AnalysisQaResponse,
DetectionListResponse,
DetectionModelsResponse,
DetectionQaRequest,
DetectionRead,
DetectionRunListResponse,
DetectionRunRead,
DetectionRunRequest,
DetectionRunResponse,
Envelope,
GeoJsonFeatureCollection,
ModelAssetListResponse,
YoloPreflightResponse,
)
from app.services.detection_service import DetectionService
from app.services.model_asset_catalog_service import ModelAssetCatalogService
from app.services.model_registry_service import ModelRegistryService
@@ -16,17 +30,17 @@ from app.utils.response import envelope
router = APIRouter(prefix="/detection", tags=["detection"])
@router.get("/models", response_model=dict)
@router.get("/models", response_model=Envelope[DetectionModelsResponse])
def list_detection_models() -> dict:
return envelope({"models": [model.model_dump() for model in ModelRegistryService.list_model_capabilities()]})
@router.get("/model-assets", response_model=dict)
@router.get("/model-assets", response_model=Envelope[ModelAssetListResponse])
def list_detection_model_assets() -> dict:
return envelope(ModelAssetCatalogService.list_assets().model_dump())
@router.get("/yolo/preflight", response_model=dict)
@router.get("/yolo/preflight", response_model=Envelope[YoloPreflightResponse])
def get_yolo_preflight(
tile_manifest_path: str | None = None,
check_model_load: bool = False,
@@ -41,7 +55,7 @@ def get_yolo_preflight(
)
@router.post("/run", response_model=dict)
@router.post("/run", response_model=Envelope[DetectionRunResponse])
def run_detection(payload: DetectionRunRequest, db: Session = Depends(get_db)) -> dict:
result = DetectionService.run_detection(
db=db,
@@ -57,7 +71,7 @@ def run_detection(payload: DetectionRunRequest, db: Session = Depends(get_db)) -
return envelope(result.model_dump())
@router.get("/runs", response_model=dict)
@router.get("/runs", response_model=Envelope[DetectionRunListResponse])
def list_detection_runs(
project_id: UUID | None = None,
dataset_id: UUID | None = None,
@@ -66,12 +80,15 @@ def list_detection_runs(
return envelope(DetectionService.list_runs(db, project_id=project_id, dataset_id=dataset_id).model_dump())
@router.get("/runs/{analysis_run_id}", response_model=dict)
@router.get("/runs/{analysis_run_id}", response_model=Envelope[DetectionRunRead])
def get_detection_run(analysis_run_id: UUID, db: Session = Depends(get_db)) -> dict:
return envelope(DetectionService.get_run(db, analysis_run_id).model_dump())
@router.get("/runs/{analysis_run_id}/detections", response_model=dict)
@router.get(
"/runs/{analysis_run_id}/detections",
response_model=Envelope[DetectionListResponse],
)
def list_detection_run_detections(
analysis_run_id: UUID,
dataset_id: UUID | None = None,
@@ -90,7 +107,10 @@ def list_detection_run_detections(
)
@router.get("/datasets/{dataset_id}/detections", response_model=dict)
@router.get(
"/datasets/{dataset_id}/detections",
response_model=Envelope[DetectionListResponse],
)
def list_dataset_detections(
dataset_id: UUID,
analysis_run_id: UUID | None = None,
@@ -109,12 +129,15 @@ def list_dataset_detections(
)
@router.get("/detections/{detection_id}", response_model=dict)
@router.get("/detections/{detection_id}", response_model=Envelope[DetectionRead])
def get_detection(detection_id: UUID, db: Session = Depends(get_db)) -> dict:
return envelope(DetectionService.get_detection(db, detection_id).model_dump())
@router.get("/runs/{analysis_run_id}/geojson", response_model=dict)
@router.get(
"/runs/{analysis_run_id}/geojson",
response_model=Envelope[GeoJsonFeatureCollection],
)
def get_detection_run_geojson(
analysis_run_id: UUID,
class_name: str | None = None,
@@ -131,7 +154,10 @@ def get_detection_run_geojson(
)
@router.get("/datasets/{dataset_id}/geojson", response_model=dict)
@router.get(
"/datasets/{dataset_id}/geojson",
response_model=Envelope[GeoJsonFeatureCollection],
)
def get_dataset_detection_geojson(
dataset_id: UUID,
analysis_run_id: UUID | None = None,
@@ -150,7 +176,10 @@ def get_dataset_detection_geojson(
)
@router.post("/runs/{analysis_run_id}/qa/reference", response_model=dict)
@router.post(
"/runs/{analysis_run_id}/qa/reference",
response_model=Envelope[AnalysisQaResponse],
)
def compare_detection_run_with_reference(
analysis_run_id: UUID,
payload: DetectionQaRequest,
+21 -8
View File
@@ -7,14 +7,24 @@ from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.export import GeoJsonExportRequest, MapResultExportRequest, MetadataExportRequest, ReportExportRequest
from app.schemas import Envelope
from app.schemas.export import (
ExportContentResponse,
ExportCreateResponse,
ExportListResponse,
ExportRead,
GeoJsonExportRequest,
MapResultExportRequest,
MetadataExportRequest,
ReportExportRequest,
)
from app.services.export_service import ExportService
from app.utils.response import envelope
router = APIRouter(prefix="/exports", tags=["exports"])
@router.post("/geojson", response_model=dict)
@router.post("/geojson", response_model=Envelope[ExportCreateResponse])
def export_geojson(payload: GeoJsonExportRequest, db: Session = Depends(get_db)):
if payload.export_kind == "vector_selection" and payload.dataset_id is not None and payload.bbox is not None:
return envelope(
@@ -40,22 +50,25 @@ def export_geojson(payload: GeoJsonExportRequest, db: Session = Depends(get_db))
return envelope({})
@router.post("/metadata", response_model=dict)
@router.post("/metadata", response_model=Envelope[ExportCreateResponse])
def export_project_metadata(payload: MetadataExportRequest, db: Session = Depends(get_db)):
return envelope(ExportService.export_project_metadata(db, payload.project_id, payload.name).model_dump(mode="json"))
@router.post("/report", response_model=dict)
@router.post("/report", response_model=Envelope[ExportCreateResponse])
def export_project_report(payload: ReportExportRequest, db: Session = Depends(get_db)):
return envelope(ExportService.export_project_report(db, payload.project_id, payload.name).model_dump(mode="json"))
@router.post("/map-result", response_model=dict)
@router.post("/map-result", response_model=Envelope[ExportCreateResponse])
def export_map_result(payload: MapResultExportRequest, db: Session = Depends(get_db)):
return envelope(ExportService.export_map_result(db, payload).model_dump(mode="json"))
@router.get("/projects/{project_id}/exports", response_model=dict)
@router.get(
"/projects/{project_id}/exports",
response_model=Envelope[ExportListResponse],
)
def list_project_exports(
project_id: UUID,
limit: int = Query(default=50, ge=1, le=100),
@@ -65,7 +78,7 @@ def list_project_exports(
return envelope(ExportService.list_project_exports(db, project_id, limit=limit, offset=offset).model_dump(mode="json"))
@router.get("/{export_id}", response_model=dict)
@router.get("/{export_id}", response_model=Envelope[ExportRead])
def get_export(export_id: UUID, db: Session = Depends(get_db)):
return envelope(ExportService.get_export(db, export_id).model_dump(mode="json"))
@@ -77,6 +90,6 @@ def download_export(export_id: UUID, db: Session = Depends(get_db)):
return FileResponse(path, filename=path.name, media_type=media_type)
@router.get("/{export_id}/content", response_model=dict)
@router.get("/{export_id}/content", response_model=Envelope[ExportContentResponse])
def get_export_content(export_id: UUID, db: Session = Depends(get_db)):
return envelope(ExportService.get_export_content(db, export_id).model_dump(mode="json"))
+39 -11
View File
@@ -7,7 +7,20 @@ from app.core.errors import AppError
from app.db.session import get_db
from app.models import Area, Project
from app.providers.registry import fetch_provider_data, get_provider, import_provider_dataset, list_provider_capabilities
from app.schemas import CoverageResolveRequest, ExternalFetchRequest, ExternalFetchResponse, ProviderImportRequest
from app.schemas import (
CoverageCatalogResponse,
CoverageResolveRequest,
CoverageResolveResponse,
Envelope,
ExternalFetchRequest,
ExternalFetchResponse,
ProviderCapabilitiesResponse,
ProviderCapabilityResponse,
ProviderImportRequest,
ProviderImportResponse,
ProviderLayersResponse,
ProviderStatusResponse,
)
from app.services.coverage_registry_service import CoverageRegistryService
from app.utils.response import envelope
@@ -40,19 +53,19 @@ def _provider_payload(provider_name: str) -> dict:
return get_provider(provider_name).capability.to_dict()
@router.get("/providers")
@router.get("/providers", response_model=Envelope[ProviderCapabilitiesResponse])
def list_external_providers() -> dict:
return envelope({
"providers": [provider.to_dict() for provider in list_provider_capabilities()],
})
@router.get("/coverage/catalog")
@router.get("/coverage/catalog", response_model=Envelope[CoverageCatalogResponse])
def get_coverage_catalog() -> dict:
return envelope(CoverageRegistryService.catalog().model_dump())
@router.post("/coverage/resolve")
@router.post("/coverage/resolve", response_model=Envelope[CoverageResolveResponse])
def resolve_project_coverage(payload: CoverageResolveRequest, db: Session = Depends(get_db)) -> dict:
result = CoverageRegistryService.resolve(
db,
@@ -63,19 +76,28 @@ def resolve_project_coverage(payload: CoverageResolveRequest, db: Session = Depe
return envelope(result.model_dump())
@router.get("/providers/capabilities")
@router.get(
"/providers/capabilities",
response_model=Envelope[ProviderCapabilitiesResponse],
)
def get_external_provider_capabilities() -> dict:
return envelope({
"providers": [provider.to_dict() for provider in list_provider_capabilities()],
})
@router.get("/providers/{provider_name}")
@router.get(
"/providers/{provider_name}",
response_model=Envelope[ProviderCapabilityResponse],
)
def get_external_provider(provider_name: str) -> dict:
return envelope(_provider_payload(provider_name))
@router.get("/providers/{provider_name}/layers")
@router.get(
"/providers/{provider_name}/layers",
response_model=Envelope[ProviderLayersResponse],
)
def get_external_provider_layers(provider_name: str) -> dict:
provider = get_provider(provider_name)
return envelope({
@@ -84,7 +106,10 @@ def get_external_provider_layers(provider_name: str) -> dict:
})
@router.get("/providers/{provider_name}/status")
@router.get(
"/providers/{provider_name}/status",
response_model=Envelope[ProviderStatusResponse],
)
def get_external_provider_status(provider_name: str) -> dict:
provider = get_provider(provider_name)
return envelope({
@@ -95,7 +120,10 @@ def get_external_provider_status(provider_name: str) -> dict:
})
@router.post("/providers/{provider_name}/import")
@router.post(
"/providers/{provider_name}/import",
response_model=Envelope[ProviderImportResponse],
)
def import_external_provider_dataset(provider_name: str, payload: ProviderImportRequest) -> dict:
result = import_provider_dataset(
provider_name=provider_name,
@@ -125,14 +153,14 @@ def _run_fetch(payload: ExternalFetchRequest, provider_name: str) -> ExternalFet
)
@router.post("/osm/fetch")
@router.post("/osm/fetch", response_model=Envelope[ExternalFetchResponse])
def fetch_osm(payload: ExternalFetchRequest, db: Session = Depends(get_db)) -> dict:
_assert_project_exists(db, payload.project_id)
_validate_area_in_project(db, payload.project_id, payload.area_id)
return envelope(_run_fetch(payload, "osm").model_dump())
@router.post("/grb/fetch")
@router.post("/grb/fetch", response_model=Envelope[ExternalFetchResponse])
def fetch_grb(payload: ExternalFetchRequest, db: Session = Depends(get_db)) -> dict:
_assert_project_exists(db, payload.project_id)
_validate_area_in_project(db, payload.project_id, payload.area_id)
+5 -5
View File
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas import JobCreate, JobList, JobRead, JobStatus
from app.schemas import Envelope, JobCreate, JobList, JobRead, JobStatus
from app.services.job_service import JobService
from app.utils.response import envelope
@@ -14,7 +14,7 @@ from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}", tags=["jobs"])
@router.post("/jobs", status_code=201, response_model=dict)
@router.post("/jobs", status_code=201, response_model=Envelope[JobRead])
def create_job(
project_id: UUID,
payload: JobCreate,
@@ -25,7 +25,7 @@ def create_job(
return envelope(JobService.create_job(db, payload).model_dump())
@router.get("/jobs", response_model=dict)
@router.get("/jobs", response_model=Envelope[JobList])
def list_jobs(
project_id: UUID,
dataset_id: UUID | None = Query(default=None),
@@ -43,7 +43,7 @@ def list_jobs(
return envelope(JobList(items=items, total=total, limit=limit, offset=offset).model_dump())
@router.get("/jobs/{job_id}", response_model=dict)
@router.get("/jobs/{job_id}", response_model=Envelope[JobRead])
def read_job(
project_id: UUID,
job_id: UUID,
@@ -55,7 +55,7 @@ def read_job(
return envelope(job.model_dump())
@router.get("/jobs/{job_id}/status", response_model=dict)
@router.get("/jobs/{job_id}/status", response_model=Envelope[JobStatus])
def read_job_status(
project_id: UUID,
job_id: UUID,
+11 -6
View File
@@ -7,14 +7,15 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
from app.schemas import Envelope
from app.schemas.project import ProjectCreate, ProjectDeleteResult, ProjectList, ProjectRead, ProjectUpdate
from app.services.project_service import ProjectService
from app.utils.response import envelope
router = APIRouter(prefix="/projects", tags=["projects"])
@router.get("", response_model=dict)
@router.get("", response_model=Envelope[ProjectList])
def list_projects(
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
@@ -32,13 +33,13 @@ def list_projects(
return envelope({"items": [ProjectRead.model_validate(item).model_dump() for item in projects], "total": total, "limit": limit, "offset": offset})
@router.post("", status_code=status.HTTP_201_CREATED, response_model=dict)
@router.post("", status_code=status.HTTP_201_CREATED, response_model=Envelope[ProjectRead])
def create_project(payload: ProjectCreate, db: Session = Depends(get_db)):
project = ProjectService.create_project(db, payload)
return envelope(ProjectRead.model_validate(project).model_dump())
@router.get("/{project_id}", response_model=dict)
@router.get("/{project_id}", response_model=Envelope[ProjectRead])
def get_project(project_id: UUID, db: Session = Depends(get_db)):
project = ProjectService.get_project(db, project_id)
if not project:
@@ -46,7 +47,7 @@ def get_project(project_id: UUID, db: Session = Depends(get_db)):
return envelope(ProjectRead.model_validate(project).model_dump())
@router.patch("/{project_id}", response_model=dict)
@router.patch("/{project_id}", response_model=Envelope[ProjectRead])
def update_project(project_id: UUID, payload: ProjectUpdate, db: Session = Depends(get_db)):
project = ProjectService.update_project(db, project_id, payload)
if not project:
@@ -54,7 +55,11 @@ def update_project(project_id: UUID, payload: ProjectUpdate, db: Session = Depen
return envelope(ProjectRead.model_validate(project).model_dump())
@router.delete("/{project_id}", status_code=status.HTTP_200_OK, response_model=dict)
@router.delete(
"/{project_id}",
status_code=status.HTTP_200_OK,
response_model=Envelope[ProjectDeleteResult],
)
def delete_project(project_id: UUID, db: Session = Depends(get_db)):
if not ProjectService.delete_project(db, project_id):
raise HTTPException(status_code=404, detail="Project not found")
+2 -2
View File
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.errors import AppError
from app.models import Dataset, Job
from app.schemas import QaProviderComparisonRequest
from app.schemas import Envelope, JobRead, QaProviderComparisonRequest
from app.services.qa_service import QaService
from app.services.job_service import JobService
from app.services.quality_service import QualityService
@@ -17,7 +17,7 @@ from app.utils.response import envelope
router = APIRouter(prefix="/qa", tags=["qa"])
@router.post("/detections-vs-reference")
@router.post("/detections-vs-reference", response_model=Envelope[JobRead])
def compare_candidate_with_reference(
payload: QaProviderComparisonRequest,
db: Session = Depends(get_db),
+15 -5
View File
@@ -6,7 +6,8 @@ from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.detection_review import DetectionReviewUpsert
from app.schemas import Envelope, QualityEvidenceResponse
from app.schemas.detection_review import DetectionReviewList, DetectionReviewRead, DetectionReviewUpsert
from app.schemas.qa import QualityCheckList
from app.services.detection_review_service import DetectionReviewService
from app.services.quality_evidence_service import QualityEvidenceService
@@ -16,7 +17,7 @@ from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}", tags=["quality-checks"])
@router.get("/quality-checks", response_model=dict)
@router.get("/quality-checks", response_model=Envelope[QualityCheckList])
def list_quality_checks(
project_id: UUID,
limit: int = Query(default=50, ge=1, le=200),
@@ -32,7 +33,10 @@ def list_quality_checks(
return envelope(QualityCheckList(items=items, total=total, limit=limit, offset=offset).model_dump())
@router.get("/quality-checks/{quality_check_id}/evidence/geojson", response_model=dict)
@router.get(
"/quality-checks/{quality_check_id}/evidence/geojson",
response_model=Envelope[QualityEvidenceResponse],
)
def get_quality_check_evidence_geojson(
project_id: UUID,
quality_check_id: UUID,
@@ -41,7 +45,10 @@ def get_quality_check_evidence_geojson(
return envelope(QualityEvidenceService.evidence_geojson(db, project_id=project_id, quality_check_id=quality_check_id))
@router.get("/quality-checks/{quality_check_id}/reviews", response_model=dict)
@router.get(
"/quality-checks/{quality_check_id}/reviews",
response_model=Envelope[DetectionReviewList],
)
def list_detection_reviews(
project_id: UUID,
quality_check_id: UUID,
@@ -66,7 +73,10 @@ def list_detection_reviews(
)
@router.post("/quality-checks/{quality_check_id}/reviews", response_model=dict)
@router.post(
"/quality-checks/{quality_check_id}/reviews",
response_model=Envelope[DetectionReviewRead],
)
def upsert_detection_review(
project_id: UUID,
quality_check_id: UUID,
+38 -11
View File
@@ -6,7 +6,19 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas import SegmentationQaRequest, SegmentationRunRequest
from app.schemas import (
AnalysisQaResponse,
Envelope,
GeoJsonFeatureCollection,
SegmentationListResponse,
SegmentationModelsResponse,
SegmentationQaRequest,
SegmentationRead,
SegmentationRunListResponse,
SegmentationRunRead,
SegmentationRunRequest,
SegmentationRunResponse,
)
from app.services.model_registry_service import ModelRegistryService
from app.services.segmentation_service import SegmentationService
from app.utils.response import envelope
@@ -14,12 +26,12 @@ from app.utils.response import envelope
router = APIRouter(prefix="/segmentation", tags=["segmentation"])
@router.get("/models", response_model=dict)
@router.get("/models", response_model=Envelope[SegmentationModelsResponse])
def list_segmentation_models() -> dict:
return envelope({"models": [model.model_dump() for model in ModelRegistryService.list_model_capabilities(task_type="segmentation")]})
@router.post("/run", response_model=dict)
@router.post("/run", response_model=Envelope[SegmentationRunResponse])
def run_segmentation(payload: SegmentationRunRequest, db: Session = Depends(get_db)) -> dict:
result = SegmentationService.run_segmentation(
db=db,
@@ -34,7 +46,7 @@ def run_segmentation(payload: SegmentationRunRequest, db: Session = Depends(get_
return envelope(result.model_dump())
@router.get("/runs", response_model=dict)
@router.get("/runs", response_model=Envelope[SegmentationRunListResponse])
def list_segmentation_runs(
project_id: UUID | None = None,
dataset_id: UUID | None = None,
@@ -43,12 +55,15 @@ def list_segmentation_runs(
return envelope(SegmentationService.list_runs(db, project_id=project_id, dataset_id=dataset_id).model_dump())
@router.get("/runs/{analysis_run_id}", response_model=dict)
@router.get("/runs/{analysis_run_id}", response_model=Envelope[SegmentationRunRead])
def get_segmentation_run(analysis_run_id: UUID, db: Session = Depends(get_db)) -> dict:
return envelope(SegmentationService.get_run(db, analysis_run_id).model_dump())
@router.get("/runs/{analysis_run_id}/segmentations", response_model=dict)
@router.get(
"/runs/{analysis_run_id}/segmentations",
response_model=Envelope[SegmentationListResponse],
)
def list_segmentation_run_outputs(
analysis_run_id: UUID,
dataset_id: UUID | None = None,
@@ -67,7 +82,10 @@ def list_segmentation_run_outputs(
)
@router.get("/datasets/{dataset_id}/segmentations", response_model=dict)
@router.get(
"/datasets/{dataset_id}/segmentations",
response_model=Envelope[SegmentationListResponse],
)
def list_dataset_segmentations(
dataset_id: UUID,
analysis_run_id: UUID | None = None,
@@ -86,12 +104,15 @@ def list_dataset_segmentations(
)
@router.get("/segmentations/{segmentation_id}", response_model=dict)
@router.get("/segmentations/{segmentation_id}", response_model=Envelope[SegmentationRead])
def get_segmentation(segmentation_id: UUID, db: Session = Depends(get_db)) -> dict:
return envelope(SegmentationService.get_segmentation(db, segmentation_id).model_dump())
@router.get("/runs/{analysis_run_id}/geojson", response_model=dict)
@router.get(
"/runs/{analysis_run_id}/geojson",
response_model=Envelope[GeoJsonFeatureCollection],
)
def get_segmentation_run_geojson(
analysis_run_id: UUID,
class_name: str | None = None,
@@ -108,7 +129,10 @@ def get_segmentation_run_geojson(
)
@router.get("/datasets/{dataset_id}/geojson", response_model=dict)
@router.get(
"/datasets/{dataset_id}/geojson",
response_model=Envelope[GeoJsonFeatureCollection],
)
def get_dataset_segmentation_geojson(
dataset_id: UUID,
analysis_run_id: UUID | None = None,
@@ -127,7 +151,10 @@ def get_dataset_segmentation_geojson(
)
@router.post("/runs/{analysis_run_id}/qa/reference", response_model=dict)
@router.post(
"/runs/{analysis_run_id}/qa/reference",
response_model=Envelope[AnalysisQaResponse],
)
def compare_segmentation_run_with_reference(
analysis_run_id: UUID,
payload: SegmentationQaRequest,
+8 -3
View File
@@ -6,7 +6,12 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.temporal import TemporalComparisonRequest
from app.schemas import Envelope, ItemList
from app.schemas.temporal import (
TemporalComparisonRequest,
TemporalComparisonResponse,
TemporalSeriesRead,
)
from app.services.temporal_analysis_service import TemporalAnalysisService
from app.utils.response import envelope
@@ -14,13 +19,13 @@ from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}/temporal", tags=["temporal"])
@router.get("/series", response_model=dict)
@router.get("/series", response_model=Envelope[ItemList[TemporalSeriesRead]])
def list_temporal_series(project_id: UUID, db: Session = Depends(get_db)):
series = TemporalAnalysisService.list_series(db, project_id)
return envelope({"items": [item.model_dump() for item in series], "total": len(series)})
@router.post("/compare", response_model=dict)
@router.post("/compare", response_model=Envelope[TemporalComparisonResponse])
def compare_temporal_snapshots(
project_id: UUID,
payload: TemporalComparisonRequest,