meshai/work/meshai/persistence/curation.py
Matt Johnson ef2df5f54b chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name
Final structural step. central_normalizer.py (973 LOC, misnamed — it lived
OUTSIDE central/ but its docstring described "the Central firehose") is DELETED.
No file or symbol named "central" survives (only immutable historical comments
in an already-applied migration .sql remain — those are not edited).

Split per the owner's rule ("next to what uses it"):
- Live geo/place-name utilities (nearest_town, _haversine_miles, _bearing_compass,
  _compute_distance_bearing, _h3_cell, _photon_reverse_places, geocoder config)
  → the EXISTING meshai/geo.py ("Geographic utilities…"), same domain. NOT a
  junk drawer. geo.py's nearest_city (hardcoded table) and central_normalizer's
  nearest_town (live Photon+H3 reverse-geocode) are different implementations —
  both kept, no false consolidation.
- WZDx work-zone parsers (_parse_wzdx_federal, _norm_wzdx_sub_type, _norm_direction,
  _parse_mile_posts, _clean_description, _is_uninformative_road) → env/wzdx_parse.py,
  next to their sole consumer env/wzdx.py.
- ~400 dead lines deleted: the Central-envelope machinery (normalize,
  _parse_state_511_atis, _parse_wfigs_incidents, _parse_itd_511_work_zone,
  should_skip_state_511_atis_id, is_incident_envelope_stale, normalize_road_name,
  _norm_sub_type, _parse_ends_at) — only caller was the deleted Central consumer.

Consumers rewired: env/wzdx.py, env/fire_render.py, formatters/fire.py,
formatters/_anchor.py, main.py, persistence/curation.py, composer.py.

Tests: test_central_normalizer.py RENAMED → test_geo_wzdx_parse.py (live geo/wzdx
tests kept + rewired; dead-parser tests dropped). test_itd_511_work_zone.py
deleted (tested only the dead itd_511 parser — verified gone from source).
Other test files: import-path rewires. Live coverage for nearest_town /
_parse_wzdx_federal / bearing preserved across test_geo_wzdx_parse, test_adapter_wzdx,
test_wfigs_handler, test_incident_refactor.

Suite: 2010 passed, 0 failed. The 49-test drop is the dead Central-envelope
parser tests, correctly removed. Stale "Central firehose" docstrings on the
moved code updated to describe present behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:11:26 +00:00

243 lines
9.7 KiB
Python

