Refresh product docs and observation freshness
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from web.services.observation_freshness import build_observation_freshness
|
||||
|
||||
|
||||
def test_amsc_awos_freshness_uses_three_minute_native_cadence():
|
||||
freshness = build_observation_freshness(
|
||||
source_code="amsc_awos",
|
||||
source_label="AMSC AWOS",
|
||||
observed_at="2026-06-06T13:01:00Z",
|
||||
now_utc=datetime(2026, 6, 6, 13, 4, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
assert freshness["source_code"] == "amsc_awos"
|
||||
assert freshness["native_update_interval_sec"] == 180
|
||||
assert freshness["expected_next_update_at"] == "2026-06-06T13:04:00+00:00"
|
||||
assert freshness["freshness_status"] == "fresh"
|
||||
@@ -103,6 +103,47 @@ def test_patch_adds_city_local_time_contract_from_observation_time():
|
||||
assert event["payload"]["observed_at_local"] == "2026-05-27T19:16:00-04:00"
|
||||
|
||||
|
||||
def test_patch_records_received_time_and_latency_for_late_runway_points(monkeypatch):
|
||||
monkeypatch.setattr("web.realtime_patch_schema.time.time", lambda: 1780750864.062)
|
||||
|
||||
event = normalize_observation_patch(
|
||||
{
|
||||
"city": "Busan",
|
||||
"changes": {
|
||||
"temp": 23.0,
|
||||
"obs_time": "2026-06-06T12:59:00Z",
|
||||
"source": "amos",
|
||||
"amos": {
|
||||
"runway_obs": {
|
||||
"point_temperatures": [{"runway": "SR/SL", "temp": 22.7}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert event["received_at_utc"] == "2026-06-06T13:01:04Z"
|
||||
assert event["latency_sec"] == 124
|
||||
assert event["payload"]["received_at_utc"] == "2026-06-06T13:01:04Z"
|
||||
assert event["payload"]["latency_sec"] == 124
|
||||
|
||||
|
||||
def test_amsc_patch_uses_three_minute_source_cadence():
|
||||
event = normalize_observation_patch(
|
||||
{
|
||||
"city": "Shanghai",
|
||||
"changes": {
|
||||
"temp": 22.4,
|
||||
"obs_time": "2026-06-06T13:01:00Z",
|
||||
"source": "amsc_awos",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert event["source_cadence_sec"] == 180
|
||||
assert event["payload"]["source_cadence_sec"] == 180
|
||||
|
||||
|
||||
def test_invalid_patch_without_city_or_observation_data_is_rejected():
|
||||
with pytest.raises(PatchValidationError):
|
||||
normalize_observation_patch({"changes": {"temp": 21.0}})
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.data_collection.weather_sources import WeatherDataCollector
|
||||
|
||||
|
||||
def test_collector_patch_post_retries_transient_failures(monkeypatch):
|
||||
collector = WeatherDataCollector({})
|
||||
collector.collector_patch_endpoint = "http://internal.local/api/internal/collector-patch"
|
||||
calls = []
|
||||
|
||||
def fake_post(url, *, json, timeout):
|
||||
calls.append((url, json, timeout))
|
||||
if len(calls) < 3:
|
||||
raise RuntimeError("temporary 502")
|
||||
return SimpleNamespace(status_code=200, text="ok")
|
||||
|
||||
monkeypatch.setattr("src.data_collection.weather_sources.requests.post", fake_post)
|
||||
monkeypatch.setattr("src.data_collection.weather_sources.time.sleep", lambda _seconds: None)
|
||||
|
||||
sent = collector._post_temperature_patch_payload(
|
||||
{"city": "busan", "changes": {"temp": 23.0}},
|
||||
city_value="busan",
|
||||
source_value="amos",
|
||||
)
|
||||
|
||||
assert sent is True
|
||||
assert len(calls) == 3
|
||||
|
||||
|
||||
def test_collector_patch_post_retries_internal_server_errors(monkeypatch):
|
||||
collector = WeatherDataCollector({})
|
||||
collector.collector_patch_endpoint = "http://internal.local/api/internal/collector-patch"
|
||||
calls = []
|
||||
|
||||
def fake_post(url, *, json, timeout):
|
||||
calls.append((url, json, timeout))
|
||||
status = 502 if len(calls) == 1 else 204
|
||||
return SimpleNamespace(status_code=status, text="bad gateway" if status == 502 else "")
|
||||
|
||||
monkeypatch.setattr("src.data_collection.weather_sources.requests.post", fake_post)
|
||||
monkeypatch.setattr("src.data_collection.weather_sources.time.sleep", lambda _seconds: None)
|
||||
|
||||
sent = collector._post_temperature_patch_payload(
|
||||
{"city": "shanghai", "changes": {"temp": 22.4}},
|
||||
city_value="shanghai",
|
||||
source_value="amsc_awos",
|
||||
)
|
||||
|
||||
assert sent is True
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_failed_collector_patch_clears_dedupe_for_next_attempt(monkeypatch):
|
||||
collector = WeatherDataCollector({})
|
||||
collector.collector_patch_endpoint = "http://internal.local/api/internal/collector-patch"
|
||||
calls = []
|
||||
|
||||
class ImmediateThread:
|
||||
def __init__(self, target, daemon):
|
||||
self._target = target
|
||||
self.daemon = daemon
|
||||
|
||||
def start(self):
|
||||
self._target()
|
||||
|
||||
def fake_post(payload, *, city_value, source_value):
|
||||
calls.append((payload, city_value, source_value))
|
||||
return False
|
||||
|
||||
monkeypatch.setattr("src.data_collection.weather_sources.threading.Thread", ImmediateThread)
|
||||
monkeypatch.setattr(collector, "_post_temperature_patch_payload", fake_post)
|
||||
|
||||
collector._emit_temperature_patch_if_changed(
|
||||
"busan",
|
||||
23.0,
|
||||
"2026-06-06T13:01:00Z",
|
||||
source="amos",
|
||||
)
|
||||
collector._emit_temperature_patch_if_changed(
|
||||
"busan",
|
||||
23.0,
|
||||
"2026-06-06T13:01:00Z",
|
||||
source="amos",
|
||||
)
|
||||
|
||||
assert len(calls) == 2
|
||||
Reference in New Issue
Block a user