mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
v0.10.3: rip out state_511_atis adapter (superseded by itd_511 v0.10.0; Castle Rock legacy shape EOL per sister-site discovery) (#88)
Closes #88 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
557230c7a7
commit
0dd83a340e
23 changed files with 45 additions and 1036 deletions
|
|
@ -1 +0,0 @@
|
|||
{"recordsTotal":2,"recordsFiltered":2,"data":[{"DT_RowId":"1","tooltipUrl":"/tooltip/Cameras/1?lang=%7Blang%7D&noCss=true","agencyLogoEnabled":false,"visible":true,"isDefault":false,"images":[{"id":1,"cameraSiteId":1,"sortOrder":1,"description":"N/A","imageUrl":"/map/Cctv/1","imageType":0,"isVideoAuthRequired":false,"videoDisabled":false,"disabled":false,"blocked":false,"language":"en"}],"id":1,"sourceId":"10.C1","source":"UDOT","type":"UDOT","areaId":null,"area":null,"sortOrder":1,"roadway":"I-15","direction":"Unknown","location":"I-15 UT/ID State Line UT","latLng":{"geography":{"coordinateSystemId":4326,"wellKnownText":"POINT (-112.198 42.0011)"}},"linkId1":"112984999F","linkId2":"114769197T","created":"2023-01-23T13:51:47.3533333+00:00","lastUpdated":"2023-01-23T13:51:47.3533333+00:00","lastEditedBy":"zeeshawn.ahmad@gmail.com","defaultCameraSite":false,"nickname":null,"language":"en","jsonData":{},"jsonDataSerialized":null,"region":null,"state":null,"county":null,"city":null,"dotDistrict":null},{"DT_RowId":"2","tooltipUrl":"/tooltip/Cameras/2?lang=%7Blang%7D&noCss=true","agencyLogoEnabled":false,"visible":true,"isDefault":false,"images":[{"id":2,"cameraSiteId":2,"sortOrder":325,"imageUrl":"/map/Cctv/2","imageType":0,"refreshRateMs":300000,"isVideoAuthRequired":false,"videoDisabled":false,"disabled":false,"blocked":false,"language":"en"},{"id":3,"cameraSiteId":2,"sortOrder":325,"imageUrl":"/map/Cctv/3","imageType":0,"refreshRateMs":300000,"isVideoAuthRequired":false,"videoDisabled":false,"disabled":false,"blocked":false,"language":"en"},{"id":4,"cameraSiteId":2,"sortOrder":325,"imageUrl":"/map/Cctv/4","imageType":0,"refreshRateMs":300000,"isVideoAuthRequired":false,"videoDisabled":false,"disabled":false,"blocked":false,"language":"en"}],"id":2,"sourceId":"100.C1","source":"RWIS","type":"RWIS","areaId":null,"area":null,"sortOrder":325,"roadway":"SH-75","direction":"Unknown","location":"SH-75 Wood River","latLng":{"geography":{"coordinateSystemId":4326,"wellKnownText":"POINT (-114.345 43.5946)"}},"linkId1":"41105278T","linkId2":"851613715T","created":"2024-08-12T07:57:34.1833333+00:00","lastUpdated":"2024-08-12T07:57:34.1833333+00:00","lastEditedBy":"barton.phelps@itd.idaho.gov","defaultCameraSite":false,"nickname":null,"language":"en","jsonData":{"rwisType":"Normal","status":"Normal"},"jsonDataSerialized":"{\"RwisType\":\"Normal\",\"Status\":\"Normal\"}","region":null,"state":null,"county":null,"city":null,"dotDistrict":null}]}
|
||||
1
tests/fixtures/state_511_atis_closures.json
vendored
1
tests/fixtures/state_511_atis_closures.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"data":[{"DT_RowId":"17","tooltipUrl":"/tooltip/Closures/17?lang=%7Blang%7D&noCss=true","id":17,"type":"Closures","layerName":"Closures","roadwayName":"N McDermott Rd","description":" Long term road construction on N McDermott Rd Both Directions from Five Mile Creek to US-20. All lanes closed. 1/30/2023 2:24 PM Mon, Tue, Wed, Thu, Fri, Sat, Sun: Active all day<div class='cellSpacer'><i><b>Comments:</b></i> Open to local traffic only.</div>","sourceId":"469","source":"ERS","comment":"Open to local traffic only.","eventSubType":"longTermRoadConstruction","startDate":"1/30/23, 2:24 PM","endDate":null,"lastUpdated":"6/18/24, 6:48 PM","isFullClosure":true,"severity":"None","direction":"Both","locationDescription":"Five Mile Creek | US-20","detourDescription":null,"laneDescription":"All lanes closed","recurrenceDescription":"<b>Mon, Tue, Wed, Thu, Fri, Sat, Sun:</b><br/>Active all day<br/><br/>","widthRestriction":null,"heightRestriction":null,"heightUnderRestriction":null,"lengthRestriction":null,"weightRestriction":null,"majorEvent":null,"county":"Ada","region":"Idaho Falls/Pocatello","state":"Idaho","country":"United States","showOnMap":true,"restrictions":"Width Restriction: <br>Height Restriction: <br>Length Restriction: <br>Weight Restriction: <br>Speed Restriction: <br>"}]}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"data":[{"DT_RowId":"23","tooltipUrl":"/tooltip/Construction/23?lang=%7Blang%7D&noCss=true","id":23,"type":"Roadwork","layerName":"Construction","roadwayName":"SH-81","description":" Work on the shoulder on SH-81 near Poverty Gulch. 7/8/2024 9:29 PM Mon: 12:00 PM - 5:00 PM, Tue, Wed, Thu, Fri, Sat, Sun: Active all day Activities: use caution, warning.","sourceId":"4277","source":"ERS","comment":null,"eventSubType":"workOnTheShoulder","startDate":"7/8/24, 9:29 PM","endDate":null,"lastUpdated":"6/11/25, 3:02 PM","isFullClosure":false,"severity":"None","direction":"Unknown","locationDescription":"Poverty Gulch","detourDescription":null,"laneDescription":"","recurrenceDescription":"<b>Mon:</b><br/>12:00 PM - 5:00 PM<br/><br/><b>Tue, Wed, Thu, Fri, Sat, Sun:</b><br/>Active all day<br/><br/>","widthRestriction":null,"heightRestriction":null,"heightUnderRestriction":null,"lengthRestriction":null,"weightRestriction":null,"majorEvent":null,"county":"Cassia","region":"Idaho Falls/Pocatello","state":"Idaho","country":"United States","showOnMap":true,"restrictions":"Width Restriction: <br>Height Restriction: <br>Length Restriction: <br>Weight Restriction: <br>Speed Restriction: <br>"}]}
|
||||
1
tests/fixtures/state_511_atis_incidents.json
vendored
1
tests/fixtures/state_511_atis_incidents.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"data":[{"DT_RowId":"33579","tooltipUrl":"/tooltip/Incidents/33579?lang=%7Blang%7D&noCss=true","id":33579,"type":"Incidents","layerName":"Incidents","roadwayName":"US-95","description":" Roadway Blocked on US-95 Both Directions near MM (469). All lanes blocked. Activities: Expect Delays, Reduced to Single Lane, Alternating Direction of Travel, Use Caution.<div class='cellSpacer'><i><b>Comments:</b></i> Milepost 469, roadway blocked. Expect delays. Use caution.</div>","sourceId":"10991","source":"ERS","comment":"Milepost 469, roadway blocked. Expect delays. Use caution.","eventSubType":"roadwayBlocked","startDate":"5/25/26, 2:32 PM","endDate":null,"lastUpdated":"5/25/26, 3:40 PM","isFullClosure":false,"severity":"None","direction":"Both","locationDescription":"Ponderosa Mobile Home Park","detourDescription":null,"laneDescription":"All lanes blocked","recurrenceDescription":null,"widthRestriction":null,"heightRestriction":null,"heightUnderRestriction":null,"lengthRestriction":null,"weightRestriction":null,"majorEvent":null,"county":"Bonner","region":"Idaho Falls/Pocatello","state":"Idaho","country":"United States","showOnMap":true,"restrictions":"Width Restriction: <br>Height Restriction: <br>Length Restriction: <br>Weight Restriction: <br>Speed Restriction: <br>"}]}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"item2":[{"polyline":{"path":"qbliGhv{eUsk@MeTEsBAsAGiAQw@Qi@MoAYmA]aA]}@a@[?[JSNKNENCTEzCGnAIX","color":"#CC0004"},"itemId":"17","location":[43.6485700000001,-116.47349],"icon":{"url":"/Generated/Content/Images/511/map_closure.svg"},"title":""}]}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"item2":[{"itemId":"23","location":[42.5168038430856,-113.711287649613],"icon":{},"title":""}]}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"item2":[{"itemId":"33579","location":[48.2055675659533,-116.563364000498],"icon":{},"title":""}]}
|
||||
|
|
@ -119,3 +119,27 @@ def test_subsections_appear_in_doc_order_matches_registry_size():
|
|||
f"duplicate per-adapter sections: {[a for a in doc_adapters if doc_adapters.count(a) > 1]}"
|
||||
)
|
||||
assert len(doc_adapters) == len(discover_adapters())
|
||||
|
||||
|
||||
def test_castle_rock_legacy_adapters_remain_removed():
|
||||
"""v0.10.3 regression guard: ``state_511_atis`` and ``state_511_atis_cameras``
|
||||
were ripped out because the Castle Rock legacy ``/map/mapIcons/`` +
|
||||
``/List/GetData/`` shape is end-of-life on the only Idaho source we cared
|
||||
about (Idaho 511) -- the official ITD adapters (``itd_511`` + ``itd_511_cameras``,
|
||||
v0.10.0) supersede them. The sister-site discovery confirmed no other
|
||||
Castle Rock customer still exposes the legacy shape that this adapter pair
|
||||
consumed. Re-adding either module would resurrect a dying-upstream dependency."""
|
||||
registry = discover_adapters()
|
||||
assert "state_511_atis" not in registry, (
|
||||
"state_511_atis was removed in v0.10.3; use itd_511 (v0.10.0) instead"
|
||||
)
|
||||
assert "state_511_atis_cameras" not in registry, (
|
||||
"state_511_atis_cameras was removed in v0.10.3; use itd_511_cameras (v0.10.0) instead"
|
||||
)
|
||||
adapters_dir = Path(__file__).resolve().parents[1] / "src" / "central" / "adapters"
|
||||
assert not (adapters_dir / "state_511_atis.py").exists(), (
|
||||
"state_511_atis.py was removed in v0.10.3; do not re-add"
|
||||
)
|
||||
assert not (adapters_dir / "state_511_atis_cameras.py").exists(), (
|
||||
"state_511_atis_cameras.py was removed in v0.10.3; do not re-add"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1143,10 +1143,8 @@ _SAMPLE_INNER = {
|
|||
"wfigs_incidents": {"county": "Montezuma", "state": "CO"},
|
||||
"wfigs_perimeters": {"county": "Carbon", "state": "MT"},
|
||||
"wzdx": {"road_names": ["I-80"], "direction": "eastbound"},
|
||||
"state_511_atis": {"layer": "Incidents", "roadway_name": "US-95", "location_description": "Ponderosa Mobile Home Park"},
|
||||
"tomtom_flow": {"road_category": "primary", "relative_speed": 0.11},
|
||||
"tomtom_incidents": {"description": "Roadworks", "from": "Early Road", "to": "Slade Road"},
|
||||
"state_511_atis_cameras": {"location_description": "I-84 Mountain Home", "camera_id": 42},
|
||||
"itd_511": {"event_type_short": "work_zone", "roadway_name": "I-84"},
|
||||
"itd_511_cameras": {"location": "I-84 Mountain Home", "camera_id": 42},
|
||||
}
|
||||
|
|
@ -1170,7 +1168,6 @@ _EXPECTED_SUBJECT = {
|
|||
"wzdx": "Work zone on I-80 eastbound",
|
||||
"tomtom_flow": "Traffic flow (primary) — 11% of free-flow",
|
||||
"tomtom_incidents": "Roadworks on Early Road → Slade Road",
|
||||
"state_511_atis_cameras": "Camera: I-84 Mountain Home",
|
||||
"itd_511": "Road work on I-84",
|
||||
"itd_511_cameras": "Camera: I-84 Mountain Home",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,12 +226,3 @@ class TestBboxMapPreview:
|
|||
assert "bbox-map" in out # map container present
|
||||
assert "L.map(" in out and "L.rectangle(" in out # Leaflet init present
|
||||
|
||||
def test_state_511_atis_no_bbox_map(self):
|
||||
"""Non-bbox model_list (StateConfig) → generic editor, no map (no regression)."""
|
||||
from central.adapters.state_511_atis import State511ATISAdapter
|
||||
s = {"states": [{"code": "ID", "base_url": "https://511.idaho.gov"}]}
|
||||
out = _render("adapters_edit.html",
|
||||
_ctx(s, describe_fields(State511ATISAdapter.settings_schema, s), None,
|
||||
name="state_511_atis", display="511 ATIS"))
|
||||
assert "model-list" in out # generic editor still renders
|
||||
assert "bbox-map" not in out # but no map div
|
||||
|
|
|
|||
|
|
@ -1,207 +0,0 @@
|
|||
"""Tests for the state_511_atis adapter (Castle Rock ATIS, Idaho).
|
||||
|
||||
Fixtures are real captures (one record + its matching marker per layer):
|
||||
state_511_atis_<layer>.json -- POST /List/GetData/<Layer> .data[0:1]
|
||||
state_511_atis_markers_<layer>.json -- GET /map/mapIcons/<Layer> matching item2
|
||||
|
||||
No tests/conftest isolation entry is added: dedup uses the supervisor-injected
|
||||
cursors.db (inherited mixin) and discovery is stateless -- no adapter-owned cache.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from central.adapter import SourceAdapter
|
||||
from central.adapters.state_511_atis import (
|
||||
LAYER_EVENT_TYPE,
|
||||
State511ATISAdapter,
|
||||
_parse_us_dt,
|
||||
)
|
||||
from central.config_models import AdapterConfig
|
||||
|
||||
FIX = Path(__file__).parent / "fixtures"
|
||||
DETAIL = {lyr: json.loads((FIX / f"state_511_atis_{lyr.lower()}.json").read_text())["data"][0]
|
||||
for lyr in ("Incidents", "Closures", "Construction")}
|
||||
MARK = {lyr: json.loads((FIX / f"state_511_atis_markers_{lyr.lower()}.json").read_text())["item2"][0]
|
||||
for lyr in ("Incidents", "Closures", "Construction")}
|
||||
|
||||
|
||||
def _coords(layer):
|
||||
loc = MARK[layer]["location"]
|
||||
return (loc[0], loc[1])
|
||||
|
||||
|
||||
def _cfg():
|
||||
return AdapterConfig(
|
||||
name="state_511_atis", enabled=True, cadence_s=300,
|
||||
settings={"states": [{"code": "ID", "base_url": "https://511.idaho.gov"}]},
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(tmp_path):
|
||||
return State511ATISAdapter(_cfg(), MagicMock(), tmp_path / "cursors.db")
|
||||
|
||||
|
||||
def test_layer_event_type_map():
|
||||
assert LAYER_EVENT_TYPE == {"Incidents": "incident", "Closures": "closure", "Construction": "work_zone"}
|
||||
|
||||
|
||||
def test_parse_us_dt():
|
||||
assert _parse_us_dt("5/25/26, 2:32 PM") == datetime(2026, 5, 25, 14, 32, tzinfo=timezone.utc)
|
||||
assert _parse_us_dt("") is None
|
||||
assert _parse_us_dt("not a date") is None
|
||||
|
||||
|
||||
def test_dedup_key(adapter):
|
||||
e = adapter._build_event(DETAIL["Incidents"], _coords("Incidents"), "ID", "Incidents")
|
||||
assert e.id == "ID:Incidents:33579"
|
||||
|
||||
|
||||
def test_build_incident(adapter):
|
||||
e = adapter._build_event(DETAIL["Incidents"], _coords("Incidents"), "ID", "Incidents")
|
||||
assert e.category == "incident.state_511_atis"
|
||||
assert e.severity == 1
|
||||
assert e.data["roadway_name"] == "US-95"
|
||||
assert e.data["county"] == "Bonner"
|
||||
assert e.data["latitude"] is not None and e.data["longitude"] is not None
|
||||
|
||||
|
||||
def test_build_closure_full_closure_severity(adapter):
|
||||
e = adapter._build_event(DETAIL["Closures"], _coords("Closures"), "ID", "Closures")
|
||||
assert e.category == "closure.state_511_atis"
|
||||
assert e.data["is_full_closure"] is True
|
||||
assert e.severity == 3 # isFullClosure -> 3
|
||||
|
||||
|
||||
def test_build_construction_maps_to_work_zone(adapter):
|
||||
e = adapter._build_event(DETAIL["Construction"], _coords("Construction"), "ID", "Construction")
|
||||
assert e.category == "work_zone.state_511_atis" # layer Construction, type "Roadwork"
|
||||
assert e.severity == 1
|
||||
assert e.data["roadway_name"] == "SH-81"
|
||||
|
||||
|
||||
def test_join_missing_coords(adapter):
|
||||
e = adapter._build_event(DETAIL["Incidents"], None, "ID", "Incidents")
|
||||
assert e.data["latitude"] is None and e.data["longitude"] is None
|
||||
assert e.geo.centroid is None # still built, just no map point
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layer,et", [("Incidents", "incident"), ("Closures", "closure"), ("Construction", "work_zone")])
|
||||
def test_subject_for(adapter, layer, et):
|
||||
e = adapter._build_event(DETAIL[layer], _coords(layer), "ID", layer)
|
||||
assert adapter.subject_for(e) == f"central.traffic.{et}.id"
|
||||
|
||||
|
||||
def test_summary_partial_renders():
|
||||
from central.gui.routes import _derive_subject
|
||||
inner = {"layer": "Incidents", "roadway_name": "US-95", "location_description": "Ponderosa Mobile Home Park"}
|
||||
row = {"adapter": "state_511_atis", "data": {"data": {"data": inner}}}
|
||||
assert _derive_subject(row) == "Incident on US-95 — Ponderosa Mobile Home Park"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_joins_and_yields(adapter):
|
||||
await adapter.startup()
|
||||
|
||||
async def fake_markers(base_url, layer):
|
||||
m = MARK[layer]
|
||||
return {str(m["itemId"]): (m["location"][0], m["location"][1])}
|
||||
|
||||
async def fake_details(base_url, layer):
|
||||
return [DETAIL[layer]]
|
||||
|
||||
adapter._fetch_markers = fake_markers
|
||||
adapter._fetch_details = fake_details
|
||||
events = [e async for e in adapter.poll()]
|
||||
await adapter.shutdown()
|
||||
assert len(events) == 3 # one ID state x three layers
|
||||
assert {e.category for e in events} == {
|
||||
"incident.state_511_atis", "closure.state_511_atis", "work_zone.state_511_atis",
|
||||
}
|
||||
assert all(e.adapter == "state_511_atis" for e in events)
|
||||
|
||||
|
||||
def test_inherits_dedup_mixin():
|
||||
for m in ("is_published", "mark_published", "sweep_old_ids"):
|
||||
assert m not in State511ATISAdapter.__dict__, f"redefines {m}"
|
||||
assert getattr(State511ATISAdapter, m) is getattr(SourceAdapter, m)
|
||||
|
||||
|
||||
# --- v0.9.7 pagination ------------------------------------------------------
|
||||
|
||||
def _rec(i):
|
||||
return {"id": i, "type": "Roadwork", "roadwayName": "SH-1", "location": f"loc {i}"}
|
||||
|
||||
|
||||
def _page(records, records_filtered):
|
||||
return {"draw": 1, "recordsTotal": records_filtered,
|
||||
"recordsFiltered": records_filtered, "data": records}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pagination_collects_all_pages(adapter):
|
||||
await adapter.startup()
|
||||
pages = {0: _page([_rec(i) for i in range(100)], 114),
|
||||
100: _page([_rec(i) for i in range(100, 114)], 114)}
|
||||
|
||||
async def fake_page(base_url, layer, start):
|
||||
return pages[start]
|
||||
|
||||
adapter._fetch_page = fake_page
|
||||
rows = await adapter._fetch_details("https://511.idaho.gov", "Construction")
|
||||
await adapter.shutdown()
|
||||
assert len(rows) == 114 # 100 + 14, not truncated at the 100-row cap
|
||||
assert {r["id"] for r in rows} == set(range(114))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pagination_handles_short_final_page(adapter):
|
||||
await adapter.startup()
|
||||
pages = {0: _page([_rec(i) for i in range(100)], 130),
|
||||
100: _page([_rec(i) for i in range(100, 130)], 130)}
|
||||
|
||||
async def fake_page(base_url, layer, start):
|
||||
return pages[start]
|
||||
|
||||
adapter._fetch_page = fake_page
|
||||
rows = await adapter._fetch_details("https://511.idaho.gov", "Construction")
|
||||
await adapter.shutdown()
|
||||
assert len(rows) == 130 # short 30-row final page collected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pagination_empty_page_breaks(adapter):
|
||||
# recordsFiltered overstates the set; an empty page must stop the loop (no hang).
|
||||
await adapter.startup()
|
||||
pages = {0: _page([_rec(i) for i in range(100)], 250), 100: _page([], 250)}
|
||||
|
||||
async def fake_page(base_url, layer, start):
|
||||
return pages.get(start, _page([], 250))
|
||||
|
||||
adapter._fetch_page = fake_page
|
||||
rows = await adapter._fetch_details("https://511.idaho.gov", "Construction")
|
||||
await adapter.shutdown()
|
||||
assert len(rows) == 100 # empty page 2 terminates cleanly
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pagination_loop_cap(adapter, caplog):
|
||||
# recordsFiltered never satisfied -> loop must stop at _MAX_PAGES and warn.
|
||||
await adapter.startup()
|
||||
|
||||
async def fake_page(base_url, layer, start):
|
||||
return _page([_rec(start + i) for i in range(100)], 999_999)
|
||||
|
||||
adapter._fetch_page = fake_page
|
||||
with caplog.at_level("WARNING"):
|
||||
rows = await adapter._fetch_details("https://511.idaho.gov", "Construction")
|
||||
await adapter.shutdown()
|
||||
from central.adapters.state_511_atis import _MAX_PAGES
|
||||
assert len(rows) == _MAX_PAGES * 100 # capped, not infinite
|
||||
assert "max_pages" in caplog.text
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
"""Tests for the state_511_atis_cameras adapter (v0.9.6).
|
||||
|
||||
Fixture is a real /List/GetData/Cameras capture (2 cameras: one single-image
|
||||
UDOT border camera, one multi-image RWIS camera):
|
||||
tests/fixtures/state_511_atis_cameras_sample.json
|
||||
|
||||
No conftest entry: dedup uses the supervisor-injected cursors.db (inherited
|
||||
mixin); polling is stateless.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from central.adapter import SourceAdapter
|
||||
from central.adapters.state_511_atis_cameras import (
|
||||
State511ATISCamerasAdapter,
|
||||
StateConfig,
|
||||
_parse_wkt,
|
||||
)
|
||||
from central.config_models import AdapterConfig
|
||||
|
||||
FIX = json.loads((Path(__file__).parent / "fixtures" / "state_511_atis_cameras_sample.json").read_text())
|
||||
CAMS = FIX["data"]
|
||||
ID = StateConfig(code="ID", base_url="https://511.idaho.gov")
|
||||
TODAY = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _cfg():
|
||||
return AdapterConfig(
|
||||
name="state_511_atis_cameras", enabled=True, cadence_s=600,
|
||||
settings={"states": [ID.model_dump()]}, updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(tmp_path):
|
||||
return State511ATISCamerasAdapter(_cfg(), MagicMock(), tmp_path / "cursors.db")
|
||||
|
||||
|
||||
def test_wkt_parse():
|
||||
assert _parse_wkt("POINT (-112.198 42.0011)") == (42.0011, -112.198) # (lat, lon)
|
||||
assert _parse_wkt(None) == (None, None)
|
||||
assert _parse_wkt("nonsense") == (None, None)
|
||||
|
||||
|
||||
def test_dedup_key_shape(adapter):
|
||||
e = adapter._build_event(CAMS[0], ID)
|
||||
assert e.id == f"ID:cam:{CAMS[0]['id']}:{TODAY}" # per-UTC-day bucketing
|
||||
|
||||
|
||||
def test_build_event_with_image_url(adapter):
|
||||
e = adapter._build_event(CAMS[0], ID)
|
||||
assert e.category == "camera.state_511_atis_cameras"
|
||||
assert e.severity == 1
|
||||
assert e.data["image_url"] == "https://511.idaho.gov" + CAMS[0]["images"][0]["imageUrl"]
|
||||
assert e.data["source"] == CAMS[0]["source"] # provenance surfaced
|
||||
assert e.data["roadway_name"] == CAMS[0]["roadway"]
|
||||
assert e.data["latitude"] is not None and e.data["longitude"] is not None
|
||||
|
||||
|
||||
def test_build_event_multi_image(adapter):
|
||||
e = adapter._build_event(CAMS[1], ID)
|
||||
assert e.data["image_count"] == len(CAMS[1]["images"])
|
||||
assert e.data["image_count"] >= 2
|
||||
|
||||
|
||||
def test_no_image_url_handled_gracefully(adapter):
|
||||
cam = {"id": 999, "roadway": "US-95", "location": "US-95 Somewhere", "source": "ITDNET",
|
||||
"direction": "Unknown", "images": [],
|
||||
"latLng": {"geography": {"wellKnownText": "POINT (-116.5 46.4)"}}}
|
||||
e = adapter._build_event(cam, ID)
|
||||
assert e is not None
|
||||
assert e.data["image_url"] is None and e.data["image_count"] == 0
|
||||
assert e.data["location_description"] == "US-95 Somewhere"
|
||||
|
||||
|
||||
def test_subject_for_state_id(adapter):
|
||||
e = adapter._build_event(CAMS[0], ID)
|
||||
assert adapter.subject_for(e) == f"central.traffic_cameras.id.{CAMS[0]['id']}"
|
||||
|
||||
|
||||
def test_subject_for_unknown_state(adapter):
|
||||
e = adapter._build_event(CAMS[0], StateConfig(code="", base_url="https://x"))
|
||||
assert adapter.subject_for(e) == f"central.traffic_cameras.unknown.{CAMS[0]['id']}"
|
||||
|
||||
|
||||
def test_summary_partial_renders():
|
||||
from central.gui.routes import _derive_subject
|
||||
inner = {"location_description": "I-84 Mountain Home", "camera_id": 42}
|
||||
row = {"adapter": "state_511_atis_cameras", "data": {"data": {"data": inner}}}
|
||||
assert _derive_subject(row) == "Camera: I-84 Mountain Home"
|
||||
|
||||
|
||||
def test_rows_partial_includes_img_tag():
|
||||
from central.gui.routes import _get_templates
|
||||
inner = {"image_url": "https://511.idaho.gov/map/Cctv/1", "roadway_name": "I-84",
|
||||
"location_description": "I-84 Mountain Home", "source": "ITDNET", "image_count": 1}
|
||||
row = {"data": {"data": {"data": inner}}}
|
||||
html = _get_templates().env.get_template("_event_rows/state_511_atis_cameras.html").render(event=row)
|
||||
assert "<img" in html
|
||||
assert "https://511.idaho.gov/map/Cctv/1" in html
|
||||
assert "ITDNET" in html # source provenance rendered
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_paginates(tmp_path):
|
||||
a = State511ATISCamerasAdapter(_cfg(), MagicMock(), tmp_path / "cursors.db")
|
||||
await a.startup()
|
||||
|
||||
def _cam(i):
|
||||
return {"id": i, "roadway": "I-84", "location": f"loc {i}", "direction": "N",
|
||||
"source": "ITDNET", "images": [{"imageUrl": f"/map/Cctv/{i}"}],
|
||||
"latLng": {"geography": {"wellKnownText": "POINT (-116.2 43.6)"}}}
|
||||
|
||||
pages = {0: {"recordsTotal": 150, "data": [_cam(i) for i in range(100)]},
|
||||
100: {"recordsTotal": 150, "data": [_cam(i) for i in range(100, 150)]}}
|
||||
|
||||
async def fake_page(base_url, start):
|
||||
return pages[start]
|
||||
|
||||
a._fetch_page = fake_page
|
||||
events = [e async for e in a.poll()]
|
||||
await a.shutdown()
|
||||
assert len(events) == 150 # all 150 fetched across 2 pages, NOT truncated at the 100-row cap
|
||||
|
||||
|
||||
def test_inherits_dedup_mixin():
|
||||
for m in ("is_published", "mark_published", "sweep_old_ids"):
|
||||
assert m not in State511ATISCamerasAdapter.__dict__, f"redefines {m}"
|
||||
assert getattr(State511ATISCamerasAdapter, m) is getattr(SourceAdapter, m)
|
||||
|
|
@ -11,7 +11,7 @@ from central.adapter_discovery import discover_adapters
|
|||
from central.gui import routes
|
||||
|
||||
# Adapters with data_class="telemetry" (the pinned split; grow as telemetry adapters land).
|
||||
_TELEMETRY = ["itd_511_cameras", "nwis", "state_511_atis_cameras", "tomtom_flow"]
|
||||
_TELEMETRY = ["itd_511_cameras", "nwis", "tomtom_flow"]
|
||||
|
||||
|
||||
# --- data_class defaults / registry split -----------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue