from __future__ import annotations from dataclasses import dataclass from datetime import UTC, datetime, timedelta import hashlib import json import math from pathlib import Path import re from typing import Any, Callable from urllib.error import HTTPError, URLError from urllib.parse import urlencode, urlparse from urllib.request import HTTPRedirectHandler, Request, build_opener from uuid import UUID from geoalchemy2.shape import to_shape from pyproj import Transformer from shapely.geometry import LineString, MultiLineString, MultiPolygon, Polygon, box, mapping, shape from shapely.ops import transform, unary_union from shapely.validation import make_valid from app.core.config import Settings, get_settings from app.core.errors import AppError from app.models import Area, Dataset, Project from app.schemas.official_vector import ( OfficialVectorAcquireRequest, OfficialVectorAcquisitionResult, OfficialVectorProductRead, ) from app.services.dataset_service import DatasetService class _RejectRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): del req, fp, code, msg, headers, newurl return None _NO_REDIRECT_OPENER = build_opener(_RejectRedirects()) _TO_LAMBERT72 = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True) _TO_WGS84 = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True) @dataclass(frozen=True) class OfficialVectorProduct: key: str display_name: str theme: str provider: str source_name: str reference_layer_name: str service_type: str collection: str source_crs: str source_version: str observation_label: str authority_level: str catalog_url: str attribution: str license_note: str limitation_message: str source: str observed_at: datetime | None valid_from: datetime | None valid_to: datetime | None primary_metric: dict[str, Any] selection_metrics: tuple[dict[str, Any], ...] geometry_types: tuple[str, ...] = ("Polygon", "MultiPolygon") coverage_zones: tuple[str, ...] = ("flanders",) endpoint_kind: str = "wfs" response_crs: str = "EPSG:4326" identity_field: str | None = None requires_coverage_area: bool = False class OfficialVectorAcquisitionService: _BWK_EVALUATION_LABELS = { "z": "Biologisch zeer waardevol", "w": "Biologisch waardevol", "m": "Biologisch minder waardevol", "wz": "Complex van biologisch 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", } @staticmethod def _products() -> dict[str, OfficialVectorProduct]: share_warning = ( "PHAB-aandelen gelden voor het volledige bronpolygoon. Bij een gedeeltelijke selectie worden " "ze evenredig geschaald en blijven ze dus een oppervlakte-inschatting." ) products = ( OfficialVectorProduct( key="bwk_natura2000_2025", display_name="BWK en Natura 2000-habitatkaart 2025", theme="nature_value", provider="INBO / Digitaal Vlaanderen", source_name="inbo_bwk_natura2000", reference_layer_name="nature_value", service_type="WFS 2.0", collection="BWK:Bwkhab", source_crs="EPSG:31370", source_version="2025", observation_label="Toestand 2025", authority_level="authoritative", catalog_url=( "https://www.vlaanderen.be/datavindplaats/catalogus/" "biologische-waarderingskaart-en-natura-2000-habitatkaart-toestand-2025" ), attribution="Bron: INBO", license_note="Hergebruik volgens de open-datavoorwaarden en bronvermelding van INBO.", limitation_message=( "De BWK is een gebiedsdekkende kartering, geen terreinmeting op aanvraag. " "PHAB-oppervlakten zijn proportionele schattingen binnen bronpolygonen." ), source="INBO BWK WFS", observed_at=datetime(2025, 12, 10, tzinfo=UTC), valid_from=None, valid_to=None, primary_metric={ "metric_key": "nature_mapped_area", "method": "intersection_area", "label": "Gekarteerde natuuroppervlakte", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "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, }, ), endpoint_kind="bwk_wfs", ), OfficialVectorProduct( key="dov_soil_types", display_name="Digitale bodemkaart Vlaanderen - bodemtypes", theme="soil", provider="Databank Ondergrond Vlaanderen", source_name="dov_soil_map", reference_layer_name="soil", service_type="WFS 2.0", collection="bodemkaart:bodemtypes", source_crs="EPSG:31370", source_version="Digitale uitgave juni 2017", observation_label="Veldkartering 1949-1971", authority_level="authoritative_historical_baseline", catalog_url=( "https://www.vlaanderen.be/datavindplaats/catalogus/" "digitale-bodemkaart-van-het-vlaams-gewest-bodemtypes" ), attribution="Databank Ondergrond Vlaanderen - Digitale bodemkaart: bodemtypes", license_note="DOV-bronvermelding en de publieke GDI-hergebruikvoorwaarden zijn van toepassing.", limitation_message=( "Historische bodemkartering op schaal 1:20.000 op basis van veldwerk 1949-1971. " "De huidige drainage en lokale bodemtoestand kunnen afwijken; dit is geen terreinonderzoek." ), source="DOV WFS bodemtypes", observed_at=datetime(1971, 12, 31, 23, 59, 59, tzinfo=UTC), valid_from=datetime(1949, 1, 1, tzinfo=UTC), valid_to=datetime(1971, 12, 31, 23, 59, 59, tzinfo=UTC), primary_metric={ "metric_key": "soil_mapped_area", "method": "intersection_area", "label": "Bodemkaartoppervlakte", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "metric_key": "soil_dry_sand_area", "method": "intersection_area", "label": "Gekarteerd als droog zand", "unit": "ha", "geometry_dimension": 2, "filter_property": "soil_generalized_legend", "filter_values": ["Droog zand", "Zeer droog zand"], }, { "metric_key": "soil_moist_sand_area", "method": "intersection_area", "label": "Gekarteerd als vochtig zand", "unit": "ha", "geometry_dimension": 2, "filter_property": "soil_generalized_legend", "filter_values": ["Vochtig zand"], }, { "metric_key": "soil_wet_sand_area", "method": "intersection_area", "label": "Gekarteerd als nat zand", "unit": "ha", "geometry_dimension": 2, "filter_property": "soil_generalized_legend", "filter_values": ["Nat zand", "Zeer nat zand"], }, { "metric_key": "soil_anthropogenic_area", "method": "intersection_area", "label": "Antropogene bodemklasse", "unit": "ha", "geometry_dimension": 2, "filter_property": "soil_generalized_legend", "filter_values": ["Antropogeen"], }, ), endpoint_kind="dov_wfs", ), OfficialVectorProduct( key="spw_picc_buildings", display_name="PICC building footprints", theme="buildings", provider="Service public de Wallonie", source_name="spw_picc", reference_layer_name="buildings", service_type="ArcGIS REST", collection="11", source_crs="EPSG:3812", source_version="2026-07-11", observation_label="Weekly updated PICC snapshot", authority_level="authoritative", catalog_url=( "https://geoportail.wallonie.be/catalogue/" "b795de68-726c-4bdf-a62a-a42686aa5b6f.html" ), attribution="Service public de Wallonie (SPW) - PICC", license_note="CC BY 4.0; cite SPW PICC and identify modifications.", limitation_message=( "Topographic building footprints from PICC; these are not cadastral parcels " "or legal building registrations." ), source="SPW PICC ArcGIS REST", observed_at=None, valid_from=None, valid_to=None, primary_metric={ "metric_key": "building_footprint_area", "method": "intersection_area", "label": "Bebouwde voetafdruk", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "metric_key": "building_count", "method": "feature_count", "label": "Gebouwen", "unit": "objecten", "geometry_dimension": 2, }, ), coverage_zones=("wallonia",), endpoint_kind="spw_arcgis", identity_field="GEOREF_ID", requires_coverage_area=True, ), OfficialVectorProduct( key="spw_picc_roads", display_name="PICC road axes", theme="roads", provider="Service public de Wallonie", source_name="spw_picc", reference_layer_name="roads", service_type="ArcGIS REST", collection="21", source_crs="EPSG:3812", source_version="2026-07-11", observation_label="Weekly updated PICC snapshot", authority_level="authoritative", catalog_url=( "https://geoportail.wallonie.be/catalogue/" "b795de68-726c-4bdf-a62a-a42686aa5b6f.html" ), attribution="Service public de Wallonie (SPW) - PICC", license_note="CC BY 4.0; cite SPW PICC and identify modifications.", limitation_message=( "PICC road axes describe topographic road geometry and are not a routing " "network or a traffic measurement." ), source="SPW PICC ArcGIS REST", observed_at=None, valid_from=None, valid_to=None, primary_metric={ "metric_key": "road_length", "method": "intersection_length", "label": "Wegaslengte", "unit": "km", "geometry_dimension": 1, "is_estimate": False, }, selection_metrics=( { "metric_key": "road_segment_count", "method": "feature_count", "label": "Wegsegmenten", "unit": "objecten", "geometry_dimension": 1, }, ), geometry_types=("LineString", "MultiLineString"), coverage_zones=("wallonia",), endpoint_kind="spw_arcgis", identity_field="GEOREF_ID", requires_coverage_area=True, ), OfficialVectorProduct( key="spw_picc_waterways", display_name="PICC hydrographic axes", theme="water", provider="Service public de Wallonie", source_name="spw_picc", reference_layer_name="water", service_type="ArcGIS REST", collection="28", source_crs="EPSG:3812", source_version="2026-07-11", observation_label="Weekly updated PICC snapshot", authority_level="authoritative", catalog_url=( "https://geoportail.wallonie.be/catalogue/" "b795de68-726c-4bdf-a62a-a42686aa5b6f.html" ), attribution="Service public de Wallonie (SPW) - PICC", license_note="CC BY 4.0; cite SPW PICC and identify modifications.", limitation_message=( "Hydrographic axes describe mapped centre lines. They do not provide depth, " "discharge or water volume." ), source="SPW PICC ArcGIS REST", observed_at=None, valid_from=None, valid_to=None, primary_metric={ "metric_key": "waterway_length", "method": "intersection_length", "label": "Waterlooplengte", "unit": "km", "geometry_dimension": 1, "is_estimate": False, }, selection_metrics=( { "metric_key": "waterway_segment_count", "method": "feature_count", "label": "Waterloopsegmenten", "unit": "objecten", "geometry_dimension": 1, }, ), geometry_types=("LineString", "MultiLineString"), coverage_zones=("wallonia",), endpoint_kind="spw_arcgis", identity_field="GEOREF_ID", requires_coverage_area=True, ), OfficialVectorProduct( key="spw_picc_water_surfaces", display_name="PICC hydrographic surfaces", theme="water", provider="Service public de Wallonie", source_name="spw_picc", reference_layer_name="water", service_type="ArcGIS REST", collection="30", source_crs="EPSG:3812", source_version="2026-07-11", observation_label="Weekly updated PICC snapshot", authority_level="authoritative", catalog_url=( "https://geoportail.wallonie.be/catalogue/" "b795de68-726c-4bdf-a62a-a42686aa5b6f.html" ), attribution="Service public de Wallonie (SPW) - PICC", license_note="CC BY 4.0; cite SPW PICC and identify modifications.", limitation_message=( "Mapped hydrographic surface area is not water volume and does not imply " "a measured water level." ), source="SPW PICC ArcGIS REST", observed_at=None, valid_from=None, valid_to=None, primary_metric={ "metric_key": "water_surface_area", "method": "intersection_area", "label": "Wateroppervlakte", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "metric_key": "water_surface_count", "method": "feature_count", "label": "Wateroppervlakken", "unit": "objecten", "geometry_dimension": 2, }, ), coverage_zones=("wallonia",), endpoint_kind="spw_arcgis", identity_field="GEOREF_ID", requires_coverage_area=True, ), OfficialVectorProduct( key="urbis_buildings", display_name="UrbIS buildings", theme="buildings", provider="Paradigm Brussels", source_name="urbis", reference_layer_name="buildings", service_type="WFS 2.0", collection="urbisvector:Buildings", source_crs="EPSG:31370", source_version="2026-06-06", observation_label="UrbIS revision 6 June 2026", authority_level="authoritative", catalog_url=( "https://datastore.brussels/web/data/dataset/" "2cf42541-1813-11ef-8a81-00090ffe0001" ), attribution="Paradigm Brussels - UrbIS", license_note="Buildings are published under CC0.", limitation_message=( "UrbIS building geometry is a regional topographic reference and is not " "a legal cadastral registration." ), source="UrbIS WFS", observed_at=datetime(2026, 6, 6, tzinfo=UTC), valid_from=None, valid_to=None, primary_metric={ "metric_key": "building_footprint_area", "method": "intersection_area", "label": "Bebouwde voetafdruk", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "metric_key": "building_count", "method": "feature_count", "label": "Gebouwen", "unit": "objecten", "geometry_dimension": 2, }, ), coverage_zones=("brussels",), endpoint_kind="urbis_wfs", response_crs="EPSG:31370", identity_field="INSPIRE_ID", requires_coverage_area=True, ), OfficialVectorProduct( key="urbis_cadastral_parcels", display_name="UrbIS cadastral parcels", theme="parcels", provider="Paradigm Brussels / FPS Finance", source_name="urbis", reference_layer_name="parcels", service_type="WFS 2.0", collection="urbisvector:CadastralParcels", source_crs="EPSG:31370", source_version="2026-06-06", observation_label="UrbIS revision 6 June 2026", authority_level="authoritative", catalog_url=( "https://datastore.brussels/web/data/dataset/" "2cf42541-1813-11ef-8a81-00090ffe0001" ), attribution="Paradigm Brussels and FPS Finance - cadastral parcel plan", license_note=( "The FPS Finance open-data cadastral plan licence applies to cadastral parcels." ), limitation_message=( "Cadastral parcel geometry is reference data. GeoIntel does not infer " "ownership, rights or legal boundaries beyond the published source." ), source="UrbIS WFS", observed_at=datetime(2026, 1, 1, tzinfo=UTC), valid_from=None, valid_to=None, primary_metric={ "metric_key": "parcel_area", "method": "intersection_area", "label": "Perceeloppervlakte", "unit": "ha", "geometry_dimension": 2, "is_estimate": False, }, selection_metrics=( { "metric_key": "parcel_count", "method": "feature_count", "label": "Percelen", "unit": "objecten", "geometry_dimension": 2, }, ), coverage_zones=("brussels",), endpoint_kind="urbis_wfs", response_crs="EPSG:31370", identity_field="INSPIRE_ID", requires_coverage_area=True, ), ) return {product.key: product for product in products} @staticmethod def list_products() -> list[dict[str, Any]]: return [ OfficialVectorProductRead( key=product.key, display_name=product.display_name, theme=product.theme, provider=product.provider, source_name=product.source_name, reference_layer_name=product.reference_layer_name, service_type=product.service_type, collection=product.collection, geometry_types=list(product.geometry_types), source_crs=product.source_crs, source_version=product.source_version, observation_label=product.observation_label, authority_level=product.authority_level, catalog_url=product.catalog_url, attribution=product.attribution, license_note=product.license_note, limitation_message=product.limitation_message, coverage_zones=list(product.coverage_zones), ).model_dump() for product in OfficialVectorAcquisitionService._products().values() ] @staticmethod def _product(product_key: str) -> OfficialVectorProduct: product = OfficialVectorAcquisitionService._products().get(product_key.strip().lower()) if product is None: raise AppError( code="OFFICIAL_VECTOR_PRODUCT_NOT_SUPPORTED", message="Select a governed official vector product", details={"product_key": product_key}, status_code=422, ) return product @staticmethod def _polygonal(geometry: Any) -> Any | None: if geometry is None or geometry.is_empty: return None if not geometry.is_valid: geometry = make_valid(geometry) parts: list[Polygon] = [] def collect(item: Any) -> None: if item is None or item.is_empty: return if isinstance(item, Polygon): parts.append(item) elif isinstance(item, MultiPolygon): parts.extend(part for part in item.geoms if not part.is_empty) elif hasattr(item, "geoms"): for part in item.geoms: collect(part) collect(geometry) if not parts: return None result = unary_union(parts) if not result.is_valid: result = make_valid(result) return result if not result.is_empty and result.is_valid else None @staticmethod def _dimensional(geometry: Any, dimension: int) -> Any | None: if geometry is None or geometry.is_empty: return None if not geometry.is_valid: geometry = make_valid(geometry) parts: list[Any] = [] def collect(item: Any) -> None: if item is None or item.is_empty: return if dimension == 2 and isinstance(item, Polygon): parts.append(item) elif dimension == 2 and isinstance(item, MultiPolygon): parts.extend(part for part in item.geoms if not part.is_empty) elif dimension == 1 and isinstance(item, LineString): parts.append(item) elif dimension == 1 and isinstance(item, MultiLineString): parts.extend(part for part in item.geoms if not part.is_empty) elif hasattr(item, "geoms"): for part in item.geoms: collect(part) collect(geometry) if not parts: return None result = unary_union(parts) if not result.is_valid: result = make_valid(result) return result if not result.is_empty and result.is_valid else None @staticmethod def _validate_scope( db, project_id: UUID, payload: OfficialVectorAcquireRequest, settings: Settings, product: OfficialVectorProduct, ) -> tuple[Any, Any, list[float], list[float]]: if not settings.official_vector_enabled: raise AppError( code="OFFICIAL_VECTOR_NOT_CONFIGURED", message="Bounded official vector acquisition is disabled", status_code=503, ) if product.endpoint_kind == "spw_arcgis" and not settings.spw_picc_enabled: raise AppError( code="SPW_PICC_NOT_CONFIGURED", message="Bounded SPW PICC acquisition is disabled", status_code=503, ) if product.endpoint_kind == "urbis_wfs" and not settings.urbis_enabled: raise AppError( code="URBIS_NOT_CONFIGURED", message="Bounded UrbIS acquisition is disabled", status_code=503, ) if not db.get(Project, project_id): raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404) if payload.bbox.crs.upper() != "EPSG:4326": raise AppError( code="OFFICIAL_VECTOR_INVALID_CRS", message="Official vector acquisition requires EPSG:4326", status_code=400, ) values = (payload.bbox.min_x, payload.bbox.min_y, payload.bbox.max_x, payload.bbox.max_y) if ( not all(math.isfinite(value) for value in values) or values[0] >= values[2] or values[1] >= values[3] or values[0] < -180 or values[2] > 180 or values[1] < -90 or values[3] > 90 ): raise AppError( code="OFFICIAL_VECTOR_INVALID_BBOX", message="Bounding box is invalid for EPSG:4326", status_code=400, ) metric_bounds = _TO_LAMBERT72.transform_bounds(*values, densify_pts=21) width_m = metric_bounds[2] - metric_bounds[0] height_m = metric_bounds[3] - metric_bounds[1] if width_m < settings.official_vector_min_side_m or height_m < settings.official_vector_min_side_m: raise AppError( code="OFFICIAL_VECTOR_SELECTION_TOO_SMALL", message=f"Select at least {settings.official_vector_min_side_m:g} by " f"{settings.official_vector_min_side_m:g} metres", status_code=422, ) if width_m > settings.official_vector_max_side_m or height_m > settings.official_vector_max_side_m: raise AppError( code="OFFICIAL_VECTOR_SELECTION_TOO_LARGE", message=f"Select no more than {settings.official_vector_max_side_m:g} by " f"{settings.official_vector_max_side_m:g} metres", details={"width_m": width_m, "height_m": height_m}, status_code=422, ) scope_wgs84 = box(*values) area = None if payload.area_id: area = db.get(Area, payload.area_id) if area is None: raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404) if area.project_id != project_id: raise AppError( code="INVALID_DATASET_SCOPE", message="Area does not belong to this project", status_code=400, ) scope_wgs84 = OfficialVectorAcquisitionService._polygonal( scope_wgs84.intersection(to_shape(area.geometry)) ) if scope_wgs84 is None: raise AppError( code="OFFICIAL_VECTOR_SCOPE_EMPTY", message="The selection does not intersect the selected area", status_code=400, ) if product.requires_coverage_area: coverage_area_names = { "wallonia": "Wallonia", "brussels": "Brussels-Capital Region", } required_names = [ coverage_area_names[zone] for zone in product.coverage_zones if zone in coverage_area_names ] coverage_rows = ( [area] if area is not None and area.name in required_names else db.query(Area) .filter( Area.project_id == project_id, Area.name.in_(required_names), ) .all() ) coverage_geometries = [ to_shape(item.geometry) for item in coverage_rows if item is not None and item.geometry is not None ] coverage_geometry = ( OfficialVectorAcquisitionService._polygonal(unary_union(coverage_geometries)) if coverage_geometries else None ) if coverage_geometry is None: raise AppError( code="OFFICIAL_VECTOR_COVERAGE_NOT_READY", message="The official regional coverage boundary is not persisted in this project", details={"required_areas": required_names}, status_code=409, ) scope_wgs84 = OfficialVectorAcquisitionService._polygonal( scope_wgs84.intersection(coverage_geometry) ) if scope_wgs84 is None: raise AppError( code="OFFICIAL_VECTOR_OUTSIDE_COVERAGE", message="The selection does not intersect the official product coverage", details={"coverage_zones": list(product.coverage_zones)}, status_code=422, ) scope_metric = OfficialVectorAcquisitionService._polygonal( transform(_TO_LAMBERT72.transform, scope_wgs84) ) if scope_metric is None: raise AppError( code="OFFICIAL_VECTOR_SCOPE_INVALID", message="The selection could not be transformed to EPSG:31370", status_code=400, ) return scope_wgs84, scope_metric, [float(value) for value in values], [ float(value) for value in scope_metric.bounds ] @staticmethod def _read_page( url: str, settings: Settings, opener: Callable[..., Any] | None, ) -> tuple[dict[str, Any], str, int]: request = Request( url, headers={ "Accept": "application/geo+json, application/json", "User-Agent": "GeoIntel/1.0 bounded-official-vector-acquisition", }, ) try: with (opener or _NO_REDIRECT_OPENER.open)( request, timeout=settings.official_vector_timeout_seconds, ) as response: content_type = "" if hasattr(response, "getheader"): content_type = str(response.getheader("Content-Type") or "") elif hasattr(response, "headers"): content_type = str(response.headers.get("Content-Type") or "") if content_type and not any( allowed in content_type.lower() for allowed in ("application/json", "application/geo+json") ): raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_CONTENT_TYPE", message="The official vector provider returned an unsupported content type", details={"content_type": content_type}, status_code=502, ) limit = settings.official_vector_max_response_mb * 1024 * 1024 content = response.read(limit + 1) except HTTPError as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_HTTP_ERROR", message="The official vector provider returned an HTTP error", details={"status_code": exc.code}, status_code=502, ) from exc except (TimeoutError, URLError, OSError) as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_UNAVAILABLE", message="The official vector provider is unavailable", status_code=502, ) from exc if len(content) > limit: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_RESPONSE_TOO_LARGE", message="An official vector response page exceeded the configured limit", status_code=502, ) try: payload = json.loads(content.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official vector provider returned invalid GeoJSON", status_code=502, ) from exc if not isinstance(payload, dict) or payload.get("type") != "FeatureCollection": raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official vector provider returned a non-FeatureCollection response", status_code=502, ) return payload, hashlib.sha256(content).hexdigest(), len(content) @staticmethod def _nature_url( settings: Settings, bbox_values: tuple[float, ...], start_index: int, ) -> str: query = urlencode( { "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": "BWK:Bwkhab", "srsName": "EPSG:4326", "bbox": ",".join(f"{value:.8f}" for value in bbox_values) + ",EPSG:4326", "count": settings.official_vector_page_size, "startIndex": start_index, "sortBy": "UIDN", "outputFormat": "application/json", } ) return f"{settings.bwk_wfs_url.rstrip('?')}?{query}" @staticmethod def _soil_url(settings: Settings, metric_bbox: tuple[float, ...], start_index: int) -> str: query = urlencode( { "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": "bodemkaart:bodemtypes", "srsName": "EPSG:4326", "bbox": ",".join(f"{value:.3f}" for value in metric_bbox) + ",EPSG:31370", "count": settings.official_vector_page_size, "startIndex": start_index, "sortBy": "gid", "outputFormat": "application/json", } ) return f"{settings.dov_soil_wfs_url.rstrip('?')}?{query}" @staticmethod def _spw_url( settings: Settings, product: OfficialVectorProduct, bbox_values: tuple[float, ...], start_index: int, ) -> str: query = urlencode( { "where": "1=1", "geometry": ",".join(f"{value:.8f}" for value in bbox_values), "geometryType": "esriGeometryEnvelope", "inSR": "4326", "outSR": "4326", "spatialRel": "esriSpatialRelIntersects", "outFields": "*", "returnGeometry": "true", "returnZ": "false", "returnM": "false", "resultOffset": start_index, "resultRecordCount": min(settings.official_vector_page_size, 2000), "orderByFields": "OBJECTID", "f": "geojson", } ) base = settings.spw_picc_mapserver_url.rstrip("/") return f"{base}/{product.collection}/query?{query}" @staticmethod def _urbis_url( settings: Settings, product: OfficialVectorProduct, metric_bbox: tuple[float, ...], start_index: int, ) -> str: query = urlencode( { "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": product.collection, "srsName": "EPSG:31370", "bbox": ",".join(f"{value:.3f}" for value in metric_bbox) + ",EPSG:31370", "count": settings.official_vector_page_size, "startIndex": start_index, "sortBy": product.identity_field or "INSPIRE_ID", "outputFormat": "application/json", } ) return f"{settings.urbis_wfs_url.rstrip('?')}?{query}" @staticmethod def _page_url( product: OfficialVectorProduct, settings: Settings, scope_wgs84: Any, scope_metric: Any, start_index: int, ) -> str: if product.endpoint_kind == "bwk_wfs": return OfficialVectorAcquisitionService._nature_url( settings, tuple(scope_wgs84.bounds), start_index, ) if product.endpoint_kind == "dov_wfs": return OfficialVectorAcquisitionService._soil_url( settings, tuple(scope_metric.bounds), start_index, ) if product.endpoint_kind == "spw_arcgis": return OfficialVectorAcquisitionService._spw_url( settings, product, tuple(scope_wgs84.bounds), start_index, ) if product.endpoint_kind == "urbis_wfs": return OfficialVectorAcquisitionService._urbis_url( settings, product, tuple(scope_metric.bounds), start_index, ) raise AppError( code="OFFICIAL_VECTOR_PRODUCT_NOT_SUPPORTED", message="The official vector product has no governed acquisition adapter", status_code=422, ) @staticmethod def _validate_page_url( url: str, product: OfficialVectorProduct, settings: Settings, ) -> None: parsed = urlparse(url) configured_url = { "bwk_wfs": settings.bwk_wfs_url, "dov_wfs": settings.dov_soil_wfs_url, "spw_arcgis": settings.spw_picc_mapserver_url, "urbis_wfs": settings.urbis_wfs_url, }.get(product.endpoint_kind) if configured_url is None: raise AppError( code="OFFICIAL_VECTOR_PRODUCT_NOT_SUPPORTED", message="The official vector product has no configured endpoint", status_code=422, ) base = urlparse(configured_url) expected_path = ( f"{base.path.rstrip('/')}/{product.collection}/query" if product.endpoint_kind == "spw_arcgis" else base.path ) if ( parsed.scheme != "https" or base.scheme != "https" or parsed.netloc.casefold() != base.netloc.casefold() or parsed.path != expected_path or parsed.username or parsed.password or parsed.fragment ): raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_PAGINATION", message="The official vector request escaped the governed HTTPS endpoint", status_code=502, ) @staticmethod def _habitat_breakdown(properties: dict[str, Any]) -> tuple[list[dict[str, Any]], float, float, float]: entries: list[dict[str, Any]] = [] natura_share = regional_share = uncertain_share = 0.0 for index in range(1, 6): code = str(properties.get(f"HAB{index}") or "").strip() if not code: continue raw_share = properties.get(f"PHAB{index}") try: share = max(0.0, min(100.0, float(raw_share or 0))) except (TypeError, ValueError): share = 0.0 entries.append({"code": code, "share_percent": share}) lowered = code.lower() if re.match(r"^\d", code): natura_share += share elif lowered.startswith("rbb"): regional_share += share elif lowered.startswith("ohab"): uncertain_share += share if str(properties.get("HABLEGENDE") or "").strip().lower() == "ohab" and uncertain_share <= 0: uncertain_share = 100.0 return entries, min(100.0, natura_share), min(100.0, regional_share), min(100.0, uncertain_share) @staticmethod def _normalize_regional_feature( product: OfficialVectorProduct, feature: dict[str, Any], scope_metric: Any, coverage_scope: str, ) -> dict[str, Any] | None: dimension = 2 if any("Polygon" in item for item in product.geometry_types) else 1 try: source_geometry = shape(feature.get("geometry")) except Exception as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_GEOMETRY", message=f"{product.display_name} returned invalid geometry", status_code=502, ) from exc if product.response_crs == "EPSG:31370": source_metric = OfficialVectorAcquisitionService._dimensional( source_geometry, dimension, ) else: source_wgs84 = OfficialVectorAcquisitionService._dimensional( source_geometry, dimension, ) source_metric = ( OfficialVectorAcquisitionService._dimensional( transform(_TO_LAMBERT72.transform, source_wgs84), dimension, ) if source_wgs84 is not None else None ) if source_metric is None or not source_metric.intersects(scope_metric): return None clipped_metric = OfficialVectorAcquisitionService._dimensional( source_metric.intersection(scope_metric), dimension, ) if clipped_metric is None: return None clipped_wgs84 = OfficialVectorAcquisitionService._dimensional( transform(_TO_WGS84.transform, clipped_metric), dimension, ) if clipped_wgs84 is None: return None raw = dict(feature.get("properties") or {}) identity = ( raw.get(product.identity_field or "") or feature.get("id") or raw.get("OBJECTID") ) if identity in (None, ""): raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message=f"{product.display_name} returned a feature without an official identity", status_code=502, ) feature_id = f"{product.collection}:{identity}" properties = { **raw, "source_name": product.source_name, "source_collection": product.collection, "source_feature_id": feature_id, "reference_layer_name": product.reference_layer_name, "theme": product.theme, "authority_level": product.authority_level, "coverage_scope": coverage_scope, "coverage_zones": list(product.coverage_zones), "source_version": product.source_version, "attribution": product.attribution, "geometry_clipped_to_selection": not scope_metric.covers(source_metric), } if dimension == 2: properties["clipped_area_ha"] = round(float(clipped_metric.area) / 10_000.0, 8) else: properties["clipped_length_km"] = round(float(clipped_metric.length) / 1_000.0, 8) return { "type": "Feature", "id": feature_id, "geometry": mapping(clipped_wgs84), "properties": properties, } @staticmethod def _normalize_feature( product: OfficialVectorProduct, feature: dict[str, Any], scope_metric: Any, coverage_scope: str, ) -> dict[str, Any] | None: if product.endpoint_kind in {"spw_arcgis", "urbis_wfs"}: return OfficialVectorAcquisitionService._normalize_regional_feature( product, feature, scope_metric, coverage_scope, ) try: source_wgs84 = OfficialVectorAcquisitionService._polygonal(shape(feature.get("geometry"))) except Exception as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_GEOMETRY", message=f"{product.display_name} returned invalid geometry", status_code=502, ) from exc if source_wgs84 is None: return None source_metric = OfficialVectorAcquisitionService._polygonal( transform(_TO_LAMBERT72.transform, source_wgs84) ) if source_metric is None or not source_metric.intersects(scope_metric): return None clipped_metric = OfficialVectorAcquisitionService._polygonal(source_metric.intersection(scope_metric)) if clipped_metric is None or clipped_metric.area <= 0: return None clipped_wgs84 = OfficialVectorAcquisitionService._polygonal( transform(_TO_WGS84.transform, clipped_metric) ) if clipped_wgs84 is None: return None raw = dict(feature.get("properties") or {}) if product.theme == "nature_value": raw_id = str(feature.get("id") or raw.get("UIDN") or raw.get("OIDN") or "").strip() if not raw_id: raw_id = hashlib.sha256(json.dumps(feature.get("geometry"), sort_keys=True).encode()).hexdigest() feature_id = f"BWK:Bwkhab:{raw.get('UIDN') or raw_id}" evaluation = str(raw.get("EVAL") or "").strip().lower() habitats, natura_share, regional_share, uncertain_share = ( OfficialVectorAcquisitionService._habitat_breakdown(raw) ) area_ha = float(clipped_metric.area) / 10_000.0 properties = { **raw, "source_name": product.source_name, "source_collection": product.collection, "source_feature_id": feature_id, "reference_layer_name": product.reference_layer_name, "theme": product.theme, "authority_level": product.authority_level, "coverage_scope": coverage_scope, "source_version": product.source_version, "attribution": product.attribution, "bwk_evaluation_code": evaluation or "unknown", "bwk_evaluation_label": OfficialVectorAcquisitionService._BWK_EVALUATION_LABELS.get( evaluation, "Onbekende of ontbrekende BWK-waardering" ), "bwk_label": str(raw.get("BWKLABEL") or "").strip(), "bwk_units": ", ".join( str(raw.get(f"EENH{index}") or "").strip() for index in range(1, 9) if str(raw.get(f"EENH{index}") or "").strip() ), "habitat_entries": habitats, "clipped_area_ha": round(area_ha, 8), "natura2000_share_percent": natura_share, "regional_biotope_share_percent": regional_share, "uncertain_habitat_share_percent": uncertain_share, "natura2000_area_ha": round(area_ha * natura_share / 100.0, 8), "regional_biotope_area_ha": round(area_ha * regional_share / 100.0, 8), "uncertain_habitat_area_ha": round(area_ha * uncertain_share / 100.0, 8), "geometry_clipped_to_selection": not scope_metric.covers(source_metric), } else: gid = raw.get("gid") map_polygon_id = raw.get("id_kaartvlak") feature_id = str(feature.get("id") or f"{product.collection}:{gid or map_polygon_id}").strip() if not feature_id: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="DOV returned a soil polygon without an official identity", status_code=502, ) properties = { **raw, "source_name": product.source_name, "source_collection": product.collection, "source_feature_id": feature_id, "source_gid": gid, "source_map_polygon_id": map_polygon_id, "reference_layer_name": product.reference_layer_name, "theme": product.theme, "authority_level": product.authority_level, "coverage_scope": coverage_scope, "source_version": product.source_version, "survey_period": "1949-1971", "soil_type_code": raw.get("Bodemtype"), "unified_soil_type_code": raw.get("Unibodemtype"), "soil_series_code": raw.get("Bodemserie"), "soil_series_description": raw.get("Beknopte_omschrijving_bodemserie"), "soil_generalized_legend": raw.get("Gegeneraliseerde_legende"), "soil_texture_class_code": raw.get("Textuurklasse_code"), "soil_texture_class": raw.get("Textuurklasse"), "soil_drainage_class_code": raw.get("Drainageklasse_code"), "soil_drainage_class": raw.get("Drainageklasse"), "soil_profile_group_code": raw.get("Profielontwikkelingsgroep_code"), "soil_profile_group": raw.get("Profielontwikkelingsgroep"), "soil_substrate_code": raw.get("Substraat_code"), "soil_substrate": raw.get("Substraat_Vlaanderen") or raw.get("Substraat_legende"), "soil_region": raw.get("Streek"), "classification_type": raw.get("Type_classificatie"), "soil_map_title": raw.get("Eenduidige_legende_titel"), "clipped_area_ha": round(float(clipped_metric.area) / 10_000.0, 8), "attribution": product.attribution, "geometry_clipped_to_selection": not scope_metric.covers(source_metric), "historical_drainage_limitation": ( "Drainage class derives from field data collected between 1949 and 1971 and may differ today." ), } return { "type": "Feature", "id": feature_id, "geometry": mapping(clipped_wgs84), "properties": properties, } @staticmethod def _fetch_features( product: OfficialVectorProduct, scope_wgs84: Any, scope_metric: Any, coverage_scope: str, settings: Settings, opener: Callable[..., Any] | None, ) -> tuple[list[dict[str, Any]], dict[str, Any]]: retained: list[dict[str, Any]] = [] seen_ids: set[str] = set() request_urls: list[str] = [] response_hashes: list[str] = [] total_bytes = candidate_count = 0 expected_total: int | None = None next_url = OfficialVectorAcquisitionService._page_url( product, settings, scope_wgs84, scope_metric, 0, ) start_index = 0 seen_pages: set[str] = set() while next_url: OfficialVectorAcquisitionService._validate_page_url( next_url, product, settings, ) if next_url in seen_pages: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_PAGINATION_LOOP", message="The official provider repeated a pagination URL", status_code=502, ) if len(request_urls) >= settings.official_vector_max_pages: raise AppError( code="OFFICIAL_VECTOR_SELECTION_TOO_LARGE", message="Official vector acquisition exceeded the configured page limit", status_code=422, ) seen_pages.add(next_url) payload, response_hash, response_size = OfficialVectorAcquisitionService._read_page( next_url, settings, opener ) request_urls.append(next_url) response_hashes.append(response_hash) total_bytes += response_size if total_bytes > settings.official_vector_max_total_response_mb * 1024 * 1024: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_RESPONSE_TOO_LARGE", message="The complete official vector response exceeded the configured transfer limit", status_code=502, ) source_features = payload.get("features") if not isinstance(source_features, list): raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official FeatureCollection has no feature list", status_code=502, ) raw_matched = payload.get("numberMatched", payload.get("totalFeatures")) if raw_matched not in (None, "unknown"): try: matched = int(raw_matched) except (TypeError, ValueError) as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official WFS returned an invalid numberMatched value", status_code=502, ) from exc if expected_total is None: expected_total = matched elif expected_total != matched: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_UNSTABLE_PAGINATION", message="The official WFS numberMatched changed during pagination", status_code=502, ) for source_feature in source_features: candidate_count += 1 if not isinstance(source_feature, dict): continue normalized = OfficialVectorAcquisitionService._normalize_feature( product, source_feature, scope_metric, coverage_scope ) if normalized is None or normalized["id"] in seen_ids: continue seen_ids.add(normalized["id"]) if len(retained) >= settings.official_vector_max_features: raise AppError( code="OFFICIAL_VECTOR_SELECTION_TOO_LARGE", message="The selection exceeds the configured feature limit; draw a smaller rectangle", details={"max_features": settings.official_vector_max_features}, status_code=422, ) retained.append(normalized) returned = payload.get("numberReturned", len(source_features)) try: returned_count = int(returned) except (TypeError, ValueError) as exc: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official WFS returned an invalid numberReturned value", status_code=502, ) from exc if returned_count != len(source_features): raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INVALID_RESPONSE", message="The official WFS numberReturned does not match its feature payload", status_code=502, ) start_index += returned_count arcgis_has_more = payload.get("exceededTransferLimit") is True if product.endpoint_kind == "spw_arcgis": if arcgis_has_more and returned_count == 0: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INCOMPLETE_RESPONSE", message="The SPW provider reported more records but returned an empty page", status_code=502, ) next_url = ( OfficialVectorAcquisitionService._page_url( product, settings, scope_wgs84, scope_metric, start_index, ) if arcgis_has_more else None ) elif returned_count == 0 or ( expected_total is not None and start_index >= expected_total ) or (expected_total is None and returned_count < settings.official_vector_page_size): if expected_total is not None and start_index != expected_total: raise AppError( code="OFFICIAL_VECTOR_PROVIDER_INCOMPLETE_RESPONSE", message="The official WFS did not return every matched feature", details={"received": start_index, "expected": expected_total}, status_code=502, ) next_url = None else: next_url = OfficialVectorAcquisitionService._page_url( product, settings, scope_wgs84, scope_metric, start_index, ) return retained, { "candidate_feature_count": candidate_count, "feature_count": len(retained), "page_count": len(request_urls), "request_urls": request_urls, "response_sha256": response_hashes, "response_size_bytes": total_bytes, "reference_truncated": False, } @staticmethod def _cached_dataset( db, project_id: UUID, product: OfficialVectorProduct, request_hash: str, settings: Settings, ) -> Dataset | None: if settings.official_vector_cache_ttl_hours <= 0: return None candidates = ( db.query(Dataset) .filter( Dataset.project_id == project_id, Dataset.source_name == product.source_name, Dataset.reference_layer_name == product.reference_layer_name, Dataset.status == "ready", ) .order_by(Dataset.imported_at.desc()) .all() ) cutoff = datetime.now(UTC) - timedelta(hours=settings.official_vector_cache_ttl_hours) for candidate in candidates: provenance = candidate.provenance_metadata if isinstance(candidate.provenance_metadata, dict) else {} if ( provenance.get("request_hash") == request_hash and candidate.storage_path and Path(candidate.storage_path).is_file() and candidate.imported_at is not None and candidate.imported_at >= cutoff ): return candidate return None @staticmethod def _result( dataset: Dataset, product: OfficialVectorProduct, *, reused: bool, bbox_values: list[float], ) -> dict[str, Any]: source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {} provenance = dataset.provenance_metadata if isinstance(dataset.provenance_metadata, dict) else {} metadata = dataset.metadata_json if isinstance(dataset.metadata_json, dict) else {} return OfficialVectorAcquisitionResult( output_dataset_id=dataset.id, reused=reused, product_key=product.key, display_name=product.display_name, theme=product.theme, provider=product.provider, source_name=product.source_name, reference_layer_name=product.reference_layer_name, service_type=product.service_type, collection=product.collection, feature_count=int(metadata.get("feature_count", source_metadata.get("feature_count", 0))), candidate_feature_count=int(provenance.get("candidate_feature_count", 0)), page_count=int(provenance.get("page_count", 0)), bbox_epsg4326=bbox_values, source_version=str(dataset.source_version or product.source_version), attribution=product.attribution, limitation_message=product.limitation_message, ).model_dump(mode="json") @staticmethod def acquire( db, project_id: UUID, payload: OfficialVectorAcquireRequest, *, settings: Settings | None = None, opener: Callable[..., Any] | None = None, ) -> dict[str, Any]: resolved_settings = settings or get_settings() product = OfficialVectorAcquisitionService._product(payload.product_key) scope_wgs84, scope_metric, bbox_values, metric_bounds = ( OfficialVectorAcquisitionService._validate_scope( db, project_id, payload, resolved_settings, product ) ) request_identity = { "product_key": product.key, "bbox_epsg4326": [round(value, 8) for value in bbox_values], "area_id": str(payload.area_id) if payload.area_id else None, "source_version": product.source_version, } request_hash = hashlib.sha256( json.dumps(request_identity, sort_keys=True).encode() ).hexdigest() if not payload.force_refresh: cached = OfficialVectorAcquisitionService._cached_dataset( db, project_id, product, request_hash, resolved_settings ) if cached is not None: return OfficialVectorAcquisitionService._result( cached, product, reused=True, bbox_values=bbox_values ) area = db.get(Area, payload.area_id) if payload.area_id else None coverage_scope = ( product.coverage_zones[0] if product.requires_coverage_area else ( "municipality" if area is not None and area.name.strip().lower().startswith("gemeente ") else "bounded_selection" ) ) features, transfer = OfficialVectorAcquisitionService._fetch_features( product, scope_wgs84, scope_metric, coverage_scope, resolved_settings, opener, ) acquired_at = datetime.now(UTC) artifact = json.dumps( { "type": "FeatureCollection", "name": product.display_name, "crs": {"type": "name", "properties": {"name": "EPSG:4326"}}, "features": features, }, ensure_ascii=False, separators=(",", ":"), ).encode("utf-8") filename = ( f"{product.source_name}_{product.key}_{request_hash[:12]}.geojson" ) source_metadata = { "provider": product.provider, "service": product.service_type, "product_key": product.key, "product_display_name": product.display_name, "source_collection": product.collection, "authority_level": product.authority_level, "theme": product.theme, "layer_type": product.reference_layer_name, "coverage_scope": coverage_scope, "coverage_zones": list(product.coverage_zones), "geometry_clipped_to_area": payload.area_id is not None, "geometry_clipped_to_selection": True, "bbox_epsg4326": bbox_values, "bbox_epsg31370": metric_bounds, "feature_count": len(features), "identity_stable": True, "source_storage_crs": product.source_crs, "persisted_crs": "EPSG:4326", "selection_aggregation": product.primary_metric, "selection_metrics": list(product.selection_metrics), "attribution": product.attribution, "license_note": product.license_note, "catalog_url": product.catalog_url, "limitation_message": product.limitation_message, } if product.theme == "soil": source_metadata.update( { "survey_period": "1949-1971", "source_scale": "1:20,000", "semantic_metrics": False, } ) provenance_metadata = { "acquisition": f"explicit_bounded_{product.service_type.lower().replace(' ', '_')}", "acquired_at": acquired_at.isoformat(), "request_hash": request_hash, "request_urls": transfer["request_urls"], "response_sha256": transfer["response_sha256"], "response_size_bytes": transfer["response_size_bytes"], "page_count": transfer["page_count"], "candidate_feature_count": transfer["candidate_feature_count"], "exact_feature_count": transfer["feature_count"], "reference_truncated": False, "artifact_sha256": hashlib.sha256(artifact).hexdigest(), "clipped_to_area_id": str(payload.area_id) if payload.area_id else None, "scope_geometry_type": scope_wgs84.geom_type, "catalog_url": product.catalog_url, "limitation_message": product.limitation_message, } try: dataset_response = DatasetService.import_vector_bytes( db, project_id=project_id, area_id=payload.area_id, filename=filename, content=artifact, source=product.source, source_name=product.source_name, dataset_role="reference", reference_layer_name=product.reference_layer_name, temporal_series_key=f"{product.source_name}:{product.key}:{request_hash[:24]}", observed_at=product.observed_at or acquired_at, valid_from=product.valid_from, valid_to=product.valid_to, temporal_granularity="period" if product.valid_from else "snapshot", source_version=product.source_version, source_metadata=source_metadata, provenance_metadata=provenance_metadata, ) except AppError: raise except Exception as exc: raise AppError( code="OFFICIAL_VECTOR_PERSISTENCE_FAILED", message="The validated official vector selection could not be persisted", details={"reason": str(exc)}, status_code=500, ) from exc persisted = db.get(Dataset, dataset_response.id) if persisted is None: raise AppError( code="OFFICIAL_VECTOR_PERSISTENCE_FAILED", message="The persisted official vector dataset could not be reloaded", status_code=500, ) return OfficialVectorAcquisitionService._result( persisted, product, reused=False, bbox_values=bbox_values )