"""v0.6-4 curation accessors + seed routines.
Both tables (gauge_sites, town_anchors) follow the same pattern as
adapter_config: created by a migration, seeded from Python data on first
boot, then runtime reads from SQLite via cached accessors.
gauge_sites replaces idaho_gauge_sites.IDAHO_CURATED_SITES.
town_anchors replaces a hardcoded _TOWN_COORDS table that used to live in
the (since-deleted) Central-envelope adapter-normalizer module.
"""
from __future__ import annotations
import logging
import sqlite3
import threading
import time
from typing import Any, Optional
logger = logging.getLogger(__name__)
# ============================================================================
# Seed data (the dict that used to live in handler code)
# ============================================================================
# Idaho stream-gauge sites originally from
# meshai/central/idaho_gauge_sites.py:IDAHO_CURATED_SITES (v0.5.12).
_GAUGE_SITES_SEED: dict[str, dict[str, Any]] = {
"USGS-13139510": {
"gauge_name": "Big Lost River near Mackay",
"lat": 43.910, "lon": -113.620,
"action_ft": 5.5, "flood_minor_ft": 7.0,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13186000": {
"gauge_name": "Snake River at Heise",
"lat": 43.612, "lon": -111.654,
"action_ft": 12.0, "flood_minor_ft": 14.0,
"flood_moderate_ft": 16.0, "flood_major_ft": None,
},
"USGS-13037500": {
"gauge_name": "Snake River at Idaho Falls",
"lat": 43.500, "lon": -112.034,
"action_ft": 8.5, "flood_minor_ft": 10.0,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13135500": {
"gauge_name": "Big Wood River near Hailey",
"lat": 43.533, "lon": -114.318,
"action_ft": 6.0, "flood_minor_ft": 7.5,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13205000": {
"gauge_name": "Boise River near Boise",
"lat": 43.690, "lon": -116.200,
"action_ft": 8.0, "flood_minor_ft": 10.5,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13247500": {
"gauge_name": "Payette River at Banks",
"lat": 44.080, "lon": -116.130,
"action_ft": 10.0, "flood_minor_ft": 12.0,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13057000": {
"gauge_name": "Henrys Fork near Rexburg",
"lat": 43.831, "lon": -111.781,
"action_ft": 9.0, "flood_minor_ft": 10.5,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13162225": {
"gauge_name": "Salmon Falls Creek near San Jacinto",
"lat": 42.180, "lon": -114.850,
"action_ft": 8.0, "flood_minor_ft": 10.0,
"flood_moderate_ft": None, "flood_major_ft": None,
},
"USGS-13083000": {
"gauge_name": "Bear River near Border WY/ID",
"lat": 42.214, "lon": -111.045,
"action_ft": 6.0, "flood_minor_ft": 8.0,
"flood_moderate_ft": None, "flood_major_ft": None,
},
}
# Idaho + neighbor towns, originally from a hardcoded _TOWN_COORDS table in
# the (since-deleted) Central-envelope adapter-normalizer module.
_TOWN_ANCHORS_SEED: dict[str, dict[str, Any]] = {
"boise": {"lat": 43.6150, "lon": -116.2023, "state": "ID"},
"meridian": {"lat": 43.6121, "lon": -116.3915, "state": "ID"},
"nampa": {"lat": 43.5407, "lon": -116.5635, "state": "ID"},
"caldwell": {"lat": 43.6629, "lon": -116.6874, "state": "ID"},
"idaho falls": {"lat": 43.4666, "lon": -112.0340, "state": "ID"},
"pocatello": {"lat": 42.8713, "lon": -112.4455, "state": "ID"},
"twin falls": {"lat": 42.5630, "lon": -114.4609, "state": "ID"},
"coeur d'alene": {"lat": 47.6777, "lon": -116.7805, "state": "ID"},
"lewiston": {"lat": 46.4165, "lon": -117.0177, "state": "ID"},
"moscow": {"lat": 46.7324, "lon": -117.0002, "state": "ID"},
"sandpoint": {"lat": 48.2766, "lon": -116.5535, "state": "ID"},
"post falls": {"lat": 47.7180, "lon": -116.9516, "state": "ID"},
"hayden": {"lat": 47.7660, "lon": -116.7866, "state": "ID"},
"rathdrum": {"lat": 47.8121, "lon": -116.8950, "state": "ID"},
"plummer": {"lat": 47.3344, "lon": -116.8856, "state": "ID"},
"kellogg": {"lat": 47.5380, "lon": -116.1352, "state": "ID"},
"bonners ferry": {"lat": 48.6914, "lon": -116.3181, "state": "ID"},
"rexburg": {"lat": 43.8260, "lon": -111.7897, "state": "ID"},
"blackfoot": {"lat": 43.1905, "lon": -112.3447, "state": "ID"},
"burley": {"lat": 42.5360, "lon": -113.7928, "state": "ID"},
"jerome": {"lat": 42.7252, "lon": -114.5187, "state": "ID"},
"mountain home": {"lat": 43.1330, "lon": -115.6912, "state": "ID"},
"stanley": {"lat": 44.2160, "lon": -114.9311, "state": "ID"},
"salmon": {"lat": 45.1758, "lon": -113.8957, "state": "ID"},
"mccall": {"lat": 44.9111, "lon": -116.0987, "state": "ID"},
"weiser": {"lat": 44.2510, "lon": -116.9690, "state": "ID"},
"soda springs": {"lat": 42.6543, "lon": -111.6047, "state": "ID"},
"preston": {"lat": 42.0963, "lon": -111.8766, "state": "ID"},
"montpelier": {"lat": 42.3232, "lon": -111.2980, "state": "ID"},
}
# ============================================================================
# Caches
# ============================================================================
_LOCK = threading.Lock()
_gauge_cache: Optional[dict[str, dict[str, Any]]] = None
_town_cache: Optional[dict[str, tuple[float, float]]] = None
def invalidate_curation_cache() -> None:
"""Drop the in-memory caches. Called by the REST API on POST/PUT/DELETE."""
global _gauge_cache, _town_cache
with _LOCK:
_gauge_cache = None
_town_cache = None
def _load_gauge_cache() -> dict[str, dict[str, Any]]:
global _gauge_cache
if _gauge_cache is not None:
return _gauge_cache
try:
from meshai.persistence import get_db
conn = get_db()
rows = conn.execute(
"SELECT site_id, gauge_name, lat, lon, action_ft, flood_minor_ft, "
"flood_moderate_ft, flood_major_ft FROM gauge_sites WHERE enabled=1"
).fetchall()
cache = {r["site_id"]: {
"gauge_name": r["gauge_name"],
"lat": r["lat"], "lon": r["lon"],
"action_ft": r["action_ft"],
"flood_minor_ft": r["flood_minor_ft"],
"flood_moderate_ft": r["flood_moderate_ft"],
"flood_major_ft": r["flood_major_ft"],
} for r in rows}
except Exception:
logger.exception("curation: gauge_sites cache load failed; using empty")
cache = {}
with _LOCK:
_gauge_cache = cache
return cache
def _load_town_cache() -> dict[str, tuple[float, float]]:
global _town_cache
if _town_cache is not None:
return _town_cache
try:
from meshai.persistence import get_db
conn = get_db()
rows = conn.execute(
"SELECT name, lat, lon FROM town_anchors WHERE enabled=1"
).fetchall()
cache = {r["name"].lower(): (r["lat"], r["lon"]) for r in rows}
except Exception:
logger.exception("curation: town_anchors cache load failed; using empty")
cache = {}
with _LOCK:
_town_cache = cache
return cache
# ============================================================================
# Lookups (called from handlers)
# ============================================================================
def lookup_gauge_site(site_id: str) -> Optional[dict[str, Any]]:
"""Return the row dict for `site_id` (canonical 'USGS-...' form) or None."""
cache = _load_gauge_cache()
return cache.get(site_id)
def lookup_town_anchor(name: str) -> Optional[tuple[float, float]]:
"""Return (lat, lon) for the lowercased town name, or None."""
if not name: return None
cache = _load_town_cache()
return cache.get(name.strip().lower())
# ============================================================================
# Seed routines (called from init_db after migrations)
# ============================================================================
def seed_gauge_sites(conn: sqlite3.Connection) -> int:
"""INSERT OR IGNORE one row per _GAUGE_SITES_SEED entry. Idempotent."""
now = time.time()
inserted = 0
for site_id, spec in _GAUGE_SITES_SEED.items():
cur = conn.execute(
"INSERT OR IGNORE INTO gauge_sites("
"site_id, gauge_name, lat, lon, action_ft, flood_minor_ft, "
"flood_moderate_ft, flood_major_ft, enabled, updated_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
(site_id, spec["gauge_name"], spec["lat"], spec["lon"],
spec.get("action_ft"), spec.get("flood_minor_ft"),
spec.get("flood_moderate_ft"), spec.get("flood_major_ft"),
1, now),
)
if cur.rowcount > 0:
inserted += 1
if inserted:
logger.info("curation: seeded %d gauge_sites rows", inserted)
return inserted
def seed_town_anchors(conn: sqlite3.Connection) -> int:
"""INSERT OR IGNORE one row per _TOWN_ANCHORS_SEED entry. Idempotent."""
now = time.time()
inserted = 0
for name, spec in _TOWN_ANCHORS_SEED.items():
cur = conn.execute(
"INSERT OR IGNORE INTO town_anchors("
"name, lat, lon, state, enabled, updated_at) "
"VALUES (?,?,?,?,?,?)",
(name, spec["lat"], spec["lon"], spec.get("state"), 1, now),
)
if cur.rowcount > 0:
inserted += 1
if inserted:
logger.info("curation: seeded %d town_anchors rows", inserted)
return inserted