feat: add temporal Mol explorer
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 15:19:42 +02:00
parent 0ec1ab4970
commit c0943fe7d4
39 changed files with 3065 additions and 163 deletions
+1 -1
View File
@@ -1 +1 @@
__all__ = ["analysis", "areas", "datasets", "health", "projects", "exports", "jobs", "external", "qa"]
__all__ = ["analysis", "areas", "datasets", "health", "projects", "exports", "jobs", "external", "qa", "temporal"]
+48 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
from datetime import datetime
from typing import Any
from uuid import UUID
from uuid import UUID as _UUID
@@ -30,7 +31,7 @@ from app.schemas import (
VectorSelectionResponse,
)
from app.schemas.job import JobCreate
from app.schemas.dataset import DatasetCreateResponse
from app.schemas.dataset import DatasetCreateResponse, DatasetTemporalUpdate
from app.schemas.operations import VectorOperationResult
from app.services.job_service import JobService
from app.services.raster_operations_service import RasterOperationsService
@@ -87,6 +88,12 @@ async def upload_dataset(
reference_layer_name: str | None = Form(None),
source_metadata_json: str | None = Form(None),
provenance_metadata_json: str | None = Form(None),
temporal_series_key: str | None = Form(None),
observed_at: datetime | None = Form(None),
valid_from: datetime | None = Form(None),
valid_to: datetime | None = Form(None),
temporal_granularity: str | None = Form(None),
source_version: str | None = Form(None),
db: Session = Depends(get_db),
):
if area_id is not None:
@@ -108,6 +115,12 @@ async def upload_dataset(
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,
temporal_series_key=temporal_series_key,
observed_at=observed_at,
valid_from=valid_from,
valid_to=valid_to,
temporal_granularity=temporal_granularity,
source_version=source_version,
)
return envelope(created.model_dump())
@@ -135,6 +148,33 @@ def get_dataset(
return envelope(DatasetCreateResponse.model_validate(dataset).model_dump())
@router.patch("/datasets/{dataset_id}/temporal", response_model=dict)
def update_dataset_temporal_metadata(
project_id: UUID,
dataset_id: UUID,
payload: DatasetTemporalUpdate,
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")
updated = DatasetService.update_temporal_metadata(db, dataset_id, payload)
return envelope(updated.model_dump())
@router.get("/datasets/{dataset_id}/versions", response_model=dict)
def list_dataset_versions(
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")
versions = DatasetService.list_versions(db, dataset_id)
return envelope({"items": [item.model_dump() for item in versions], "total": len(versions)})
@router.post("/datasets/{dataset_id}/metadata/refresh", response_model=dict)
def refresh_dataset_metadata(
project_id: UUID,
@@ -203,6 +243,13 @@ def select_vector_features(
bbox=payload.bbox.model_dump(),
limit=payload.limit,
)
if isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"):
result["summary"] = VectorFeatureService.summarize_features_by_bbox(
db,
dataset=dataset,
bbox=payload.bbox.model_dump(),
total_feature_count=result.get("total_feature_count"),
)
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
+29
View File
@@ -0,0 +1,29 @@
from __future__ import annotations
from uuid import UUID
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.services.temporal_analysis_service import TemporalAnalysisService
from app.utils.response import envelope
router = APIRouter(prefix="/projects/{project_id}/temporal", tags=["temporal"])
@router.get("/series", response_model=dict)
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)
def compare_temporal_snapshots(
project_id: UUID,
payload: TemporalComparisonRequest,
db: Session = Depends(get_db),
):
return envelope(TemporalAnalysisService.compare(db, project_id=project_id, payload=payload).model_dump())