Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s

This commit is contained in:
Jens
2026-08-31 21:56:53 +02:00
commit faeb58ef6d
1386 changed files with 263203 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
"""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())