v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters

* v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters

Adds regional subject tokens to four adapters that previously published
without location-based routing:

- usgs_quake: central.quake.event.<tier>.us.<state> (US) or .<country> (intl)
- firms: central.fire.hotspot.<sat>.<conf>.us.<state> or .<country>
- nwis: central.hydro.<param>.<agency>.<site>.us.<state> (always US)
- eonet: central.disaster.eonet.<cat>.<country> (replaces hardcoded .global)

The us.<state> pattern (two tokens for US events) matches the NWS precedent
and resolves the ISO-2 collision between Idaho (id) and Indonesia.

New shared helper module: src/central/adapters/_subject_helpers.py
- US_STATE_NAME_TO_CODE: 50 states + DC + territories
- subject_for_country(): normalized country token
- subject_for_region(): returns us.<state>, <country>, or unknown

gdacs.py refactored to import subject_for_country from shared module.

Fixes: meshai v0.4 Phase C.3 bug (M4.1 Nevada quake routed globally)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* v0.9.20: stale docstring nit on renamed test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
malice 2026-05-27 23:50:30 -06:00 committed by GitHub
commit 6ea7bd70f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 383 additions and 38 deletions

View file

@ -54,7 +54,7 @@ class TestEONETHelpers:
assert _subject_category(None) == "unknown"
assert _subject_category("") == "unknown"
# Through subject_for: a category with no upstream component yields .unknown.global
# Through subject_for: a category with no upstream component yields .unknown.unknown
adapter = EONETAdapter(_config(), MagicMock(), Path("/tmp/never_used.db"))
event = Event(
id="X",
@ -65,7 +65,7 @@ class TestEONETHelpers:
geo=Geo(),
data={},
)
assert adapter.subject_for(event).endswith(".unknown.global")
assert adapter.subject_for(event).endswith(".unknown.unknown")
def test_dedup_key_includes_latest_geometry_date(self):
from central.adapters.eonet import _dedup_key
@ -148,8 +148,8 @@ class TestEONETAdapter:
assert out_lat == lat_in, "second centroid element must equal upstream lat (no swap)"
@pytest.mark.asyncio
async def test_country_always_global(self, tmp_path: Path):
"""Every emitted event has subject suffix '.global' (no country resolution in v1)."""
async def test_country_unknown_when_no_geocoder(self, tmp_path: Path):
"""No geocoder enrichment -> subject ends with '.unknown'."""
from central.adapters.eonet import EONETAdapter
adapter = EONETAdapter(_config(), MagicMock(), tmp_path / "cursors.db")
@ -160,7 +160,7 @@ class TestEONETAdapter:
assert events, "fixture should produce at least one emitted event"
for e in events:
assert adapter.subject_for(e).endswith(".global"), e.category
assert adapter.subject_for(e).endswith(".unknown"), e.category
@pytest.mark.asyncio
async def test_magnitude_value_surfaced(self, tmp_path: Path):
@ -244,9 +244,9 @@ class TestEONETAdapter:
# Subject pattern: subtype BEFORE 'removed' per §8 canonical pattern.
# Subscriber filtering on central.disaster.eonet.<cat>.> must match the
# removal subject central.disaster.eonet.<cat>.removed.global.
# removal subject central.disaster.eonet.<cat>.removed.unknown.
expected_cat = _subject_category(first_event["categories"][0]["id"])
subj = adapter.subject_for(ts)
assert subj.startswith(f"central.disaster.eonet.{expected_cat}.")
assert ".removed." in subj
assert subj.endswith(".global")
assert subj.endswith(".unknown")