feat(provenance): govern source snapshots and data inputs
This commit is contained in:
@@ -23,6 +23,7 @@ from app.schemas.flood_hazard import FloodHazardPartitionSelectionRequest, Flood
|
||||
from app.schemas.temporal import TemporalComparisonRequest
|
||||
from app.schemas.thematic_raster import ThematicRasterSelectionRequest
|
||||
from app.services.dataset_service import DatasetService
|
||||
from app.services.dataset_consumption_gate_service import DatasetConsumptionGate
|
||||
from app.services.detection_service import DetectionService
|
||||
from app.services.flood_hazard_analysis_service import FloodHazardAnalysisService
|
||||
from app.services.segmentation_service import SegmentationService
|
||||
@@ -34,12 +35,39 @@ from app.services.vector_feature_service import VectorFeatureService
|
||||
|
||||
|
||||
class ExportService:
|
||||
@staticmethod
|
||||
def _assert_run_source_dataset_exportable(db: Session, run: AnalysisRun) -> Dataset:
|
||||
"""Block an output export when its persisted source dataset is unsafe."""
|
||||
|
||||
if not run.dataset_id:
|
||||
raise AppError(
|
||||
code="DATASET_PROVENANCE_INCOMPLETE",
|
||||
message="Analysis output cannot be exported without a persisted source dataset.",
|
||||
status_code=409,
|
||||
)
|
||||
dataset = db.get(Dataset, run.dataset_id)
|
||||
if not dataset or dataset.project_id != run.project_id:
|
||||
raise AppError(code="DATASET_NOT_FOUND", message="Analysis source dataset not found", status_code=404)
|
||||
DatasetConsumptionGate.assert_eligible(dataset, purpose="export")
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def export_map_result(
|
||||
db: Session,
|
||||
payload: MapResultExportRequest,
|
||||
) -> ExportCreateResponse:
|
||||
if payload.mode == "evolution":
|
||||
earlier_dataset = db.get(Dataset, payload.earlier_dataset_id)
|
||||
later_dataset = db.get(Dataset, payload.later_dataset_id)
|
||||
if (
|
||||
not earlier_dataset
|
||||
or not later_dataset
|
||||
or earlier_dataset.project_id != payload.project_id
|
||||
or later_dataset.project_id != payload.project_id
|
||||
):
|
||||
raise AppError(code="DATASET_NOT_FOUND", message="Temporal export dataset not found", status_code=404)
|
||||
DatasetConsumptionGate.assert_eligible(earlier_dataset, purpose="export")
|
||||
DatasetConsumptionGate.assert_eligible(later_dataset, purpose="export")
|
||||
comparison = TemporalAnalysisService.compare(
|
||||
db,
|
||||
project_id=payload.project_id,
|
||||
@@ -86,6 +114,7 @@ class ExportService:
|
||||
dataset = db.get(Dataset, payload.dataset_id)
|
||||
if not dataset or dataset.project_id != payload.project_id:
|
||||
raise AppError(code="DATASET_NOT_FOUND", message="Dataset not found", status_code=404)
|
||||
DatasetConsumptionGate.assert_eligible(dataset, purpose="export")
|
||||
if dataset.dataset_type in DatasetService.VECTOR_TYPES:
|
||||
if payload.partitioned:
|
||||
return ExportService.export_partitioned_vector_selection_geojson(
|
||||
@@ -219,6 +248,7 @@ class ExportService:
|
||||
limit: int = 1000,
|
||||
name: str | None = None,
|
||||
) -> ExportCreateResponse:
|
||||
DatasetConsumptionGate.assert_eligible(dataset, purpose="export")
|
||||
if dataset.source_name != "vmm_vha_bathymetry_profiles" or partition_scope_key != "flanders":
|
||||
raise AppError(
|
||||
code="PARTITIONED_VECTOR_EXPORT_UNSUPPORTED",
|
||||
@@ -308,6 +338,7 @@ class ExportService:
|
||||
details={"dataset_type": dataset.dataset_type},
|
||||
status_code=400,
|
||||
)
|
||||
DatasetConsumptionGate.assert_eligible(dataset, purpose="export")
|
||||
|
||||
selection_kwargs: dict[str, Any] = {
|
||||
"dataset_id": dataset_id,
|
||||
@@ -374,6 +405,7 @@ class ExportService:
|
||||
details={"dataset_type": dataset.dataset_type},
|
||||
status_code=400,
|
||||
)
|
||||
DatasetConsumptionGate.assert_eligible(dataset, purpose="export")
|
||||
|
||||
feature_collection = DatasetService.get_dataset_geojson(db, dataset_id)
|
||||
filename = ExportService._filename(name, f"{dataset.id}.geojson", ".geojson")
|
||||
@@ -401,6 +433,7 @@ class ExportService:
|
||||
run = db.get(AnalysisRun, analysis_run_id)
|
||||
if not run or run.analysis_type != "detection":
|
||||
raise AppError(code="DETECTION_RUN_NOT_FOUND", message="Detection run not found", status_code=404)
|
||||
ExportService._assert_run_source_dataset_exportable(db, run)
|
||||
|
||||
feature_collection = DetectionService.detections_to_geojson(db, analysis_run_id=analysis_run_id)
|
||||
filename = ExportService._filename(name, f"{run.id}-detections.geojson", ".geojson")
|
||||
@@ -428,6 +461,7 @@ class ExportService:
|
||||
run = db.get(AnalysisRun, analysis_run_id)
|
||||
if not run or run.analysis_type != "segmentation":
|
||||
raise AppError(code="SEGMENTATION_RUN_NOT_FOUND", message="Segmentation run not found", status_code=404)
|
||||
ExportService._assert_run_source_dataset_exportable(db, run)
|
||||
|
||||
feature_collection = SegmentationService.segmentations_to_geojson(db, analysis_run_id=analysis_run_id)
|
||||
filename = ExportService._filename(name, f"{run.id}-segmentations.geojson", ".geojson")
|
||||
|
||||
Reference in New Issue
Block a user