fix(platform): govern geospatial analysis and raster handoffs

This commit is contained in:
Jens
2026-08-30 06:00:15 +02:00
parent 96f90373dc
commit 80a2d1654d
63 changed files with 2335 additions and 312 deletions
@@ -1,8 +1,8 @@
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
from typing import Any
@@ -16,6 +16,7 @@ from app.models import Area, Dataset, DatasetVersion
from app.services.derived_dataset_governance_service import DerivedDatasetGovernanceService
from app.services.raster_service import extract_raster_metadata
from app.services.storage_service import StorageService
from app.services.tile_manifest_service import TileManifestService, canonical_manifest_json
def _import_rasterio():
@@ -70,6 +71,39 @@ class RasterOperationsService:
raise AppError(code="DATASET_FILE_MISSING", message="Stored raster file missing", status_code=404)
return dataset
@staticmethod
def _validate_storage_checksum(dataset: Dataset) -> None:
"""Refuse tiling pixels that no longer match the governed Dataset row."""
expected_checksum = str(dataset.checksum_sha256 or "").strip().lower()
governed_artifact = bool(getattr(dataset, "data_contract_key", None))
valid_checksum = len(expected_checksum) == 64 and all(character in "0123456789abcdef" for character in expected_checksum)
if not valid_checksum:
if expected_checksum or governed_artifact:
raise AppError(
code="DATASET_STORAGE_CHECKSUM_UNVERIFIABLE",
message="Governed raster storage requires a valid SHA-256 checksum before tiling.",
details={"checksum_sha256": dataset.checksum_sha256},
status_code=409,
)
return
digest = sha256()
with Path(str(dataset.storage_path)).open("rb") as stream:
for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
digest.update(chunk)
actual_checksum = digest.hexdigest()
if actual_checksum != expected_checksum:
raise AppError(
code="DATASET_STORAGE_CHECKSUM_MISMATCH",
message="Raster dataset storage no longer matches its validated checksum.",
details={
"expected_checksum_sha256": expected_checksum,
"actual_checksum_sha256": actual_checksum,
},
status_code=409,
)
@staticmethod
def _raster_dependencies() -> tuple[Any, Any]:
try:
@@ -970,14 +1004,17 @@ class RasterOperationsService:
tile_size: int = 512,
overlap: int = 64,
output_name: str | None = None,
max_tiles: int | None = None,
) -> dict[str, Any]:
RasterOperationsService._validate_tile_request(tile_size=tile_size, overlap=overlap)
if max_tiles is not None and max_tiles <= 0:
raise AppError(code="INVALID_PARAMETERS", message="max_tiles must be positive", status_code=400)
dataset = RasterOperationsService._load_dataset(db, dataset_id)
RasterOperationsService._validate_storage_checksum(dataset)
rasterio, _ = RasterOperationsService._raster_dependencies()
tile_set_id = str(uuid.uuid4())
tile_root = StorageService.raster_tiles_root(str(dataset.project_id), str(dataset.id), tile_set_id)
tile_root.mkdir(parents=True, exist_ok=True)
manifest_tiles: list[dict[str, Any]] = []
tile_paths: list[str] = []
@@ -997,9 +1034,23 @@ class RasterOperationsService:
source_width = int(source.width)
source_height = int(source.height)
step = max(1, tile_size - overlap)
x_offsets = RasterOperationsService._tile_offsets(source_width, tile_size, step)
y_offsets = RasterOperationsService._tile_offsets(source_height, tile_size, step)
expected_tile_count = len(x_offsets) * len(y_offsets)
if max_tiles is not None and expected_tile_count > max_tiles:
raise AppError(
code="RASTER_TILE_LIMIT_EXCEEDED",
message="Raster tile generation exceeds the guest analysis limit.",
details={
"expected_tile_count": expected_tile_count,
"max_tiles": max_tiles,
},
status_code=422,
)
tile_root.mkdir(parents=True, exist_ok=True)
tile_index = 0
for yoff in range(0, source_height, step):
for xoff in range(0, source_width, step):
for yoff in y_offsets:
for xoff in x_offsets:
tile_width = min(tile_size, source_width - xoff)
tile_height = min(tile_size, source_height - yoff)
if tile_width <= 0 or tile_height <= 0:
@@ -1022,6 +1073,7 @@ class RasterOperationsService:
tile_dest.write(tile_data)
tile_paths.append(str(tile_path))
tile_integrity = TileManifestService.tile_integrity(tile_path)
manifest_tiles.append(
{
"path": str(tile_path),
@@ -1030,6 +1082,7 @@ class RasterOperationsService:
"transform": [float(item) for item in transform.to_gdal()],
"crs": source_crs,
"index": tile_index,
**tile_integrity,
},
)
tile_index += 1
@@ -1044,9 +1097,8 @@ class RasterOperationsService:
bounds = source_metadata.get("bounds", [0.0, 0.0, 0.0, 0.0])
manifest_crs = source_crs or source_metadata.get("crs") or dataset.crs
manifest_payload = {
**TileManifestService.dataset_binding(db, dataset),
"tile_set_id": tile_set_id,
"source_dataset_id": str(dataset.id),
"source_raster_id": str(dataset.id),
"crs": manifest_crs,
"source_crs": manifest_crs,
"dataset_crs": dataset.crs,
@@ -1066,7 +1118,7 @@ class RasterOperationsService:
"tile_server": None,
}
manifest_path = tile_root / "manifest.json"
manifest_path.write_text(json.dumps(manifest_payload), encoding="utf-8")
manifest_path.write_text(canonical_manifest_json(manifest_payload), encoding="utf-8")
return {
"dataset_id": str(dataset.id),
@@ -1079,3 +1131,17 @@ class RasterOperationsService:
"count": len(manifest_tiles),
"manifest": manifest_payload,
}
@staticmethod
def _tile_offsets(dimension: int, tile_size: int, step: int) -> list[int]:
"""Return full-tile starts plus one unique edge-aligned final start."""
if dimension <= 0 or tile_size <= 0 or step <= 0:
raise AppError(code="INVALID_PARAMETERS", message="Raster tile dimensions must be positive", status_code=400)
if dimension <= tile_size:
return [0]
final_start = dimension - tile_size
offsets = list(range(0, final_start + 1, step))
if offsets[-1] != final_start:
offsets.append(final_start)
return offsets