mirror of
https://github.com/zvx-echo6/central.git
synced 2026-05-22 10:34:43 +02:00
feat(2-G.5): preview_for_settings framework hook + NWIS opt-in
Adds an optional async hook on SourceAdapter so any adapter can surface a
settings-driven preview on its /adapters/<name> edit page. The framework
renders the result generically as a table — no adapter-name branches in
GUI templates or route code.
Framework changes:
- src/central/adapter.py: new async preview_for_settings(self, settings)
on the base class, default returns None. Adapters opt in by overriding;
non-overriding adapters render unchanged.
- src/central/gui/routes.py: GET /adapters/{name} instantiates the adapter
with a no-op _PreviewConfigStore stub and a /dev/null cursor path (GUI
has no live ConfigStore), constructs settings_obj via the schema, and
calls preview_for_settings inside a try/except. Result lands in template
context as preview_rows / preview_error.
- src/central/gui/templates/_adapter_preview.html: new partial. Generic
table with columns derived from the first dict's keys; error banner
mirrors the existing last_error article style.
- src/central/gui/templates/adapters_edit.html: one-line include between
the Region fieldset and Save/Cancel.
NWIS opt-in:
- New NWIS_MONITORING_LOCATIONS_URL constant and _PREVIEW_LIMIT cap of 50.
- preview_for_settings returns None when region is None, otherwise one-shot
fetches monitoring-locations within the bbox via a fresh aiohttp session.
Must work even when adapter is not started -- the GUI process never calls
startup(). Returns list[dict] with the contract column order: site_id,
name, site_type, state. Errors propagate so the framework can render the
operator-visible banner.
- HTTP call factored into _fetch_preview_text so tests mock cleanly.
Tests (7 new):
- tests/test_preview_hook.py: default returns None; partial renders list
with correct headers/rows/count; partial renders error banner; partial
renders empty when both context values are None.
- tests/test_nwis.py adds TestNWISPreview: returns None without region,
returns rows with correct column order, propagates HTTP errors.
Verification:
- 457/457 full suite green (was 450; +7 new tests).
- Live /adapters/nwis preview returns 50 rows with the contract keys
against the current production Iowa bbox.
- /adapters/eonet preview_for_settings returns None via base default --
proves framework is duck-typed, no NWIS-specific code in framework.
This commit is contained in:
parent
1f0e2a091e
commit
ead6ef8ce1
7 changed files with 319 additions and 1 deletions
|
|
@ -276,3 +276,84 @@ class TestNWISAdapter:
|
|||
|
||||
assert first_pass
|
||||
assert second_pass == []
|
||||
|
||||
|
||||
class TestNWISPreview:
|
||||
"""Preview hook (PR G.5) — exercised without starting the adapter."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_returns_none_without_region(self, tmp_path: Path):
|
||||
from central.adapters.nwis import NWISAdapter, NWISSettings
|
||||
|
||||
adapter = NWISAdapter(_config({}), MagicMock(), tmp_path / "cursors.db")
|
||||
result = await adapter.preview_for_settings(NWISSettings())
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_returns_rows_with_region(self, tmp_path: Path):
|
||||
"""Given a bbox, preview returns one dict per monitoring-locations feature
|
||||
with the contract column order: site_id, name, site_type, state."""
|
||||
from central.adapters.nwis import NWISAdapter, NWISSettings
|
||||
|
||||
sample_response = json.dumps({
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"id": "USGS-05420500",
|
||||
"geometry": {"type": "Point", "coordinates": [-90.25, 41.78]},
|
||||
"properties": {
|
||||
"monitoring_location_name": "MISSISSIPPI RIVER AT CLINTON, IA",
|
||||
"site_type_code": "ST",
|
||||
"state_name": "Iowa",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"id": "USGS-05454500",
|
||||
"geometry": {"type": "Point", "coordinates": [-91.65, 41.66]},
|
||||
"properties": {
|
||||
"monitoring_location_name": "IOWA RIVER AT IOWA CITY, IA",
|
||||
"site_type_code": "ST",
|
||||
"state_name": "Iowa",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
adapter = NWISAdapter(
|
||||
_config({"region": {"west": -94.0, "south": 40.0, "east": -93.0, "north": 41.0}}),
|
||||
MagicMock(),
|
||||
tmp_path / "cursors.db",
|
||||
)
|
||||
adapter._fetch_preview_text = AsyncMock(return_value=sample_response)
|
||||
settings = NWISSettings(region=adapter.region)
|
||||
rows = await adapter.preview_for_settings(settings)
|
||||
|
||||
assert rows is not None
|
||||
assert len(rows) == 2
|
||||
# Column order is part of the contract — first row's keys must match exactly.
|
||||
expected_keys = ["site_id", "name", "site_type", "state"]
|
||||
assert list(rows[0].keys()) == expected_keys
|
||||
# Row content reflects fixture data.
|
||||
assert rows[0]["site_id"] == "USGS-05420500"
|
||||
assert rows[0]["name"] == "MISSISSIPPI RIVER AT CLINTON, IA"
|
||||
assert rows[0]["site_type"] == "ST"
|
||||
assert rows[0]["state"] == "Iowa"
|
||||
assert rows[1]["site_id"] == "USGS-05454500"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_propagates_http_error(self, tmp_path: Path):
|
||||
"""Preview must not swallow upstream errors — the framework needs them
|
||||
to render the operator-visible 'Preview unavailable: …' banner."""
|
||||
from central.adapters.nwis import NWISAdapter, NWISSettings
|
||||
|
||||
adapter = NWISAdapter(
|
||||
_config({"region": {"west": -94.0, "south": 40.0, "east": -93.0, "north": 41.0}}),
|
||||
MagicMock(),
|
||||
tmp_path / "cursors.db",
|
||||
)
|
||||
adapter._fetch_preview_text = AsyncMock(side_effect=RuntimeError("upstream 503"))
|
||||
settings = NWISSettings(region=adapter.region)
|
||||
with pytest.raises(RuntimeError, match="upstream 503"):
|
||||
await adapter.preview_for_settings(settings)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue