mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 09:21:33 +00:00
fix: make TomTom + FIRMS API keys optional (keyless-capable) (#137)
traffic and firms no longer idle when their key is blank — they build a keyless request (traffic: omit key= param; firms: omit the map_key path segment), matching roads511's existing optional-key pattern. Enables routing these feeds through a key-injecting proxy (Conduit) with the key held only there. Key-set behavior is byte-identical. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
998838bcb2
commit
1199b3576a
4 changed files with 199 additions and 15 deletions
13
work/meshai/env/firms.py
vendored
13
work/meshai/env/firms.py
vendored
|
|
@ -77,12 +77,8 @@ class FIRMSAdapter:
|
|||
|
||||
self._last_tick = now
|
||||
|
||||
if not self._map_key:
|
||||
if not self._last_error:
|
||||
logger.warning("FIRMS: No MAP_KEY configured, skipping")
|
||||
self._last_error = "No MAP_KEY configured"
|
||||
return False
|
||||
|
||||
# MAP_KEY is optional — a blank key builds a keyless request path for
|
||||
# a key-injecting proxy (e.g. Conduit); bbox is the real prerequisite.
|
||||
if not self._bbox or len(self._bbox) != 4:
|
||||
if not self._last_error:
|
||||
logger.warning("FIRMS: No valid bbox configured, skipping")
|
||||
|
|
@ -100,7 +96,10 @@ class FIRMSAdapter:
|
|||
# Format bbox as west,south,east,north
|
||||
bbox_str = ",".join(str(c) for c in self._bbox)
|
||||
|
||||
url = f"{self._base_url}/{self._map_key}/{self._source}/{bbox_str}/{self._day_range}"
|
||||
if self._map_key:
|
||||
url = f"{self._base_url}/{self._map_key}/{self._source}/{bbox_str}/{self._day_range}"
|
||||
else:
|
||||
url = f"{self._base_url}/{self._source}/{bbox_str}/{self._day_range}"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "MeshAI/1.0",
|
||||
|
|
|
|||
14
work/meshai/env/traffic.py
vendored
14
work/meshai/env/traffic.py
vendored
|
|
@ -74,8 +74,9 @@ class TomTomTrafficAdapter:
|
|||
self._daily_requests = 0
|
||||
self._daily_reset = now
|
||||
|
||||
# No API key or corridors
|
||||
if not self._api_key or not self._corridors:
|
||||
# No corridors configured (the key is optional — a blank key builds
|
||||
# a keyless request for a key-injecting proxy, e.g. Conduit)
|
||||
if not self._corridors:
|
||||
return False
|
||||
|
||||
# Check tick interval
|
||||
|
|
@ -146,11 +147,10 @@ class TomTomTrafficAdapter:
|
|||
Returns:
|
||||
Event dict or None on error
|
||||
"""
|
||||
params = {
|
||||
"point": f"{lat},{lon}",
|
||||
"key": self._api_key,
|
||||
"unit": "MPH",
|
||||
}
|
||||
params = {"point": f"{lat},{lon}"}
|
||||
if self._api_key:
|
||||
params["key"] = self._api_key
|
||||
params["unit"] = "MPH"
|
||||
|
||||
url = f"{self._base_url}?{urlencode(params)}"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Tests for FIRMS adapter Phase 2.6 — to_event() method."""
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -152,3 +152,98 @@ def test_to_event_does_not_raise_on_corrupted_dict(adapter):
|
|||
# Should not raise
|
||||
event = adapter.to_event(evt)
|
||||
assert event is None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# OPTIONAL MAP_KEY — Conduit keyless-request support
|
||||
#
|
||||
# The FIRMS MAP_KEY moves to Conduit's keystore; meshai sends a keyless
|
||||
# request and Conduit injects the key into the URL path downstream. The
|
||||
# key gate must not idle the adapter, and a blank key must build a URL
|
||||
# with NO map_key path segment. A configured key must produce a
|
||||
# byte-identical URL to before (backward compat).
|
||||
# ============================================================
|
||||
|
||||
class _FakeCM:
|
||||
"""Minimal context manager mimicking urlopen()'s return value."""
|
||||
|
||||
def __init__(self, body: bytes):
|
||||
self._body = body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
|
||||
# Empty-body CSV (header only, no data rows) short-circuits _parse_csv
|
||||
# before any fusion/persistence path is touched.
|
||||
_EMPTY_CSV = b"latitude,longitude,confidence,frp,acq_date,acq_time\n"
|
||||
|
||||
|
||||
def test_fetch_url_with_map_key_is_byte_identical(mock_config):
|
||||
"""With MAP_KEY configured, the built URL is unchanged (backward compat)."""
|
||||
adapter = FIRMSAdapter(mock_config, region_anchors=[], fires_adapter=None)
|
||||
captured = {}
|
||||
|
||||
def fake_urlopen(req, timeout=30):
|
||||
captured["url"] = req.full_url
|
||||
return _FakeCM(_EMPTY_CSV)
|
||||
|
||||
with patch("meshai.env.firms.urlopen", side_effect=fake_urlopen):
|
||||
adapter._fetch()
|
||||
|
||||
assert captured["url"] == (
|
||||
"https://firms.modaps.eosdis.nasa.gov/api/area/csv/"
|
||||
"test-key/VIIRS_SNPP_NRT/-117,42,-114,44/1"
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_url_blank_map_key_omits_path_segment(mock_config):
|
||||
"""With a blank MAP_KEY, the URL is built keyless (no map_key path
|
||||
segment) for a key-injecting proxy (Conduit) to complete downstream."""
|
||||
mock_config.map_key = ""
|
||||
adapter = FIRMSAdapter(mock_config, region_anchors=[], fires_adapter=None)
|
||||
captured = {}
|
||||
|
||||
def fake_urlopen(req, timeout=30):
|
||||
captured["url"] = req.full_url
|
||||
return _FakeCM(_EMPTY_CSV)
|
||||
|
||||
with patch("meshai.env.firms.urlopen", side_effect=fake_urlopen):
|
||||
adapter._fetch()
|
||||
|
||||
assert captured["url"] == (
|
||||
"https://firms.modaps.eosdis.nasa.gov/api/area/csv/"
|
||||
"VIIRS_SNPP_NRT/-117,42,-114,44/1"
|
||||
)
|
||||
assert "test-key" not in captured["url"]
|
||||
|
||||
|
||||
def test_tick_does_not_idle_when_map_key_blank(mock_config):
|
||||
"""A blank MAP_KEY must not gate tick(); bbox is the real prerequisite."""
|
||||
mock_config.map_key = ""
|
||||
adapter = FIRMSAdapter(mock_config, region_anchors=[], fires_adapter=None)
|
||||
|
||||
with patch.object(adapter, "_fetch", return_value=False) as fetch:
|
||||
result = adapter.tick()
|
||||
|
||||
fetch.assert_called_once()
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_tick_still_gates_on_no_bbox(mock_config):
|
||||
"""bbox remains a real prerequisite — tick() still idles without it,
|
||||
even with MAP_KEY configured."""
|
||||
mock_config.bbox = []
|
||||
adapter = FIRMSAdapter(mock_config, region_anchors=[], fires_adapter=None)
|
||||
|
||||
with patch.object(adapter, "_fetch") as fetch:
|
||||
result = adapter.tick()
|
||||
|
||||
fetch.assert_not_called()
|
||||
assert result is False
|
||||
|
|
|
|||
|
|
@ -249,3 +249,93 @@ def test_fetch_point_non400_http_error_sets_last_error(mock_config):
|
|||
assert result is None
|
||||
assert adapter._last_error == "HTTP 503"
|
||||
assert adapter._consecutive_errors == 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# OPTIONAL API KEY — Conduit keyless-request support
|
||||
#
|
||||
# TomTom key moves to Conduit's keystore; meshai sends a keyless request
|
||||
# and Conduit injects the key downstream. Key gate must not idle the
|
||||
# adapter, and a blank key must build a keyless URL. A configured key
|
||||
# must produce a byte-identical URL to before (backward compat).
|
||||
# ============================================================
|
||||
|
||||
class _FakeCM:
|
||||
"""Minimal context manager mimicking urlopen()'s return value."""
|
||||
|
||||
def __init__(self, body: bytes):
|
||||
self._body = body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
|
||||
def test_fetch_point_url_with_key_is_byte_identical(mock_config):
|
||||
"""With a key configured, the built URL is unchanged (backward compat)."""
|
||||
adapter = TomTomTrafficAdapter(mock_config)
|
||||
captured = {}
|
||||
|
||||
def fake_urlopen(req, timeout=15):
|
||||
captured["url"] = req.full_url
|
||||
return _FakeCM(b'{"flowSegmentData": {}}')
|
||||
|
||||
with patch("meshai.env.traffic.urlopen", side_effect=fake_urlopen):
|
||||
adapter._fetch_point("Cole Rd", 43.6, -116.3, 0.0)
|
||||
|
||||
assert captured["url"] == (
|
||||
"https://api.tomtom.com/traffic/services/4/flowSegmentData/relative0/10/json"
|
||||
"?point=43.6%2C-116.3&key=test-key&unit=MPH"
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_point_url_blank_key_omits_key_param(mock_config):
|
||||
"""With a blank key, the URL is built keyless (no key= param) for a
|
||||
key-injecting proxy (Conduit) to complete downstream."""
|
||||
mock_config.api_key = ""
|
||||
adapter = TomTomTrafficAdapter(mock_config)
|
||||
captured = {}
|
||||
|
||||
def fake_urlopen(req, timeout=15):
|
||||
captured["url"] = req.full_url
|
||||
return _FakeCM(b'{"flowSegmentData": {}}')
|
||||
|
||||
with patch("meshai.env.traffic.urlopen", side_effect=fake_urlopen):
|
||||
adapter._fetch_point("Cole Rd", 43.6, -116.3, 0.0)
|
||||
|
||||
assert captured["url"] == (
|
||||
"https://api.tomtom.com/traffic/services/4/flowSegmentData/relative0/10/json"
|
||||
"?point=43.6%2C-116.3&unit=MPH"
|
||||
)
|
||||
assert "key=" not in captured["url"]
|
||||
|
||||
|
||||
def test_tick_does_not_idle_when_key_blank(mock_config):
|
||||
"""A blank key must not gate tick(); corridors is the real prerequisite."""
|
||||
mock_config.api_key = ""
|
||||
mock_config.corridors = [{"name": "Cole Rd", "lat": 43.6, "lon": -116.3}]
|
||||
adapter = TomTomTrafficAdapter(mock_config)
|
||||
|
||||
with patch.object(adapter, "_fetch_all", return_value=True) as fetch_all:
|
||||
result = adapter.tick()
|
||||
|
||||
fetch_all.assert_called_once()
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_tick_still_gates_on_no_corridors(mock_config):
|
||||
"""Corridors remain a real prerequisite — tick() still idles without them,
|
||||
even with a key configured."""
|
||||
mock_config.corridors = []
|
||||
adapter = TomTomTrafficAdapter(mock_config)
|
||||
|
||||
with patch.object(adapter, "_fetch_all") as fetch_all:
|
||||
result = adapter.tick()
|
||||
|
||||
fetch_all.assert_not_called()
|
||||
assert result is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue