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
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:
@@ -0,0 +1,154 @@
|
||||
"""Provision official VHA cross-section profile points for the persisted Mol Area.
|
||||
|
||||
The operator uses only canonical GeoIntel API endpoints. The backend queries the
|
||||
official VHA service, clips points against the exact persisted Area geometry,
|
||||
stores the GeoJSON artifact and persists VectorFeature rows through DatasetService.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Provision official VHA cross-section profiles 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("--timeout", type=int, default=900)
|
||||
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-Bathymetry-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"])
|
||||
|
||||
job = unwrap(
|
||||
session.post(
|
||||
f"{base_url}/api/v1/projects/{project['id']}/datasets/bathymetry/profiles/acquire",
|
||||
json={
|
||||
"bbox": bbox,
|
||||
"area_id": area["id"],
|
||||
"force_refresh": args.force,
|
||||
},
|
||||
timeout=args.timeout,
|
||||
)
|
||||
)
|
||||
if job.get("status") != "success" or not job.get("output_dataset_id"):
|
||||
raise RuntimeError(f"Bathymetry profile acquisition failed: {job.get('error_message') or job}")
|
||||
result = job.get("result_json") or {}
|
||||
dataset_id = job["output_dataset_id"]
|
||||
selection = unwrap(
|
||||
session.post(
|
||||
f"{base_url}/api/v1/projects/{project['id']}/datasets/{dataset_id}/vector/select",
|
||||
json={"bbox": bbox, "area_id": area["id"], "limit": 1000},
|
||||
timeout=args.timeout,
|
||||
)
|
||||
)
|
||||
if selection.get("total_feature_count") != result.get("profile_count"):
|
||||
raise RuntimeError(
|
||||
"Persisted vector selection count does not match the exact acquired profile count "
|
||||
f"({selection.get('total_feature_count')} != {result.get('profile_count')})"
|
||||
)
|
||||
metrics = {
|
||||
item["metric_key"]: item
|
||||
for item in (selection.get("summary") or {}).get("metrics", [])
|
||||
if isinstance(item, dict) and item.get("metric_key")
|
||||
}
|
||||
if metrics.get("profile_count", {}).get("metric_value") != result.get("profile_count"):
|
||||
raise RuntimeError("Bathymetry profile summary does not expose the persisted profile count")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"project_id": project["id"],
|
||||
"area_id": area["id"],
|
||||
"area_name": area["name"],
|
||||
"bbox": bbox,
|
||||
"dataset_id": dataset_id,
|
||||
"reused": bool(result.get("reused")),
|
||||
"profile_count": result.get("profile_count"),
|
||||
"document_count": result.get("document_count"),
|
||||
"structured_depth_count": result.get("structured_depth_count"),
|
||||
"watercourse_count": result.get("watercourse_count"),
|
||||
"measurement_date_min": result.get("measurement_date_min"),
|
||||
"measurement_date_max": result.get("measurement_date_max"),
|
||||
"metrics": list(metrics.values()),
|
||||
"limitation_message": result.get("limitation_message"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user