Files
geointel/scripts/orthophoto_release_preflight.py
T
Jens faeb58ef6d
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
Initial public release
2026-08-31 21:56:53 +02:00

695 lines
30 KiB
Python

#!/usr/bin/env python3
"""Read-only release preflight for the governed current orthophoto product.
The preflight does not request or stage raster pixels. It binds the canonical
GeoIntel catalog result to the current product registry and the exact official
WMS capabilities, then probes the queryable flight-day layer on a bounded,
deterministic grid. Local rolling markers remain non-comparable and blocked.
"""
from __future__ import annotations
import argparse
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from hashlib import sha256
import json
import math
import os
import re
import sys
from typing import Any, Callable
from urllib.error import HTTPError, URLError
from urllib.parse import parse_qs, urlencode, urlparse
from urllib.request import Request, urlopen
from xml.etree import ElementTree
from pyproj import Transformer
from geographic_scopes import GEOGRAPHIC_SCOPES
DEFAULT_API_URL = "http://127.0.0.1:8000/api/v1"
DEFAULT_SCOPE = "kempen-transport-region"
SOURCE_NAME = "digitaal_vlaanderen_orthophoto"
PRODUCT_KEY = "most_recent"
WMS_HOST = "geo.api.vlaanderen.be"
WMS_PATH = "/OMWRGBMRVL/wms"
WCS_PATH = "/OMWRGBMRVL/wcs"
CATALOG_URL = (
"https://www.vlaanderen.be/datavindplaats/catalogus/"
"orthofotomozaiek-middenschalig-winteropnamen-kleur-meest-recent-vlaanderen"
)
METADATA_IDENTIFIER = "f5304d6d-0dd4-43fd-a726-427af31e8d61"
EDITION_PATTERN = re.compile(r"^(20[0-9]{2})\.([0-9]{2})$")
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
FLIGHT_YEAR_PATTERN = re.compile(r"(?:^|[^0-9])(20[0-9]{2})(?:$|[^0-9])")
MAX_CAPABILITIES_BYTES = 2 * 1024 * 1024
MAX_COVERAGE_DESCRIPTION_BYTES = 512 * 1024
MAX_FEATURE_INFO_BYTES = 128 * 1024
MAX_SAMPLE_SPACING_M = 128.0
MAX_SAMPLE_COUNT = 64
MIN_SIDE_M = 128.0
MAX_SIDE_M = 1024.0
FEATURE_INFO_SIZE = 1000
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Read-only current-orthophoto release preflight. No raster pixels are fetched, staged or imported."
)
)
parser.add_argument("--project-id", required=True, help="GeoIntel project UUID for the governed scope")
parser.add_argument("--scope", choices=(DEFAULT_SCOPE,), default=DEFAULT_SCOPE)
parser.add_argument("--api-url", default=os.environ.get("GEOINTEL_API_URL", DEFAULT_API_URL))
parser.add_argument(
"--bbox",
nargs=4,
type=float,
metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"),
required=True,
help="Exact EPSG:4326 selection; each projected side must be between 128 and 1024 metres",
)
parser.add_argument("--refresh-catalog", action="store_true", help="Bypass the short catalog cache")
parser.add_argument("--api-timeout", type=int, default=180)
parser.add_argument("--wms-timeout", type=int, default=30)
return parser.parse_args()
def api_data(api_url: str, path: str, timeout: int) -> dict[str, Any]:
endpoint = f"{api_url.rstrip('/')}/{path.lstrip('/')}"
request = Request(
endpoint,
headers={"Accept": "application/json", "User-Agent": "GeoIntel-orthophoto-preflight/1.0"},
)
try:
with urlopen(request, timeout=timeout) as response:
payload = json.load(response)
except HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"GeoIntel API returned HTTP {exc.code}: {body[-1000:]}") from exc
except URLError as exc:
raise RuntimeError(f"GeoIntel API is unreachable: {exc.reason}") from exc
if not isinstance(payload, dict) or not isinstance(payload.get("data"), dict):
raise RuntimeError("GeoIntel API response is not a canonical data envelope")
return payload["data"]
def validate_project_scope(args: argparse.Namespace, loader: Callable[[str, str, int], dict[str, Any]]) -> None:
project = loader(args.api_url, f"projects/{args.project_id}", args.api_timeout)
expected_name = GEOGRAPHIC_SCOPES[args.scope].project_name
if project.get("name") != expected_name:
raise RuntimeError(
f"Project {args.project_id} is '{project.get('name')}', but scope {args.scope} requires '{expected_name}'"
)
def _validate_service_url(url: str, *, path: str, request_name: str | None = None) -> str:
parsed = urlparse(url)
if (
parsed.scheme.lower() != "https"
or parsed.hostname != WMS_HOST
or parsed.port not in (None, 443)
or parsed.username
or parsed.password
or parsed.fragment
or parsed.path.rstrip("/").lower() != path.lower()
):
raise RuntimeError("Orthophoto service URL is outside the official allowlist")
if request_name is not None:
requests = parse_qs(parsed.query).get("REQUEST") or parse_qs(parsed.query).get("request") or []
if len(requests) != 1 or requests[0].lower() != request_name.lower():
raise RuntimeError(f"Orthophoto service URL is not an exact {request_name} request")
return f"https://{WMS_HOST}{path}"
def _validate_wms_url(url: str, *, request_name: str | None = None) -> str:
return _validate_service_url(url, path=WMS_PATH, request_name=request_name)
def _validate_wcs_url(url: str, *, request_name: str | None = None) -> str:
return _validate_service_url(url, path=WCS_PATH, request_name=request_name)
def _bounded_get(
url: str,
*,
timeout: int,
max_bytes: int,
accept: str,
opener: Callable[..., Any] | None = None,
validator: Callable[[str], str] = _validate_wms_url,
) -> tuple[bytes, str, str]:
request = Request(url, headers={"Accept": accept, "User-Agent": "GeoIntel-orthophoto-preflight/1.0"})
fetch = opener or urlopen
try:
response = fetch(request, timeout=timeout)
with response:
final_url = response.geturl()
validator(final_url)
content_type = str(response.headers.get("Content-Type") or "").split(";", 1)[0].strip().lower()
declared = response.headers.get("Content-Length")
try:
if declared and int(declared) > max_bytes:
raise RuntimeError("Official WMS response exceeds the configured preflight limit")
except ValueError as exc:
raise RuntimeError("Official WMS response has an invalid Content-Length") from exc
body = response.read(max_bytes + 1)
except HTTPError as exc:
raise RuntimeError(f"Official orthophoto WMS returned HTTP {exc.code}") from exc
except URLError as exc:
raise RuntimeError(f"Official orthophoto WMS is unreachable: {exc.reason}") from exc
if len(body) > max_bytes:
raise RuntimeError("Official WMS response exceeds the configured preflight limit")
return body, content_type, final_url
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _children(element: ElementTree.Element, name: str) -> list[ElementTree.Element]:
return [child for child in element if _local_name(child.tag) == name]
def _child_text(element: ElementTree.Element, name: str) -> str | None:
for child in element:
if _local_name(child.tag) == name and child.text:
return child.text.strip()
return None
def parse_capabilities(body: bytes, expected_sha256: str) -> dict[str, Any]:
if not SHA256_PATTERN.fullmatch(expected_sha256):
raise RuntimeError("Canonical catalog response has no valid capabilities SHA-256")
actual_sha256 = sha256(body).hexdigest()
if actual_sha256 != expected_sha256:
raise RuntimeError("Official WMS capabilities changed after the canonical catalog check")
upper = body[:4096].upper()
if b"<!DOCTYPE" in upper or b"<!ENTITY" in upper:
raise RuntimeError("Official WMS capabilities contains a prohibited XML declaration")
try:
root = ElementTree.fromstring(body)
except ElementTree.ParseError as exc:
raise RuntimeError("Official WMS capabilities is invalid XML") from exc
if _local_name(root.tag) != "WMS_Capabilities" or root.attrib.get("version") != "1.3.0":
raise RuntimeError("Official orthophoto service is not WMS 1.3.0")
capability = next((node for node in root if _local_name(node.tag) == "Capability"), None)
root_layer = (
next((node for node in capability if _local_name(node.tag) == "Layer"), None)
if capability is not None
else None
)
if capability is None or root_layer is None:
raise RuntimeError("Official WMS capabilities has no root layer")
layers: dict[str, ElementTree.Element] = {}
for node in root.iter():
if _local_name(node.tag) != "Layer":
continue
name = _child_text(node, "Name")
if name:
layers[name] = node
if set(("Ortho", "Vliegdagcontour")) - set(layers):
raise RuntimeError("Official WMS capabilities does not contain both governed orthophoto layers")
if layers["Vliegdagcontour"].attrib.get("queryable") != "1":
raise RuntimeError("Official Vliegdagcontour layer is not queryable")
formats: set[str] = set()
for request_node in capability:
if _local_name(request_node.tag) != "Request":
continue
for operation in request_node:
if _local_name(operation.tag) == "GetFeatureInfo":
formats.update(
(item.text or "").strip().lower()
for item in operation
if _local_name(item.tag) == "Format" and item.text
)
if "application/geo+json" not in formats:
raise RuntimeError("Official WMS does not advertise GeoJSON feature info")
bbox = None
for node in _children(root_layer, "BoundingBox"):
if node.attrib.get("CRS") == "EPSG:31370":
try:
bbox = [float(node.attrib[key]) for key in ("minx", "miny", "maxx", "maxy")]
except (KeyError, ValueError) as exc:
raise RuntimeError("Official WMS EPSG:31370 extent is invalid") from exc
break
if bbox is None or bbox[0] >= bbox[2] or bbox[1] >= bbox[3]:
raise RuntimeError("Official WMS does not advertise a valid EPSG:31370 extent")
metadata_identifiers: set[str] = set()
for layer_name in ("Ortho", "Vliegdagcontour"):
for node in layers[layer_name].iter():
if _local_name(node.tag) != "OnlineResource":
continue
href = next((value for key, value in node.attrib.items() if _local_name(key) == "href"), "")
if METADATA_IDENTIFIER in href:
metadata_identifiers.add(METADATA_IDENTIFIER)
if metadata_identifiers != {METADATA_IDENTIFIER}:
raise RuntimeError("Governed orthophoto layers do not share the expected ISO metadata identity")
return {
"service_version": "1.3.0",
"capabilities_sha256": actual_sha256,
"layers": ["Ortho", "Vliegdagcontour"],
"vliegdagcontour_queryable": True,
"feature_info_format": "application/geo+json",
"extent_epsg31370": bbox,
"metadata_identifier": METADATA_IDENTIFIER,
}
def parse_coverage_description(body: bytes) -> dict[str, Any]:
upper = body[:4096].upper()
if b"<!DOCTYPE" in upper or b"<!ENTITY" in upper:
raise RuntimeError("Official WCS coverage description contains a prohibited XML declaration")
try:
root = ElementTree.fromstring(body)
except ElementTree.ParseError as exc:
raise RuntimeError("Official WCS coverage description is invalid XML") from exc
coverage_ids = [
(node.text or "").strip()
for node in root.iter()
if _local_name(node.tag) == "CoverageId" and node.text
]
if coverage_ids != ["Ortho"]:
raise RuntimeError("Official WCS coverage description is not the governed Ortho coverage")
envelope = next((node for node in root.iter() if _local_name(node.tag) == "Envelope"), None)
if envelope is None or not str(envelope.attrib.get("srsName") or "").endswith("/31370"):
raise RuntimeError("Official WCS Ortho domain is not EPSG:31370")
try:
lower = [float(value) for value in (_child_text(envelope, "lowerCorner") or "").split()]
upper_corner = [float(value) for value in (_child_text(envelope, "upperCorner") or "").split()]
except ValueError as exc:
raise RuntimeError("Official WCS Ortho domain corners are invalid") from exc
if len(lower) != 2 or len(upper_corner) != 2 or lower[0] >= upper_corner[0] or lower[1] >= upper_corner[1]:
raise RuntimeError("Official WCS Ortho domain corners are invalid")
grid = next((node for node in root.iter() if _local_name(node.tag) == "RectifiedGrid"), None)
if grid is None or grid.attrib.get("dimension") != "2":
raise RuntimeError("Official WCS Ortho domain is not a two-dimensional rectified grid")
offsets: list[list[float]] = []
for node in grid.iter():
if _local_name(node.tag) == "offsetVector" and node.text:
try:
offsets.append([float(value) for value in node.text.split()])
except ValueError as exc:
raise RuntimeError("Official WCS Ortho grid resolution is invalid") from exc
if (
len(offsets) != 2
or any(len(vector) != 2 for vector in offsets)
or not math.isclose(offsets[0][0], 0.15, abs_tol=1e-9)
or not math.isclose(offsets[0][1], 0.0, abs_tol=1e-9)
or not math.isclose(offsets[1][0], 0.0, abs_tol=1e-9)
or not math.isclose(offsets[1][1], -0.15, abs_tol=1e-9)
):
raise RuntimeError("Official WCS Ortho grid is no longer the governed 15 cm product")
field_count = sum(_local_name(node.tag) == "field" for node in root.iter())
subtype = next(
((node.text or "").strip() for node in root.iter() if _local_name(node.tag) == "CoverageSubtype"),
"",
)
native_format = next(
((node.text or "").strip() for node in root.iter() if _local_name(node.tag) == "nativeFormat"),
"",
)
if field_count != 3 or subtype != "RectifiedGridCoverage" or native_format != "image/tiff":
raise RuntimeError("Official WCS Ortho range or native format changed")
return {
"coverage_id": "Ortho",
"crs": "EPSG:31370",
"extent_epsg31370": [lower[0], lower[1], upper_corner[0], upper_corner[1]],
"native_resolution_m": 0.15,
"band_count": 3,
"native_format": "image/tiff",
"coverage_description_sha256": sha256(body).hexdigest(),
}
def validate_bbox(values: list[float], advertised_extent: list[float]) -> dict[str, Any]:
if len(values) != 4 or not all(math.isfinite(value) for value in values):
raise RuntimeError("Selection bbox must contain four finite EPSG:4326 values")
min_x, min_y, max_x, max_y = values
if min_x >= max_x or min_y >= max_y or not (-180 <= min_x < max_x <= 180) or not (-90 <= min_y < max_y <= 90):
raise RuntimeError("Selection bbox is not a valid EPSG:4326 rectangle")
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
lambert = [float(value) for value in transformer.transform_bounds(min_x, min_y, max_x, max_y, densify_pts=21)]
width_m = lambert[2] - lambert[0]
height_m = lambert[3] - lambert[1]
if width_m < MIN_SIDE_M or height_m < MIN_SIDE_M:
raise RuntimeError(f"Orthophoto preflight selection must be at least {MIN_SIDE_M:.0f} by {MIN_SIDE_M:.0f} metres")
if width_m > MAX_SIDE_M or height_m > MAX_SIDE_M:
raise RuntimeError(f"Orthophoto preflight selection may not exceed {MAX_SIDE_M:.0f} by {MAX_SIDE_M:.0f} metres")
if not (
advertised_extent[0] <= lambert[0]
and advertised_extent[1] <= lambert[1]
and lambert[2] <= advertised_extent[2]
and lambert[3] <= advertised_extent[3]
):
raise RuntimeError("Selection is outside the official WMS advertised EPSG:31370 extent")
return {
"bbox_epsg4326": [min_x, min_y, max_x, max_y],
"bbox_epsg31370": lambert,
"width_m": width_m,
"height_m": height_m,
}
def validate_product(items: list[dict[str, Any]]) -> dict[str, Any]:
matches = [item for item in items if item.get("key") == PRODUCT_KEY]
if len(matches) != 1:
raise RuntimeError("Orthophoto product registry does not contain exactly one most_recent product")
product = matches[0]
expected = {
"temporal_granularity": "snapshot",
"native_resolution_m": 0.15,
"supports_detection": True,
"color_mode": "rgb",
"catalog_url": CATALOG_URL,
}
if any(product.get(key) != value for key, value in expected.items()):
raise RuntimeError("Current orthophoto product variant no longer matches the governed registry contract")
return {"key": PRODUCT_KEY, **expected, "display_name": product.get("display_name")}
def catalog_decision(item: dict[str, Any]) -> dict[str, Any]:
remote_version = str(item.get("remote_version") or "")
remote_match = EDITION_PATTERN.fullmatch(remote_version)
if (
item.get("source_name") != SOURCE_NAME
or item.get("status") != "available"
or item.get("reachable") is not True
or set(item.get("matched_layers") or []) != {"Ortho", "Vliegdagcontour"}
or item.get("missing_layers") not in (None, [])
or item.get("metadata_identifier") != METADATA_IDENTIFIER
or not SHA256_PATTERN.fullmatch(str(item.get("capabilities_sha256") or ""))
or not remote_match
or remote_version not in str(item.get("remote_title") or "")
):
raise RuntimeError("Canonical catalog report does not contain one complete governed orthophoto release")
local_version = str(item.get("local_source_version") or "")
local_match = EDITION_PATTERN.fullmatch(local_version)
if not local_version:
status = "not_loaded"
expected_comparison = "no_local_data"
elif not local_match:
status = "blocked_local_version"
expected_comparison = "not_comparable"
else:
local_order = tuple(int(value) for value in local_match.groups())
remote_order = tuple(int(value) for value in remote_match.groups())
if local_order == remote_order:
status = "current"
expected_comparison = "same"
elif local_order < remote_order:
status = "update_available"
expected_comparison = "different"
else:
status = "blocked_remote_older"
expected_comparison = "different"
if item.get("comparison_status") != expected_comparison:
raise RuntimeError("Canonical catalog local/remote comparison is internally inconsistent")
return {
"status": status,
"remote_edition": remote_version,
"remote_year": int(remote_match.group(1)),
"local_source_version": local_version or None,
"comparison_status": expected_comparison,
"metadata_identifier": METADATA_IDENTIFIER,
"metadata_url": item.get("metadata_url"),
"remote_title": item.get("remote_title"),
"remote_modified_at": item.get("remote_modified_at"),
"remote_published_at": item.get("remote_published_at"),
"catalog_checked_at": item.get("checked_at"),
"capabilities_url": item.get("endpoint_url"),
"capabilities_sha256": item.get("capabilities_sha256"),
}
def _sample_grid(selection: dict[str, Any]) -> list[dict[str, Any]]:
count_x = max(1, math.ceil(selection["width_m"] / MAX_SAMPLE_SPACING_M))
count_y = max(1, math.ceil(selection["height_m"] / MAX_SAMPLE_SPACING_M))
if count_x * count_y > MAX_SAMPLE_COUNT:
raise RuntimeError("Deterministic flight-day grid exceeds the fixed preflight limit")
min_x, min_y, max_x, max_y = selection["bbox_epsg31370"]
samples: list[dict[str, Any]] = []
for row in range(count_y):
fraction_y = (row + 0.5) / count_y
for column in range(count_x):
fraction_x = (column + 0.5) / count_x
samples.append(
{
"column": column,
"row": row,
"i": min(FEATURE_INFO_SIZE - 1, int(fraction_x * FEATURE_INFO_SIZE)),
"j": min(FEATURE_INFO_SIZE - 1, int((1.0 - fraction_y) * FEATURE_INFO_SIZE)),
"x": min_x + fraction_x * (max_x - min_x),
"y": min_y + fraction_y * (max_y - min_y),
}
)
return samples
def _flight_sample(
base_url: str,
selection: dict[str, Any],
sample: dict[str, Any],
*,
timeout: int,
opener: Callable[..., Any] | None,
) -> dict[str, Any]:
params = {
"SERVICE": "WMS",
"VERSION": "1.3.0",
"REQUEST": "GetFeatureInfo",
"LAYERS": "Vliegdagcontour",
"QUERY_LAYERS": "Vliegdagcontour",
"STYLES": "",
"CRS": "EPSG:31370",
"BBOX": ",".join(f"{value:.3f}" for value in selection["bbox_epsg31370"]),
"WIDTH": str(FEATURE_INFO_SIZE),
"HEIGHT": str(FEATURE_INFO_SIZE),
"I": str(sample["i"]),
"J": str(sample["j"]),
"INFO_FORMAT": "application/geo+json",
"FEATURE_COUNT": "10",
"FORMAT": "image/png",
}
body, content_type, _ = _bounded_get(
f"{base_url}?{urlencode(params)}",
timeout=timeout,
max_bytes=MAX_FEATURE_INFO_BYTES,
accept="application/geo+json",
opener=opener,
)
if content_type not in {"application/geo+json", "application/json"}:
raise RuntimeError("Vliegdagcontour feature-info response is not GeoJSON")
try:
payload = json.loads(body)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("Vliegdagcontour feature-info response is invalid JSON") from exc
features = payload.get("features") if isinstance(payload, dict) else None
if not isinstance(payload, dict) or payload.get("type") != "FeatureCollection" or not isinstance(features, list) or not features:
raise RuntimeError("Vliegdagcontour has no coverage at one deterministic selection sample")
dates: set[str] = set()
feature_ids: set[str] = set()
years: set[int] = set()
for feature in features:
if not isinstance(feature, dict) or feature.get("layerName") != "Vliegdagcontour":
raise RuntimeError("Vliegdagcontour feature-info returned an unexpected layer")
properties = feature.get("properties")
if not isinstance(properties, dict):
raise RuntimeError("Vliegdagcontour feature-info has no properties")
date_value = str(properties.get("OpnDatum") or "").strip()
match = FLIGHT_YEAR_PATTERN.search(date_value)
if not match:
raise RuntimeError("Vliegdagcontour feature-info has no parseable acquisition year")
dates.add(date_value)
years.add(int(match.group(1)))
if properties.get("FID") is not None:
feature_ids.add(str(properties["FID"]))
return {
"column": sample["column"],
"row": sample["row"],
"x": round(sample["x"], 3),
"y": round(sample["y"], 3),
"dates": sorted(dates),
"years": sorted(years),
"feature_ids": sorted(feature_ids),
"response_sha256": sha256(body).hexdigest(),
}
def verify_flight_coverage(
base_url: str,
selection: dict[str, Any],
*,
timeout: int,
opener: Callable[..., Any] | None = None,
) -> dict[str, Any]:
samples = _sample_grid(selection)
worker_count = min(8, len(samples))
with ThreadPoolExecutor(max_workers=worker_count) as executor:
evidence = list(
executor.map(
lambda sample: _flight_sample(
base_url,
selection,
sample,
timeout=timeout,
opener=opener,
),
samples,
)
)
years = sorted({year for item in evidence for year in item["years"]})
dates = sorted({date for item in evidence for date in item["dates"]})
feature_ids = sorted({feature_id for item in evidence for feature_id in item["feature_ids"]})
canonical = json.dumps(evidence, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
count_x = len({item["column"] for item in evidence})
count_y = len({item["row"] for item in evidence})
return {
"status": "passed",
"mode": "official_queryable_flight_day_grid",
"sample_count": len(evidence),
"grid_columns": count_x,
"grid_rows": count_y,
"maximum_sample_spacing_m": MAX_SAMPLE_SPACING_M,
"covered_sample_count": len(evidence),
"sample_coverage_ratio": 1.0,
"flight_dates": dates,
"flight_years": years,
"feature_ids": feature_ids,
"sample_evidence_sha256": sha256(canonical).hexdigest(),
"claim_boundary": (
"Every deterministic grid sample is covered by the official queryable flight-day layer. "
"The WMS exposes no vector geometry, so this is bounded point evidence and not a polygon-union proof."
),
}
def run_preflight(
args: argparse.Namespace,
*,
loader: Callable[[str, str, int], dict[str, Any]] = api_data,
opener: Callable[..., Any] | None = None,
) -> dict[str, Any]:
validate_project_scope(args, loader)
products = loader(
args.api_url,
f"projects/{args.project_id}/datasets/orthophoto/products",
args.api_timeout,
)
product = validate_product(products.get("items") or [])
refresh = "true" if args.refresh_catalog else "false"
report = loader(
args.api_url,
f"projects/{args.project_id}/datasets/source-catalog-probes?refresh={refresh}",
args.api_timeout,
)
matches = [item for item in report.get("items") or [] if item.get("source_name") == SOURCE_NAME]
if len(matches) != 1:
raise RuntimeError("Source catalog report did not contain exactly one orthophoto contract")
release = catalog_decision(matches[0])
capabilities_url = str(release["capabilities_url"] or "")
base_url = _validate_wms_url(capabilities_url, request_name="GetCapabilities")
capabilities_body, content_type, _ = _bounded_get(
capabilities_url,
timeout=args.wms_timeout,
max_bytes=MAX_CAPABILITIES_BYTES,
accept="application/xml,text/xml",
opener=opener,
)
if content_type and "xml" not in content_type and "text" not in content_type:
raise RuntimeError("Official WMS capabilities response is not XML")
capabilities = parse_capabilities(capabilities_body, release["capabilities_sha256"])
coverage_description_url = (
f"https://{WMS_HOST}{WCS_PATH}?"
+ urlencode(
{
"SERVICE": "WCS",
"VERSION": "2.0.1",
"REQUEST": "DescribeCoverage",
"COVERAGEID": "Ortho",
}
)
)
_validate_wcs_url(coverage_description_url, request_name="DescribeCoverage")
coverage_body, coverage_content_type, _ = _bounded_get(
coverage_description_url,
timeout=args.wms_timeout,
max_bytes=MAX_COVERAGE_DESCRIPTION_BYTES,
accept="application/xml,text/xml",
opener=opener,
validator=_validate_wcs_url,
)
if coverage_content_type and "xml" not in coverage_content_type and "text" not in coverage_content_type:
raise RuntimeError("Official WCS coverage description response is not XML")
coverage_description = parse_coverage_description(coverage_body)
selection = validate_bbox(list(args.bbox), coverage_description["extent_epsg31370"])
validate_bbox(list(args.bbox), capabilities["extent_epsg31370"])
coverage = verify_flight_coverage(
base_url,
selection,
timeout=args.wms_timeout,
opener=opener,
)
flight_year_matches = coverage["flight_years"] == [release["remote_year"]]
release_actionable = release["status"] in {"not_loaded", "update_available"}
staging_permitted = release_actionable and flight_year_matches
if release["status"] == "blocked_local_version":
next_action = "establish_official_local_edition_before_staging"
elif release["status"] == "current":
next_action = "none_current"
elif release["status"] == "blocked_remote_older":
next_action = "investigate_catalog_regression"
elif not flight_year_matches:
next_action = "split_or_review_selection_flight_years"
else:
next_action = "governed_pixel_stage"
return {
"schema_version": 1,
"status": "passed",
"generated_at": datetime.now(timezone.utc).isoformat(),
"project_id": args.project_id,
"scope": args.scope,
"product": product,
"release": release,
"capabilities": capabilities,
"coverage_domain": {
**coverage_description,
"selected_area_fully_inside_domain": True,
"pixel_data_requested": False,
},
"selection": {
"bbox_epsg4326": selection["bbox_epsg4326"],
"bbox_epsg31370": [round(value, 3) for value in selection["bbox_epsg31370"]],
"width_m": round(selection["width_m"], 3),
"height_m": round(selection["height_m"], 3),
},
"flight_day_coverage": coverage,
"flight_year_matches_release": flight_year_matches,
"staging_permitted": staging_permitted,
"next_action": next_action,
"pixel_requests_performed": 0,
"datasets_mutated": 0,
"automatic_staging": False,
"automatic_import": False,
}
def main() -> int:
try:
report = run_preflight(parse_args())
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
except (RuntimeError, ValueError) as exc:
print(json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())