feat: complete Wallonia land cover and terrain sources
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-22 06:28:46 +02:00
parent d45d34186d
commit cee6cd05ae
46 changed files with 2542 additions and 276 deletions
+240 -36
View File
@@ -16,7 +16,10 @@ from app.core.config import Settings
from app.db.session import get_db
from app.main import app
from app.models import Dataset, Job, Project
from app.schemas.thematic_raster import ThematicRasterAcquireRequest, ThematicRasterSelectionRequest
from app.schemas.thematic_raster import (
ThematicRasterAcquireRequest,
ThematicRasterSelectionRequest,
)
from app.schemas.temporal import TemporalComparisonRequest
from app.services.dataset_service import DatasetService
from app.services.temporal_analysis_service import TemporalAnalysisService
@@ -24,8 +27,12 @@ from app.services.walous_land_cover_service import WalousLandCoverService
def load_provisioner():
path = Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
spec = importlib.util.spec_from_file_location("walous_source_provisioner_test", path)
path = (
Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
)
spec = importlib.util.spec_from_file_location(
"walous_source_provisioner_test", path
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
@@ -54,7 +61,14 @@ class FakeSession:
return self.project
if model is Dataset and self.dataset is not None and row_id == self.dataset.id:
return self.dataset
match = next((item for item in self.added if isinstance(item, model) and item.id == row_id), None)
match = next(
(
item
for item in self.added
if isinstance(item, model) and item.id == row_id
),
None,
)
if match is not None:
return match
return None
@@ -80,12 +94,13 @@ def make_source(
*,
dtype: str = "uint8",
nodata: int = 255,
class_codes: list[int] | None = None,
) -> tuple[list[float], np.ndarray]:
to_3812 = Transformer.from_crs("EPSG:4326", "EPSG:3812", always_xy=True)
to_4326 = Transformer.from_crs("EPSG:3812", "EPSG:4326", always_xy=True)
x, y = to_3812.transform(4.85, 50.45)
transform = from_origin(x, y + 100, 1, 1)
class_codes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
class_codes = class_codes or [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
values = np.empty((100, len(class_codes) * 20), dtype=dtype)
for index, class_code in enumerate(class_codes):
values[:, index * 20 : (index + 1) * 20] = class_code
@@ -118,20 +133,40 @@ def settings(source_dir: Path) -> Settings:
def test_walous_registry_reports_real_provisioning_state(tmp_path: Path) -> None:
before = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
before = {
item["key"]: item
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
}
assert before["walous_land_cover_2023"]["status"] == "source_not_provisioned"
make_source(tmp_path / "walous_land_cover_2023_3812.tif")
after = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
after = {
item["key"]: item
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
}
assert after["walous_land_cover_2023"]["configured"] is True
assert after["walous_land_cover_2023"]["source_crs"] == "EPSG:3812"
assert after["walous_land_cover_2023"]["native_resolution_m"] == 1.0
assert after["walous_land_cover_2023"]["analysis_resolution_m"] == 10.0
assert after["walous_land_cover_2023"]["coverage_zones"] == ["wallonia"]
assert after["walous_land_cover_2023"]["included_source_values"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
assert after["walous_land_cover_2023"]["included_source_values"] == [
1,
2,
3,
4,
5,
6,
7,
8,
9,
80,
90,
]
assert after["walous_land_cover_2023"]["source_value_unit"] == "walous_class_code"
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path: Path) -> None:
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(
tmp_path: Path,
) -> None:
source_path = tmp_path / "walous_land_cover_2023_3812.tif"
make_source(source_path)
@@ -140,7 +175,86 @@ def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path
assert validation["sample_classes"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path: Path, monkeypatch) -> None:
def test_walous_2018_registry_and_provisioner_accept_official_stacked_classes(
tmp_path: Path,
) -> None:
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
source_path = tmp_path / "walous_land_cover_2018_3812.tif"
make_source(source_path, class_codes=source_codes)
validation = load_provisioner().validate_raster(source_path)
registry = {
item["key"]: item
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
}
assert validation["sample_classes"] == source_codes
assert validation["implicit_source_nodata_values"] == [0]
assert registry["walous_land_cover_2018"]["configured"] is True
assert registry["walous_land_cover_2018"]["observation_year"] == 2018
assert "crosswalk" in registry["walous_land_cover_2018"]["limitation_message"]
def test_walous_2018_acquisition_normalizes_stacked_classes_with_explicit_provenance(
tmp_path: Path, monkeypatch
) -> None:
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
bbox, _values = make_source(
tmp_path / "walous_land_cover_2018_3812.tif",
class_codes=source_codes,
)
project = Project(id=uuid4(), name="Belgium")
captured = {}
def persist(_db, **kwargs):
captured.update(kwargs)
return SimpleNamespace(id=uuid4())
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
result = WalousLandCoverService.acquire(
FakeSession(project),
project.id,
ThematicRasterAcquireRequest(
bbox={
"min_x": bbox[0],
"min_y": bbox[1],
"max_x": bbox[2],
"max_y": bbox[3],
"crs": "EPSG:4326",
},
product_key="walous_land_cover_2018",
force_refresh=True,
),
settings=settings(tmp_path),
)
assert result["observation_year"] == 2018
assert captured["source_metadata"]["classes_present"] == [
1,
2,
3,
4,
5,
6,
7,
8,
9,
80,
90,
]
assert captured["source_metadata"]["source_classes_present"] == source_codes
assert captured["source_metadata"]["class_crosswalk"][62] == 2
assert captured["source_metadata"]["class_crosswalk"][0] == 255
assert (
captured["source_metadata"]["attribution"]
== "Service public de Wallonie (SPW), UCLouvain, ULB, ISSeP"
)
assert captured["observed_at"].date().isoformat() == "2018-12-31"
def test_walous_acquisition_reads_real_classes_and_persists_provenance(
tmp_path: Path, monkeypatch
) -> None:
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
project = Project(id=uuid4(), name="Belgium")
db = FakeSession(project)
@@ -156,7 +270,13 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
db,
project.id,
ThematicRasterAcquireRequest(
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
bbox={
"min_x": bbox[0],
"min_y": bbox[1],
"max_x": bbox[2],
"max_y": bbox[3],
"crs": "EPSG:4326",
},
product_key="walous_land_cover_2023",
force_refresh=True,
),
@@ -166,7 +286,19 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
assert result["output_dataset_id"] == str(output_id)
assert result["resolution_m"] == 10
assert captured["source_name"] == "spw_walous_land_cover"
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
assert captured["source_metadata"]["classes_present"] == [
1,
2,
3,
4,
5,
6,
7,
8,
9,
80,
90,
]
assert captured["provenance_metadata"]["resampling"] == "nearest"
assert captured["temporal_series_key"].startswith("spw:walous:land-cover:")
assert captured["observed_at"].date().isoformat() == "2023-06-25"
@@ -174,7 +306,9 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
assert captured["valid_to"] == captured["observed_at"]
def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path, monkeypatch) -> None:
def test_walous_acquisition_accepts_official_signed_int8_nodata(
tmp_path: Path, monkeypatch
) -> None:
bbox, _values = make_source(
tmp_path / "walous_land_cover_2023_3812.tif",
dtype="int8",
@@ -193,7 +327,13 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
FakeSession(project),
project.id,
ThematicRasterAcquireRequest(
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
bbox={
"min_x": bbox[0],
"min_y": bbox[1],
"max_x": bbox[2],
"max_y": bbox[3],
"crs": "EPSG:4326",
},
product_key="walous_land_cover_2023",
force_refresh=True,
),
@@ -201,14 +341,28 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
)
assert result["output_dataset_id"] == str(output_id)
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
assert captured["source_metadata"]["classes_present"] == [
1,
2,
3,
4,
5,
6,
7,
8,
9,
80,
90,
]
with rasterio.MemoryFile(captured["content"]) as memory:
with memory.open() as derived:
assert derived.dtypes == ("uint8",)
assert derived.nodata == 255
def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypatch) -> None:
def test_walous_analysis_returns_semantic_area_metrics(
tmp_path: Path, monkeypatch
) -> None:
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
project = Project(id=uuid4(), name="Belgium")
output_id = uuid4()
@@ -221,7 +375,13 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
db = FakeSession(project)
payload = ThematicRasterAcquireRequest(
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
bbox={
"min_x": bbox[0],
"min_y": bbox[1],
"max_x": bbox[2],
"max_y": bbox[3],
"crs": "EPSG:4326",
},
product_key="walous_land_cover_2023",
force_refresh=True,
)
@@ -247,7 +407,10 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
output_id,
ThematicRasterSelectionRequest(bbox=payload.bbox),
)
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
metrics = {
item["metric_key"]: item["metric_value"]
for item in result["summary"]["metrics"]
}
assert result["metric_kind"] == "categorical_area"
assert metrics["land_cover_observed_area_ha"] > 0
@@ -270,7 +433,10 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
dataset_type="raster",
source="SPW WALOUS",
source_name="spw_walous_land_cover",
source_metadata={"product_key": "walous_land_cover_2023", "bbox_epsg4326": bbox},
source_metadata={
"product_key": "walous_land_cover_2023",
"bbox_epsg4326": bbox,
},
storage_path=str(tmp_path / "walous_land_cover_2023_3812.tif"),
status="ready",
)
@@ -281,7 +447,9 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
assert rendered.startswith(b"\x89PNG\r\n\x1a\n")
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch) -> None:
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(
monkeypatch,
) -> None:
project_id = uuid4()
earlier = Dataset(
id=uuid4(),
@@ -320,14 +488,16 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
"metric_unit": "ha",
"aggregation_method": "nearest_resampled_cells_times_cell_area",
"primary_metric_key": "land_cover_observed_area_ha",
"metrics": [{
"metric_key": "land_cover_observed_area_ha",
"metric_label": "Gekarteerde landbedekking",
"metric_value": value,
"metric_unit": "ha",
"aggregation_method": "nearest_resampled_cells_times_cell_area",
"is_estimate": True,
}],
"metrics": [
{
"metric_key": "land_cover_observed_area_ha",
"metric_label": "Gekarteerde landbedekking",
"metric_value": value,
"metric_unit": "ha",
"aggregation_method": "nearest_resampled_cells_times_cell_area",
"is_estimate": True,
}
],
},
"limitation_message": "Cell-based estimate.",
}
@@ -336,10 +506,18 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
payload = TemporalComparisonRequest(
earlier_dataset_id=earlier.id,
later_dataset_id=later.id,
bbox={"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
bbox={
"min_x": 4.8,
"min_y": 50.4,
"max_x": 4.9,
"max_y": 50.5,
"crs": "EPSG:4326",
},
)
result = TemporalAnalysisService.compare(TemporalSession(), project_id=project_id, payload=payload)
result = TemporalAnalysisService.compare(
TemporalSession(), project_id=project_id, payload=payload
)
assert result.metric.earlier_value == 4.0
assert result.metric.later_value == 5.5
@@ -354,7 +532,10 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
monkeypatch.setattr(
WalousLandCoverService,
"acquire",
lambda *_args, **_kwargs: {"output_dataset_id": str(dataset_id), "provider": WalousLandCoverService.PROVIDER},
lambda *_args, **_kwargs: {
"output_dataset_id": str(dataset_id),
"provider": WalousLandCoverService.PROVIDER,
},
)
monkeypatch.setattr(
WalousLandCoverService,
@@ -364,7 +545,13 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
"product_key": "walous_land_cover_2023",
"theme": "land_cover_use",
"metric_kind": "categorical_area",
"selection_bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
"selection_bbox": {
"min_x": 4.8,
"min_y": 50.4,
"max_x": 4.9,
"max_y": 50.5,
"crs": "EPSG:4326",
},
"selected_cell_count": 100,
"valid_cell_count": 100,
"coverage_ratio": 1.0,
@@ -390,20 +577,37 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
acquisition = client.post(
f"/api/v1/projects/{project.id}/datasets/walous/acquire",
json={
"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
"bbox": {
"min_x": 4.8,
"min_y": 50.4,
"max_x": 4.9,
"max_y": 50.5,
"crs": "EPSG:4326",
},
"product_key": "walous_land_cover_2023",
},
)
selection = client.post(
f"/api/v1/projects/{project.id}/datasets/{dataset_id}/raster/walous/select",
json={"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"}},
json={
"bbox": {
"min_x": 4.8,
"min_y": 50.4,
"max_x": 4.9,
"max_y": 50.5,
"crs": "EPSG:4326",
}
},
)
finally:
app.dependency_overrides.clear()
assert products.status_code == 200 and set(products.json()) == {"data"}
assert products.json()["data"]["total"] == 2
assert products.json()["data"]["total"] == 3
assert acquisition.status_code == 200 and set(acquisition.json()) == {"data"}
assert acquisition.json()["data"]["job_type"] == "raster.walous.acquire"
assert selection.status_code == 200 and selection.json()["data"]["theme"] == "land_cover_use"
assert (
selection.status_code == 200
and selection.json()["data"]["theme"] == "land_cover_use"
)
assert any(isinstance(item, Job) for item in db.added)