mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-11 12:24:37 +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
|
|
@ -46,6 +46,9 @@ from central.gui.audit import (
|
|||
)
|
||||
from functools import cache
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from central.config_models import AdapterConfig
|
||||
from central.gui.db import get_pool
|
||||
from central.gui.form_descriptors import describe_fields, FieldDescriptor
|
||||
from central.adapter_discovery import discover_adapters
|
||||
|
|
@ -55,13 +58,23 @@ from pydantic import ValidationError
|
|||
@cache
|
||||
def _adapter_classes() -> dict:
|
||||
"""Cached adapter class discovery.
|
||||
|
||||
|
||||
GUI is a separate process from supervisor; walks pkgutil itself.
|
||||
Python's import cache makes subsequent calls free.
|
||||
"""
|
||||
return discover_adapters()
|
||||
|
||||
|
||||
class _PreviewConfigStore:
|
||||
"""No-op stand-in passed to adapter __init__ when calling preview_for_settings.
|
||||
|
||||
preview_for_settings implementations must create their own one-shot HTTP
|
||||
session and must not depend on config_store / cursor_db state — the GUI
|
||||
process has no live ConfigStore (the supervisor owns the real one)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Streams to display on dashboard -- derived from the registry's dashboard flag.
|
||||
|
|
@ -1450,6 +1463,28 @@ async def adapters_edit_form(
|
|||
)
|
||||
api_key_missing = not has_key
|
||||
|
||||
# Generic settings-driven preview. Adapters opt in by overriding
|
||||
# SourceAdapter.preview_for_settings; the framework is duck-typed on the
|
||||
# returned list[dict] shape and never branches on adapter name.
|
||||
preview_rows: list[dict] | None = None
|
||||
preview_error: str | None = None
|
||||
if adapter_cls is not None and hasattr(adapter_cls, "settings_schema"):
|
||||
try:
|
||||
settings_obj = adapter_cls.settings_schema(**settings)
|
||||
preview_cfg = AdapterConfig(
|
||||
name=row["name"],
|
||||
enabled=row["enabled"],
|
||||
cadence_s=row["cadence_s"],
|
||||
settings=settings,
|
||||
updated_at=row["updated_at"],
|
||||
)
|
||||
preview_adapter = adapter_cls(
|
||||
preview_cfg, _PreviewConfigStore(), Path("/dev/null")
|
||||
)
|
||||
preview_rows = await preview_adapter.preview_for_settings(settings_obj)
|
||||
except Exception as exc:
|
||||
preview_error = f"Preview unavailable: {exc}"
|
||||
|
||||
csrf_token = request.state.csrf_token
|
||||
response = templates.TemplateResponse(
|
||||
request=request,
|
||||
|
|
@ -1466,6 +1501,8 @@ async def adapters_edit_form(
|
|||
"tile_attribution": tile_attribution,
|
||||
"api_key_missing": api_key_missing,
|
||||
"requires_api_key_alias": requires_api_key_alias,
|
||||
"preview_rows": preview_rows,
|
||||
"preview_error": preview_error,
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue