"""Provision the official 2025 BWK/Natura 2000 dataset for Mol. The operator follows every WFS page, retains raw checksummed responses, clips polygon geometry to the persisted Mol Area in EPSG:31370 and imports the final GeoJSON through the canonical DatasetService upload route. It never writes directly to vector_features. """ from __future__ import annotations import argparse import hashlib import json import os import re import sys from collections import defaultdict from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable import requests from pyproj import Transformer from requests.adapters import HTTPAdapter from shapely.geometry import MultiPolygon, Polygon, mapping, shape from shapely.ops import transform as transform_geometry from shapely.ops import unary_union from shapely.validation import make_valid from urllib3.util.retry import Retry WFS_URL = "https://geo.api.vlaanderen.be/BWK/wfs" CATALOG_URL = ( "https://www.vlaanderen.be/datavindplaats/catalogus/" "biologische-waarderingskaart-en-natura-2000-habitatkaart-toestand-2025" ) REPORT_URL = "https://doi.org/10.21436/inbor.129502912" TYPE_NAME = "BWK:Bwkhab" SOURCE_VERSION = "2025" TEMPORAL_SERIES_KEY = "inbo-bwk-natura2000:mol" PUBLICATION_DATE = "2025-12-10T00:00:00Z" ATTRIBUTION = "Bron: INBO" DEFAULT_API_URL = "http://127.0.0.1:8000" DEFAULT_OUTPUT_DIR = "/app/storage/operator-evidence/bwk-natura2000-2025/mol" DEFAULT_PROJECT_NAME = "Kempen Regional Workbench" DEFAULT_AREA_NAME = "Gemeente Mol - officiele grens" DATASET_FILENAME = "bwk_natura2000_2025_mol.geojson" MANIFEST_FILENAME = "bwk_natura2000_2025_mol.manifest.json" SCHEMA_VERSION = 1 EVALUATION_LABELS = { "z": "Biologisch zeer waardevol", "w": "Biologisch waardevol", "m": "Biologisch minder waardevol", "wz": "Complex van waardevolle en zeer waardevolle elementen", "mwz": "Complex van minder waardevolle, waardevolle en zeer waardevolle elementen", "mz": "Complex van minder waardevolle en zeer waardevolle elementen", "mw": "Complex van minder waardevolle en waardevolle elementen", } HABITAT_STATUS_LABELS = { "gh": "Geen Natura 2000-habitat aangeduid", "hab": "Volledig habitatwaardig kaartvlak", "phab": "Gedeeltelijk habitatwaardig kaartvlak", "ohab": "Onzeker habitat of kennislacune", } TO_LAMBERT72 = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True) TO_WGS84 = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Provision official BWK/Natura 2000 state 2025 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_NAME) parser.add_argument( "--output-dir", type=Path, default=Path(os.environ.get("GEOINTEL_BWK_OUTPUT_DIR", DEFAULT_OUTPUT_DIR)), ) parser.add_argument("--page-limit", type=int, default=1000) parser.add_argument("--max-features", type=int, default=30_000) parser.add_argument("--request-timeout", type=int, default=180) parser.add_argument("--import-timeout", type=int, default=1800) parser.add_argument("--force", action="store_true", help="Refetch source pages and rebuild the artifact.") parser.add_argument("--fetch-only", action="store_true", help="Build evidence without importing a Dataset.") return parser.parse_args() def utc_now() -> str: return datetime.now(timezone.utc).isoformat() def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def write_bytes_atomic(path: Path, value: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_suffix(path.suffix + ".tmp") temporary.write_bytes(value) temporary.replace(path) def write_json_atomic(path: Path, value: Any, *, pretty: bool = False) -> None: encoded = json.dumps( value, ensure_ascii=False, indent=2 if pretty else None, separators=None if pretty else (",", ":"), ).encode("utf-8") write_bytes_atomic(path, encoded) def source_session() -> requests.Session: retry = Retry( total=5, connect=5, read=5, status=5, backoff_factor=1.0, status_forcelist=(429, 500, 502, 503, 504), allowed_methods=frozenset({"GET"}), raise_on_status=True, ) session = requests.Session() session.headers.update({"User-Agent": "GeoIntel-BWK-Natura2000-Mol-Operator/1.0"}) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) session.mount("http://", adapter) return session def response_data(response: requests.Response) -> Any: try: payload = response.json() except ValueError as exc: raise RuntimeError(f"GeoIntel API returned non-JSON ({response.status_code}): {response.text[:300]}") from exc if not response.ok: raise RuntimeError( f"GeoIntel API failed ({response.status_code}): {json.dumps(payload, ensure_ascii=False)[:800]}" ) if not isinstance(payload, dict) or "data" not in payload: raise RuntimeError("GeoIntel API response does not use the canonical data envelope") return payload["data"] def paginated_items(session: requests.Session, url: str, *, timeout: int) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] offset = 0 total: int | None = None while total is None or offset < total: page = response_data(session.get(url, params={"limit": 200, "offset": offset}, timeout=timeout)) page_items = list(page.get("items") or []) page_total = int(page.get("total") or 0) if total is None: total = page_total elif page_total != total: raise RuntimeError("GeoIntel pagination total changed while reading the workspace") items.extend(page_items) if not page_items: break offset += len(page_items) if total is not None and len(items) != total: raise RuntimeError(f"GeoIntel pagination returned {len(items)} of {total} items") return items def polygonal_geometry(geometry): if geometry is None or geometry.is_empty: return None if not geometry.is_valid: geometry = make_valid(geometry) polygons: list[Polygon] = [] def collect(candidate) -> None: if candidate is None or candidate.is_empty: return if isinstance(candidate, Polygon): polygons.append(candidate) return if isinstance(candidate, MultiPolygon): polygons.extend(part for part in candidate.geoms if not part.is_empty) return if hasattr(candidate, "geoms"): for part in candidate.geoms: collect(part) collect(geometry) if not polygons: return None result = unary_union(polygons) if not result.is_valid: result = make_valid(result) return result if not result.is_empty and result.is_valid else None def locate_workspace( session: requests.Session, base_url: str, project_name: str, area_name: str, timeout: int, ) -> tuple[str, str, Any, list[dict[str, Any]]]: projects = paginated_items(session, f"{base_url}/api/v1/projects", timeout=timeout) project = next((item for item in projects if item.get("name") == project_name), None) if not project: raise RuntimeError(f"Project {project_name!r} is missing") project_id = str(project["id"]) areas = paginated_items(session, f"{base_url}/api/v1/projects/{project_id}/areas", timeout=timeout) normalized_target = area_name.casefold().replace("officiele", "officiƫle") area = next( ( item for item in areas if str(item.get("name") or "").casefold().replace("officiele", "officiƫle") == normalized_target ), None, ) if not area: area = next((item for item in areas if "gemeente mol" in str(item.get("name") or "").casefold()), None) if not area or not area.get("geometry"): raise RuntimeError(f"Persisted Mol Area {area_name!r} with geometry is missing") boundary_wgs84 = polygonal_geometry(shape(area["geometry"])) if boundary_wgs84 is None: raise RuntimeError("Persisted Mol Area geometry is invalid or not polygonal") datasets = paginated_items(session, f"{base_url}/api/v1/projects/{project_id}/datasets", timeout=timeout) return project_id, str(area["id"]), boundary_wgs84, datasets def next_page_url(payload: dict[str, Any]) -> str | None: for link in payload.get("links") or []: if str(link.get("rel") or "").lower() == "next" and link.get("href"): return str(link["href"]) return None def iter_wfs_pages( session: requests.Session, bbox: tuple[float, float, float, float], *, page_limit: int, timeout: int, ) -> Iterable[tuple[dict[str, Any], str, bytes]]: url: str | None = WFS_URL base_params: dict[str, str] = { "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": TYPE_NAME, "srsName": "EPSG:4326", "bbox": ",".join(f"{value:.8f}" for value in bbox) + ",EPSG:4326", "count": str(page_limit), "sortBy": "UIDN", "outputFormat": "application/json", } params: dict[str, str] | None = dict(base_params) seen: set[str] = set() next_start_index = 0 while url: request_key = requests.Request("GET", url, params=params).prepare().url or url if request_key in seen: raise RuntimeError("BWK WFS pagination loop detected") seen.add(request_key) response = session.get(url, params=params, timeout=timeout) params = None response.raise_for_status() payload = response.json() if not isinstance(payload, dict) or payload.get("type") != "FeatureCollection": raise RuntimeError("BWK WFS returned an invalid FeatureCollection") yield payload, response.url, response.content next_url = next_page_url(payload) number_returned = int(payload.get("numberReturned") or len(payload.get("features") or [])) next_start_index += number_returned if next_url: url = next_url params = None elif number_returned >= page_limit: url = WFS_URL params = {**base_params, "startIndex": str(next_start_index)} else: url = None params = None def habitat_breakdown(properties: dict[str, Any]) -> tuple[list[dict[str, Any]], float, float, float]: entries: list[dict[str, Any]] = [] natura_share = 0.0 regional_share = 0.0 uncertain_share = 0.0 for index in range(1, 6): code = str(properties.get(f"HAB{index}") or "").strip() try: share = max(0.0, min(100.0, float(properties.get(f"PHAB{index}") or 0.0))) except (TypeError, ValueError): share = 0.0 if not code or share <= 0: continue normalized = code.lower() entries.append({"code": code, "share_percent": share}) if re.match(r"^\d", normalized): natura_share += share elif normalized.startswith("rbb"): regional_share += share elif normalized == "ohab": uncertain_share += share status = str(properties.get("HABLEGENDE") or "").strip().lower() if status == "ohab" and uncertain_share <= 0: uncertain_share = 100.0 return entries, min(natura_share, 100.0), min(regional_share, 100.0), min(uncertain_share, 100.0) def normalize_feature( feature: dict[str, Any], boundary_lambert72, *, municipality: str = "Mol", nis_code: str = "13025", coverage_scope: str = "municipality", feature_id_suffix: str | None = None, ) -> tuple[dict[str, Any] | None, bool]: geometry_payload = feature.get("geometry") if not geometry_payload: return None, False source_wgs84 = polygonal_geometry(shape(geometry_payload)) if source_wgs84 is None: return None, False source_lambert72 = polygonal_geometry(transform_geometry(TO_LAMBERT72.transform, source_wgs84)) if source_lambert72 is None or not source_lambert72.intersects(boundary_lambert72): return None, False was_clipped = not source_lambert72.within(boundary_lambert72) clipped_lambert72 = polygonal_geometry(source_lambert72.intersection(boundary_lambert72)) if clipped_lambert72 is None or clipped_lambert72.area <= 0: return None, was_clipped clipped_wgs84 = polygonal_geometry(transform_geometry(TO_WGS84.transform, clipped_lambert72)) if clipped_wgs84 is None: return None, was_clipped raw_properties = dict(feature.get("properties") or {}) source_id = str(feature.get("id") or raw_properties.get("UIDN") or raw_properties.get("OIDN") or "").strip() if not source_id: source_id = sha256_bytes(json.dumps(geometry_payload, sort_keys=True).encode("utf-8")) stable_id = f"BWK:Bwkhab:{raw_properties.get('UIDN') or source_id}" if feature_id_suffix: stable_id = f"{stable_id}:{feature_id_suffix}" evaluation_code = str(raw_properties.get("EVAL") or "").strip().lower() habitat_status = str(raw_properties.get("HABLEGENDE") or "").strip().lower() habitats, natura_share, regional_share, uncertain_share = habitat_breakdown(raw_properties) clipped_area_ha = float(clipped_lambert72.area) / 10_000.0 natura_codes = [entry["code"] for entry in habitats if re.match(r"^\d", str(entry["code"]))] regional_codes = [entry["code"] for entry in habitats if str(entry["code"]).lower().startswith("rbb")] properties = { **raw_properties, "source_name": "inbo_bwk_natura2000", "source_collection": TYPE_NAME, "source_feature_id": stable_id, "reference_layer_name": "nature_value", "theme": "nature_value", "authority_level": "authoritative", "coverage_scope": coverage_scope, "municipality": municipality, "nis_code": nis_code, "source_version": SOURCE_VERSION, "attribution": ATTRIBUTION, "bwk_evaluation_code": evaluation_code or "unknown", "bwk_evaluation_label": EVALUATION_LABELS.get(evaluation_code, "Onbekende of ontbrekende BWK-waardering"), "bwk_label": str(raw_properties.get("BWKLABEL") or "").strip(), "bwk_units": ", ".join( str(raw_properties.get(f"EENH{index}") or "").strip() for index in range(1, 9) if str(raw_properties.get(f"EENH{index}") or "").strip() ), "bwk_origin_code": str(raw_properties.get("HERK") or "").strip(), "habitat_status_code": habitat_status or "unknown", "habitat_status_label": HABITAT_STATUS_LABELS.get(habitat_status, "Onbekende of ontbrekende habitatstatus"), "natura2000_codes": ", ".join(natura_codes), "regional_biotope_codes": ", ".join(regional_codes), "habitat_entries": habitats, "habitat_origin_code": str(raw_properties.get("HERKHAB") or "").strip(), "habitat_share_origin_code": str(raw_properties.get("HERKPHAB") or "").strip(), "clipped_area_ha": round(clipped_area_ha, 8), "natura2000_share_percent": natura_share, "regional_biotope_share_percent": regional_share, "uncertain_habitat_share_percent": uncertain_share, "natura2000_area_ha": round(clipped_area_ha * natura_share / 100.0, 8), "regional_biotope_area_ha": round(clipped_area_ha * regional_share / 100.0, 8), "uncertain_habitat_area_ha": round(clipped_area_ha * uncertain_share / 100.0, 8), "habitat_share_spatial_limitation": ( "PHAB percentages apply to the full source polygon; a partial map selection scales them by intersected area." ), } return { "type": "Feature", "id": stable_id, "geometry": mapping(clipped_wgs84), "properties": properties, }, was_clipped def reusable_artifact(output_dir: Path) -> tuple[Path, Path, dict[str, Any]] | None: artifact_path = output_dir / DATASET_FILENAME manifest_path = output_dir / MANIFEST_FILENAME if not artifact_path.is_file() or not manifest_path.is_file(): return None try: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) except (OSError, ValueError): return None if manifest.get("schema_version") != SCHEMA_VERSION or manifest.get("source_version") != SOURCE_VERSION: return None if manifest.get("artifact_sha256") != sha256_file(artifact_path): return None for page in manifest.get("raw_pages") or []: page_path = output_dir / str(page.get("path") or "") if not page_path.is_file() or page.get("sha256") != sha256_file(page_path): return None return artifact_path, manifest_path, manifest def prepare_artifact( session: requests.Session, boundary_wgs84, output_dir: Path, *, page_limit: int, max_features: int, timeout: int, force: bool, ) -> tuple[Path, Path, dict[str, Any]]: if not force: reusable = reusable_artifact(output_dir) if reusable: return reusable output_dir.mkdir(parents=True, exist_ok=True) raw_dir = output_dir / "raw" raw_dir.mkdir(parents=True, exist_ok=True) boundary_lambert72 = polygonal_geometry(transform_geometry(TO_LAMBERT72.transform, boundary_wgs84)) if boundary_lambert72 is None: raise RuntimeError("Mol boundary could not be transformed to EPSG:31370") retained: list[dict[str, Any]] = [] raw_pages: list[dict[str, Any]] = [] source_urls: list[str] = [] seen_ids: set[str] = set() raw_feature_count = 0 duplicate_count = 0 rejected_count = 0 clipped_count = 0 for page_number, (payload, source_url, raw_bytes) in enumerate( iter_wfs_pages(session, boundary_wgs84.bounds, page_limit=page_limit, timeout=timeout), start=1, ): page_path = raw_dir / f"bwk_bwkhab_page_{page_number:05d}.json" write_bytes_atomic(page_path, raw_bytes) page_features = list(payload.get("features") or []) raw_feature_count += len(page_features) if raw_feature_count > max_features: raise RuntimeError( f"BWK WFS returned more than the {max_features} feature safety limit; refusing a truncated import" ) raw_pages.append( { "path": str(page_path.relative_to(output_dir)), "sha256": sha256_bytes(raw_bytes), "size_bytes": len(raw_bytes), "feature_count": len(page_features), "source_url": source_url, } ) source_urls.append(source_url) for source_feature in page_features: source_identity = str( source_feature.get("id") or (source_feature.get("properties") or {}).get("UIDN") or "" ) if source_identity and source_identity in seen_ids: duplicate_count += 1 continue if source_identity: seen_ids.add(source_identity) normalized, was_clipped = normalize_feature(source_feature, boundary_lambert72) if normalized is None: rejected_count += 1 continue if was_clipped: clipped_count += 1 retained.append(normalized) if not retained: raise RuntimeError("BWK WFS returned no valid polygon features inside the persisted Mol Area") evaluation_area: dict[str, float] = defaultdict(float) habitat_status_area: dict[str, float] = defaultdict(float) natura_area = 0.0 regional_area = 0.0 uncertain_area = 0.0 for feature in retained: properties = feature["properties"] area = float(properties["clipped_area_ha"]) evaluation_area[str(properties["bwk_evaluation_code"])] += area habitat_status_area[str(properties["habitat_status_code"])] += area natura_area += float(properties["natura2000_area_ha"]) regional_area += float(properties["regional_biotope_area_ha"]) uncertain_area += float(properties["uncertain_habitat_area_ha"]) generated_at = utc_now() artifact = { "type": "FeatureCollection", "name": "BWK en Natura 2000 - toestand 2025 - Gemeente Mol", "features": retained, "source": "INBO BWK/Natura 2000 WFS", "source_version": SOURCE_VERSION, "attribution": ATTRIBUTION, "catalog_url": CATALOG_URL, "report_url": REPORT_URL, "coverage_scope": "municipality", "municipality": "Mol", "nis_code": "13025", "reference_truncated": False, "generated_at": generated_at, } artifact_path = output_dir / DATASET_FILENAME write_json_atomic(artifact_path, artifact) manifest = { "schema_version": SCHEMA_VERSION, "source_version": SOURCE_VERSION, "source_type_name": TYPE_NAME, "wfs_url": WFS_URL, "catalog_url": CATALOG_URL, "report_url": REPORT_URL, "attribution": ATTRIBUTION, "generated_at": generated_at, "crs_source": "EPSG:4326", "crs_clip": "EPSG:31370", "crs_persisted": "EPSG:4326", "boundary_sha256": sha256_bytes(json.dumps(mapping(boundary_wgs84), sort_keys=True).encode("utf-8")), "boundary_bbox_wgs84": list(boundary_wgs84.bounds), "page_limit": page_limit, "page_count": len(raw_pages), "raw_source_feature_count": raw_feature_count, "feature_count": len(retained), "duplicate_count": duplicate_count, "rejected_or_outside_count": rejected_count, "clipped_feature_count": clipped_count, "reference_truncated": False, "raw_pages": raw_pages, "source_urls": source_urls, "evaluation_area_ha": {key: round(value, 6) for key, value in sorted(evaluation_area.items())}, "habitat_status_area_ha": {key: round(value, 6) for key, value in sorted(habitat_status_area.items())}, "natura2000_area_ha": round(natura_area, 6), "regional_biotope_area_ha": round(regional_area, 6), "uncertain_habitat_area_ha": round(uncertain_area, 6), "artifact_path": str(artifact_path), "artifact_sha256": sha256_file(artifact_path), "artifact_size_bytes": artifact_path.stat().st_size, "limitations": [ "The 2025 edition is the best available map state, not one uniform 2025 field survey.", "PHAB percentages may be theoretical shares; partial selections scale shares by intersected polygon area.", "The real field situation remains authoritative for policy and legal use.", ], } manifest_path = output_dir / MANIFEST_FILENAME write_json_atomic(manifest_path, manifest, pretty=True) return artifact_path, manifest_path, manifest def selection_metrics() -> list[dict[str, Any]]: share_warning = ( "Oppervlakte op basis van de officiele PHAB-aandelen. Automatisch verdeelde aandelen kunnen lokaal " "afwijken van de terreinsituatie." ) return [ { "metric_key": "bwk_very_valuable_area", "method": "intersection_area", "label": "Biologisch zeer waardevol", "unit": "ha", "geometry_dimension": 2, "filter_property": "bwk_evaluation_code", "filter_values": ["z"], }, { "metric_key": "bwk_valuable_area", "method": "intersection_area", "label": "Biologisch waardevol", "unit": "ha", "geometry_dimension": 2, "filter_property": "bwk_evaluation_code", "filter_values": ["w"], }, { "metric_key": "bwk_less_valuable_area", "method": "intersection_area", "label": "Biologisch minder waardevol", "unit": "ha", "geometry_dimension": 2, "filter_property": "bwk_evaluation_code", "filter_values": ["m"], }, { "metric_key": "bwk_mixed_value_area", "method": "intersection_area", "label": "Gemengde BWK-waardering", "unit": "ha", "geometry_dimension": 2, "filter_property": "bwk_evaluation_code", "filter_values": ["wz", "mwz", "mz", "mw"], }, { "metric_key": "natura2000_area", "method": "area_weighted_sum", "property": "natura2000_area_ha", "label": "Natura 2000-habitat", "unit": "ha", "is_estimate": True, "warning": share_warning, "warning_only_when_estimate": False, }, { "metric_key": "regional_biotope_area", "method": "area_weighted_sum", "property": "regional_biotope_area_ha", "label": "Regionaal belangrijk biotoop", "unit": "ha", "is_estimate": True, "warning": share_warning, "warning_only_when_estimate": False, }, { "metric_key": "uncertain_habitat_area", "method": "area_weighted_sum", "property": "uncertain_habitat_area_ha", "label": "Onzeker habitat / kennislacune", "unit": "ha", "is_estimate": True, "warning": "Dit is een maximale potentiele oppervlakte van kaartvlakken met de status ohab, geen bevestigd habitat.", "warning_only_when_estimate": False, }, ] def upload_artifact( session: requests.Session, *, base_url: str, project_id: str, area_id: str, artifact_path: Path, manifest_path: Path, manifest: dict[str, Any], timeout: int, ) -> dict[str, Any]: source_metadata = { "provider": "Instituut voor Natuur- en Bosonderzoek", "theme": "nature_value", "layer_name": "BWK/Natura 2000 toestand 2025", "authority_level": "authoritative", "coverage_scope": "municipality", "municipality": "Mol", "nis_code": "13025", "feature_count": manifest["feature_count"], "geometry_clipped_to_area": True, "semantic_metrics": False, "attribution": ATTRIBUTION, "catalog_url": CATALOG_URL, "report_url": REPORT_URL, "selection_aggregation": { "metric_key": "bwk_mapped_area", "method": "intersection_area", "label": "BWK-gekarteerde oppervlakte", "unit": "ha", "geometry_dimension": 2, "warning": ( "Uitgave 2025 is de best beschikbare kaarttoestand, maar niet elk kaartvlak is in 2025 op terrein gekarteerd." ), }, "selection_metrics": selection_metrics(), } provenance_metadata = { "operator_tool": "provision_mol_bwk_natura2000.py", "operator_explicit_fetch": True, "geometry_clipped_to_area": True, "source_type_name": TYPE_NAME, "wfs_url": WFS_URL, "catalog_url": CATALOG_URL, "report_url": REPORT_URL, "manifest_path": str(manifest_path), "artifact_sha256": manifest["artifact_sha256"], "raw_page_checksums": {page["path"]: page["sha256"] for page in manifest["raw_pages"]}, "source_urls": manifest["source_urls"], "reference_truncated": False, "generated_at": manifest["generated_at"], "limitations": manifest["limitations"], } with artifact_path.open("rb") as handle: response = session.post( f"{base_url}/api/v1/projects/{project_id}/datasets/upload", data={ "dataset_type": "vector", "source": "operator_official_import", "dataset_role": "reference", "source_name": "inbo_bwk_natura2000", "reference_layer_name": "nature_value", "source_metadata_json": json.dumps(source_metadata, ensure_ascii=False), "provenance_metadata_json": json.dumps(provenance_metadata, ensure_ascii=False), "area_id": area_id, "temporal_series_key": TEMPORAL_SERIES_KEY, "observed_at": PUBLICATION_DATE, "temporal_granularity": "snapshot", "source_version": SOURCE_VERSION, }, files={"file": (artifact_path.name, handle, "application/geo+json")}, timeout=timeout, ) return response_data(response) def main() -> int: args = parse_args() if args.page_limit < 1 or args.page_limit > 5000 or args.max_features < args.page_limit: print(json.dumps({"status": "error", "message": "Invalid page or feature safety limits"}), file=sys.stderr) return 2 base_url = args.base_url.rstrip("/") try: with requests.Session() as api_session: project_id, area_id, boundary, existing = locate_workspace( api_session, base_url, args.project_name, args.area_name, args.import_timeout, ) with source_session() as official_session: artifact_path, manifest_path, manifest = prepare_artifact( official_session, boundary, args.output_dir, page_limit=args.page_limit, max_features=args.max_features, timeout=args.request_timeout, force=args.force, ) existing_dataset = next( ( item for item in existing if item.get("source_name") == "inbo_bwk_natura2000" and item.get("source_version") == SOURCE_VERSION and str(item.get("area_id") or "") == area_id ), None, ) if existing_dataset: persisted_checksum = str(existing_dataset.get("checksum_sha256") or "") if persisted_checksum and persisted_checksum != manifest["artifact_sha256"]: raise RuntimeError( "A different BWK 2025 Mol artifact is already persisted; refusing a silent duplicate or replacement" ) result = { "status": "existing", "dataset_id": existing_dataset["id"], "feature_count": existing_dataset.get("feature_count") or manifest["feature_count"], } elif args.fetch_only: result = { "status": "prepared", "artifact_path": str(artifact_path), "feature_count": manifest["feature_count"], } else: dataset = upload_artifact( api_session, base_url=base_url, project_id=project_id, area_id=area_id, artifact_path=artifact_path, manifest_path=manifest_path, manifest=manifest, timeout=args.import_timeout, ) result = { "status": "imported", "dataset_id": dataset["id"], "feature_count": dataset.get("feature_count"), } except (KeyError, OSError, RuntimeError, ValueError, requests.RequestException) as exc: print(json.dumps({"status": "error", "message": str(exc)}, ensure_ascii=False), file=sys.stderr) return 1 print( json.dumps( { "status": "ok", "project": args.project_name, "area": args.area_name, "source_version": SOURCE_VERSION, "manifest_path": str(manifest_path), "metrics": { "evaluation_area_ha": manifest["evaluation_area_ha"], "natura2000_area_ha": manifest["natura2000_area_ha"], "regional_biotope_area_ha": manifest["regional_biotope_area_ha"], "uncertain_habitat_area_ha": manifest["uncertain_habitat_area_ha"], }, "result": result, }, ensure_ascii=False, indent=2, ) ) return 0 if __name__ == "__main__": sys.exit(main())