Files
geointel/backend/app/api/routes/datasets.py
T
Codex 6ea3586a3e
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Initial GeoIntel V1 foundation
2026-06-16 23:36:32 +02:00

490 lines
16 KiB
Python

from __future__ import annotations
import json
from typing import Any
from uuid import UUID
from uuid import UUID as _UUID
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query
from fastapi import UploadFile
from sqlalchemy.orm import Session
from app.models import Area
from app.core.errors import AppError
from app.db.session import get_db
from app.schemas import (
RasterClipRequest,
RasterStatsResponse,
RasterReprojectRequest,
RasterTileRequest,
RasterNdviRequest,
RasterNdwiRequest,
RasterNdbiRequest,
VectorBBoxResponse,
VectorBufferRequest,
VectorClipRequest,
VectorIntersectRequest,
)
from app.schemas.job import JobCreate
from app.schemas.dataset import DatasetCreateResponse
from app.schemas.operations import VectorOperationResult
from app.services.job_service import JobService
from app.services.raster_operations_service import RasterOperationsService
from app.services.vector_operations_service import VectorOperationsService
from app.services.dataset_service import DatasetService
from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}", tags=["datasets"])
def _parse_metadata_json(raw: str | None, field_name: str) -> dict | None:
if raw is None:
return None
raw = raw.strip()
if not raw:
return None
try:
value = json.loads(raw)
except json.JSONDecodeError as exc:
raise AppError(code="INVALID_JSON", message=f"Invalid JSON for {field_name}", details={"field": field_name}, status_code=400) from exc
if not isinstance(value, dict):
raise AppError(code="INVALID_JSON", message=f"{field_name} must be a JSON object", details={"field": field_name}, status_code=400)
return value
def _run_job_sync(
db: Session,
project_id: UUID,
input_dataset_id: UUID,
job_type: str,
parameters: dict[str, Any],
operation,
) -> dict[str, Any]:
return JobService.run_sync_job(
db=db,
project_id=project_id,
job_type=job_type,
parameters=parameters,
operation=operation,
input_dataset_id=input_dataset_id,
)
@router.post("/datasets/upload", status_code=201, response_model=dict)
async def upload_dataset(
project_id: UUID,
file: UploadFile = File(...),
dataset_type: str = Form(...),
source: str = Form("user_upload"),
area_id: UUID | None = Form(None),
dataset_role: str = Form("source"),
source_name: str | None = Form(None),
reference_layer_name: str | None = Form(None),
source_metadata_json: str | None = Form(None),
provenance_metadata_json: str | None = Form(None),
db: Session = Depends(get_db),
):
if area_id is not None:
area = db.get(Area, area_id)
if not area:
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
if area.project_id != project_id:
raise AppError(code="INVALID_DATASET_SCOPE", message="Area does not belong to this project", status_code=400)
created = await DatasetService.upload_dataset(
db,
project_id=project_id,
file=file,
dataset_type=dataset_type,
source=source,
dataset_role=dataset_role,
source_name=source_name,
reference_layer_name=reference_layer_name,
source_metadata=_parse_metadata_json(source_metadata_json, "source_metadata_json"),
provenance_metadata=_parse_metadata_json(provenance_metadata_json, "provenance_metadata_json"),
area_id=area_id,
)
return envelope(created.model_dump())
@router.get("/datasets", response_model=dict)
def list_datasets(
project_id: UUID,
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
db: Session = Depends(get_db),
):
datasets, total = DatasetService.list_datasets(db, project_id, limit=limit, offset=offset)
return envelope({"items": [item.model_dump() for item in datasets], "total": total, "limit": limit, "offset": offset})
@router.get("/datasets/{dataset_id}", response_model=dict)
def get_dataset(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(DatasetCreateResponse.model_validate(dataset).model_dump())
@router.post("/datasets/{dataset_id}/metadata/refresh", response_model=dict)
def refresh_dataset_metadata(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
refreshed = DatasetService.refresh_metadata(db, dataset_id)
return envelope(refreshed.model_dump())
@router.get("/datasets/{dataset_id}/vector/inspect", response_model=dict)
def inspect_vector_dataset(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(VectorOperationsService.inspect(db, dataset_id).model_dump())
@router.get("/datasets/{dataset_id}/vector/bbox", response_model=dict)
def vector_bbox(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
payload = VectorOperationsService.bbox(db, dataset_id)
return envelope(VectorBBoxResponse(**payload).model_dump())
@router.get("/datasets/{dataset_id}/vector/stats", response_model=dict)
def vector_stats(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(VectorOperationsService.stats(db, dataset_id))
@router.post("/datasets/{dataset_id}/vector/clip", status_code=201, response_model=dict)
def clip_vector_dataset(
project_id: UUID,
dataset_id: UUID,
payload: VectorClipRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="vector.clip",
parameters=payload.model_dump(),
operation=lambda: VectorOperationsService.clip_by_area(
db,
dataset_id=dataset_id,
area_id=payload.area_id,
output_name=payload.output_name,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/vector/buffer", status_code=201, response_model=dict)
def buffer_vector_dataset(
project_id: UUID,
dataset_id: UUID,
payload: VectorBufferRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="vector.buffer",
parameters=payload.model_dump(),
operation=lambda: VectorOperationsService.buffer(
db,
dataset_id=dataset_id,
distance_m=payload.distance_m,
dissolve=payload.dissolve,
output_name=payload.output_name,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/vector/intersect", status_code=201, response_model=dict)
def intersect_vector_dataset(
project_id: UUID,
dataset_id: UUID,
payload: VectorIntersectRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="vector.intersect",
parameters=payload.model_dump(),
operation=lambda: VectorOperationsService.intersect(
db,
source_dataset_id=dataset_id,
target_dataset_id=UUID(payload.other_dataset_id),
output_name=payload.output_name,
),
)
return envelope(job)
@router.get("/datasets/{dataset_id}/vector/summary", response_model=dict)
def vector_dataset_summary(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(DatasetService.vector_summary(db, dataset_id))
@router.get("/datasets/{dataset_id}/raster/inspect", response_model=dict)
def raster_dataset_inspect(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
payload = RasterOperationsService.inspect(db, dataset_id)
return envelope(payload)
@router.get("/datasets/{dataset_id}/raster/preview", response_model=dict)
def raster_preview_readiness(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(RasterOperationsService.preview(db, dataset_id))
@router.get("/datasets/{dataset_id}/raster/stats", response_model=dict)
def raster_stats(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
payload = RasterOperationsService.stats(db, dataset_id)
return envelope(RasterStatsResponse(**payload).model_dump())
@router.post("/datasets/{dataset_id}/raster/reproject", status_code=201, response_model=dict)
def raster_reproject_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterReprojectRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.reproject",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.reproject(
db,
dataset_id,
target_crs=payload.target_crs,
output_name=payload.output_name,
resampling=payload.resampling,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/clip", status_code=201, response_model=dict)
def raster_clip_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterClipRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.clip",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.clip(db, dataset_id, UUID(payload.area_id), payload.output_name),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/tile", status_code=201, response_model=dict)
def raster_tile_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterTileRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.tile",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.tile(
db,
dataset_id,
tile_size=payload.tile_size,
overlap=payload.overlap,
output_name=payload.output_name,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndvi", status_code=201, response_model=dict)
def raster_ndvi_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterNdviRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.ndvi",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.ndvi(
db,
dataset_id=dataset_id,
nir_band=payload.nir_band,
red_band=payload.red_band,
output_name=payload.output_name,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndwi", status_code=201, response_model=dict)
def raster_ndwi_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterNdwiRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.ndwi",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.ndwi(
db,
dataset_id=dataset_id,
green_band=payload.green_band,
nir_band=payload.nir_band,
output_name=payload.output_name,
),
)
return envelope(job)
@router.post("/datasets/{dataset_id}/raster/indices/ndbi", status_code=201, response_model=dict)
def raster_ndbi_dataset(
project_id: UUID,
dataset_id: UUID,
payload: RasterNdbiRequest,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
job = _run_job_sync(
db=db,
project_id=project_id,
input_dataset_id=dataset_id,
job_type="raster.ndbi",
parameters=payload.model_dump(),
operation=lambda: RasterOperationsService.ndbi(
db,
dataset_id=dataset_id,
swir_band=payload.swir_band,
nir_band=payload.nir_band,
output_name=payload.output_name,
),
)
return envelope(job)
@router.get("/datasets/{dataset_id}/raster/metadata", response_model=dict)
def raster_dataset_metadata(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(RasterOperationsService.metadata(db, dataset_id))
@router.get("/datasets/{dataset_id}/content", response_model=dict)
def dataset_content(
project_id: UUID,
dataset_id: UUID,
db: Session = Depends(get_db),
):
dataset = DatasetService.get_dataset(db, dataset_id)
if dataset.project_id != project_id:
raise HTTPException(status_code=404, detail="Dataset not found")
return envelope(DatasetService.get_dataset_geojson(db, dataset_id))