mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
Merge PR #69: wfigs_perimeters true polygons via Geo.geometry (v0.9.8)
feat(wfigs_perimeters): emit true polygons via Geo.geometry (v0.9.8)
This commit is contained in:
commit
00a450b22f
2 changed files with 80 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
"""WFIGS Perimeters adapter for wildfire perimeter polygons."""
|
"""WFIGS Perimeters adapter for wildfire perimeter polygons."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
|
@ -39,6 +40,19 @@ from central.models import Event, Geo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_geometry(geometry: Any) -> dict[str, Any] | None:
|
||||||
|
"""Coerce upstream geometry to a GeoJSON dict for Geo.geometry; None if absent/malformed."""
|
||||||
|
if isinstance(geometry, dict):
|
||||||
|
return geometry
|
||||||
|
if isinstance(geometry, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(geometry)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
LAYER_NAME = "perimeters"
|
LAYER_NAME = "perimeters"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -305,6 +319,7 @@ class WFIGSPerimetersAdapter(SourceAdapter):
|
||||||
geo = Geo(
|
geo = Geo(
|
||||||
centroid=centroid,
|
centroid=centroid,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
|
geometry=_as_geometry(geometry),
|
||||||
regions=regions,
|
regions=regions,
|
||||||
primary_region=primary_region,
|
primary_region=primary_region,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,20 @@ class TestWFIGSIncidentsAdapter:
|
||||||
assert adapter.region.south == 35.0
|
assert adapter.region.south == 35.0
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_PERIMETERS_MULTIPOLYGON = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {"type": "MultiPolygon", "coordinates": [
|
||||||
|
[[[-113.6, 48.4], [-113.4, 48.4], [-113.4, 48.6], [-113.6, 48.4]]],
|
||||||
|
[[[-114.0, 48.0], [-113.8, 48.0], [-113.8, 48.2], [-114.0, 48.0]]]]},
|
||||||
|
"properties": {"attr_IrwinID": "GUID-002-MULTI", "attr_IncidentTypeCategory": "WF",
|
||||||
|
"attr_POOState": "US-MT", "attr_POOCounty": "Glacier",
|
||||||
|
"attr_FireDiscoveryDateTime": 1716000000000},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestWFIGSPerimetersAdapter:
|
class TestWFIGSPerimetersAdapter:
|
||||||
"""Tests for WFIGS Perimeters adapter."""
|
"""Tests for WFIGS Perimeters adapter."""
|
||||||
|
|
||||||
|
|
@ -532,3 +546,54 @@ class TestWFIGSPerimetersAdapter:
|
||||||
subject = adapter.subject_for(event)
|
subject = adapter.subject_for(event)
|
||||||
# Subject uses normalized state: mt.glacier not us-mt.glacier
|
# Subject uses normalized state: mt.glacier not us-mt.glacier
|
||||||
assert subject == "central.fire.perimeter.mt.glacier"
|
assert subject == "central.fire.perimeter.mt.glacier"
|
||||||
|
|
||||||
|
async def _poll_once(self, adapter, response):
|
||||||
|
resp = AsyncMock()
|
||||||
|
resp.raise_for_status = MagicMock()
|
||||||
|
resp.json = AsyncMock(return_value=response)
|
||||||
|
with patch.object(adapter._session, "get", return_value=AsyncMock(__aenter__=AsyncMock(return_value=resp), __aexit__=AsyncMock())):
|
||||||
|
return [e async for e in adapter.poll()]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_geometry_field_set_from_upstream_polygon(self, mock_config, mock_config_store, cursor_db_path):
|
||||||
|
"""geo.geometry carries the full upstream Polygon (v0.9.3 framework)."""
|
||||||
|
from central.adapters.wfigs_perimeters import WFIGSPerimetersAdapter
|
||||||
|
adapter = WFIGSPerimetersAdapter(mock_config, mock_config_store, cursor_db_path)
|
||||||
|
await adapter.startup()
|
||||||
|
events = await self._poll_once(adapter, SAMPLE_PERIMETERS_RESPONSE)
|
||||||
|
await adapter.shutdown()
|
||||||
|
geom = events[0].geo.geometry
|
||||||
|
assert geom is not None and geom["type"] == "Polygon"
|
||||||
|
assert geom["coordinates"] == SAMPLE_PERIMETERS_RESPONSE["features"][0]["geometry"]["coordinates"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_geometry_field_set_from_multipolygon(self, mock_config, mock_config_store, cursor_db_path):
|
||||||
|
"""geo.geometry carries a MultiPolygon intact (archive ST_GeomFromGeoJSON handles it)."""
|
||||||
|
from central.adapters.wfigs_perimeters import WFIGSPerimetersAdapter
|
||||||
|
adapter = WFIGSPerimetersAdapter(mock_config, mock_config_store, cursor_db_path)
|
||||||
|
await adapter.startup()
|
||||||
|
events = await self._poll_once(adapter, SAMPLE_PERIMETERS_MULTIPOLYGON)
|
||||||
|
await adapter.shutdown()
|
||||||
|
geom = events[0].geo.geometry
|
||||||
|
assert geom is not None and geom["type"] == "MultiPolygon"
|
||||||
|
assert len(geom["coordinates"]) == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fall_off_geometry_stays_none(self, mock_config, mock_config_store, cursor_db_path):
|
||||||
|
"""Synthetic removal events carry no geometry."""
|
||||||
|
from central.adapters.wfigs_perimeters import WFIGSPerimetersAdapter
|
||||||
|
adapter = WFIGSPerimetersAdapter(mock_config, mock_config_store, cursor_db_path)
|
||||||
|
await adapter.startup()
|
||||||
|
await self._poll_once(adapter, SAMPLE_PERIMETERS_RESPONSE)
|
||||||
|
events = await self._poll_once(adapter, {"type": "FeatureCollection", "features": []})
|
||||||
|
await adapter.shutdown()
|
||||||
|
removed = [e for e in events if e.category == "fire.perimeter.removed"]
|
||||||
|
assert removed and removed[0].geo.geometry is None
|
||||||
|
|
||||||
|
def test_geometry_coercion_handles_malformed_and_none(self):
|
||||||
|
"""_as_geometry: dict passthrough, JSON-string coercion, malformed/absent -> None."""
|
||||||
|
from central.adapters.wfigs_perimeters import _as_geometry
|
||||||
|
assert _as_geometry({"type": "Polygon"})["type"] == "Polygon"
|
||||||
|
assert _as_geometry('{"type": "Point"}')["type"] == "Point"
|
||||||
|
assert _as_geometry("not json") is None
|
||||||
|
assert _as_geometry(None) is None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue