"""Provision governed VMM flood-depth scenarios for the persisted Mol Area. All data flows through the canonical API, Job abstraction and DatasetService. The operator never writes raster files or database rows directly. """ from __future__ import annotations import argparse import json import os from typing import Any, Iterable import requests DEFAULT_API_URL = "http://127.0.0.1:8000" DEFAULT_PROJECT_NAME = "Kempen Regional Workbench" DEFAULT_AREA_FRAGMENT = "Gemeente Mol" PRODUCTS = tuple( f"{mechanism}_{climate}_t{period}" for mechanism in ("pluviaal", "fluviaal") for climate in ("current", "future_2050") for period in (10, 100, 1000) ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Provision official VMM flood-depth scenarios for Mol.") parser.add_argument("--base-url", default=os.environ.get("GEOINTEL_INTERNAL_API_URL", DEFAULT_API_URL)) parser.add_argument("--project-name", default=DEFAULT_PROJECT_NAME) parser.add_argument("--area-name", default=DEFAULT_AREA_FRAGMENT) parser.add_argument("--products", default=",".join(PRODUCTS), help="Comma-separated governed product keys.") parser.add_argument("--resolution-m", type=float, default=5.0) parser.add_argument("--timeout", type=int, default=1800) parser.add_argument("--force", action="store_true") return parser.parse_args() def unwrap(response: requests.Response) -> Any: response.raise_for_status() payload = response.json() if not isinstance(payload, dict) or "data" not in payload: raise RuntimeError(f"Non-canonical API response from {response.url}") return payload["data"] def coordinates(geometry: dict[str, Any]) -> Iterable[tuple[float, float]]: def walk(value: Any): if isinstance(value, list) and len(value) >= 2 and all(isinstance(item, (int, float)) for item in value[:2]): yield float(value[0]), float(value[1]) return if isinstance(value, list): for child in value: yield from walk(child) yield from walk(geometry.get("coordinates", [])) def geometry_bbox(geometry: dict[str, Any]) -> dict[str, float | str]: points = list(coordinates(geometry)) if not points: raise RuntimeError("Persisted Area geometry contains no coordinates") xs = [point[0] for point in points] ys = [point[1] for point in points] return {"min_x": min(xs), "min_y": min(ys), "max_x": max(xs), "max_y": max(ys), "crs": "EPSG:4326"} def main() -> int: args = parse_args() base_url = args.base_url.rstrip("/") session = requests.Session() session.headers.update({"User-Agent": "GeoIntel-VMM-Flood-Hazard-Operator/1.0"}) projects = unwrap(session.get(f"{base_url}/api/v1/projects", params={"limit": 200, "offset": 0}, timeout=60))["items"] project = next((item for item in projects if item["name"] == args.project_name), None) if project is None: raise RuntimeError(f"Project {args.project_name!r} was not found") areas = unwrap( session.get( f"{base_url}/api/v1/projects/{project['id']}/areas", params={"limit": 200, "offset": 0}, timeout=60, ) )["items"] fragment = args.area_name.casefold() area = next((item for item in areas if fragment in item["name"].casefold()), None) if area is None: raise RuntimeError(f"Area containing {args.area_name!r} was not found") bbox = geometry_bbox(area["geometry"]) registry = unwrap(session.get(f"{base_url}/api/v1/projects/{project['id']}/datasets/flood-hazard/products", timeout=60))["items"] registry_keys = {item["key"] for item in registry} if registry_keys != set(PRODUCTS): raise RuntimeError("Backend flood-hazard registry does not expose the governed twelve-product set") requested_products = [item.strip() for item in args.products.split(",") if item.strip()] invalid = sorted(set(requested_products) - registry_keys) if invalid: raise RuntimeError(f"Unsupported flood-hazard product keys: {', '.join(invalid)}") results = [] for product_key in requested_products: job = unwrap( session.post( f"{base_url}/api/v1/projects/{project['id']}/datasets/flood-hazard/acquire", json={ "bbox": bbox, "area_id": area["id"], "product_key": product_key, "resolution_m": args.resolution_m, "force_refresh": args.force, }, timeout=args.timeout, ) ) if job.get("status") != "success" or not job.get("output_dataset_id"): raise RuntimeError(f"Flood-hazard acquisition failed for {product_key}: {job.get('error_message') or job}") analysis = unwrap( session.post( f"{base_url}/api/v1/projects/{project['id']}/datasets/{job['output_dataset_id']}/raster/flood-hazard/select", json={"bbox": bbox, "area_id": area["id"]}, timeout=args.timeout, ) ) unsupported = set(analysis.get("unsupported_metrics", [])) if {"bathymetry_depth_m", "permanent_water_volume_m3", "concurrent_flood_volume_m3"} - unsupported: raise RuntimeError("Flood-hazard contract must keep bathymetry and definitive water volumes unavailable") results.append( { "product_key": product_key, "dataset_id": job["output_dataset_id"], "reused": bool((job.get("result_json") or {}).get("reused")), "resolution_m": analysis["resolution_m"], "selected_cell_count": analysis["selected_cell_count"], "inundated_cell_count": analysis["inundated_cell_count"], "metrics": analysis["summary"]["metrics"], } ) print( json.dumps( { "status": "ok", "project_id": project["id"], "area_id": area["id"], "area_name": area["name"], "bbox": bbox, "products": results, }, ensure_ascii=False, indent=2, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())