Expose station network coverage and settlement station details
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
from src.data_collection.country_networks import build_country_network_snapshot
|
||||
from web.analysis_service import _build_city_detail_payload
|
||||
|
||||
|
||||
def test_turkey_mgm_provider_returns_official_nearby_rows():
|
||||
raw = {
|
||||
"metar": {
|
||||
"observation_time": "2026-04-06T10:00:00.000Z",
|
||||
"current": {"temp": 16.0},
|
||||
},
|
||||
"mgm_nearby": [
|
||||
{
|
||||
"name": "Airport (MGM/17128)",
|
||||
"istNo": "17128",
|
||||
"lat": 40.1,
|
||||
"lon": 32.9,
|
||||
"temp": 17.1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
snapshot = build_country_network_snapshot("ankara", raw)
|
||||
|
||||
assert snapshot["provider_code"] == "turkey_mgm"
|
||||
assert snapshot["official_network_status"]["available"] is True
|
||||
assert snapshot["official_nearby"][0]["source_code"] == "mgm"
|
||||
assert snapshot["official_nearby"][0]["is_official"] is True
|
||||
|
||||
|
||||
def test_china_provider_falls_back_to_metar_cluster_without_replacing_airport_anchor():
|
||||
raw = {
|
||||
"metar": {
|
||||
"observation_time": "2026-04-06T10:00:00.000Z",
|
||||
"current": {"temp": 22.5},
|
||||
},
|
||||
"mgm_nearby": [
|
||||
{
|
||||
"name": "Hongqiao",
|
||||
"icao": "ZSSS",
|
||||
"lat": 31.2,
|
||||
"lon": 121.3,
|
||||
"temp": 23.1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
snapshot = build_country_network_snapshot("shanghai", raw)
|
||||
|
||||
assert snapshot["provider_code"] == "china_cma"
|
||||
assert snapshot["airport_primary_current"]["source_code"] == "metar"
|
||||
assert snapshot["airport_primary_current"]["is_airport_station"] is True
|
||||
assert snapshot["official_network_status"]["mode"] == "fallback_metar_cluster"
|
||||
assert snapshot["official_nearby"][0]["source_code"] == "metar_cluster"
|
||||
assert snapshot["official_nearby"][0]["is_official"] is False
|
||||
|
||||
|
||||
def test_china_provider_prefers_nmc_rows_when_available():
|
||||
raw = {
|
||||
"metar": {
|
||||
"observation_time": "2026-04-06T10:00:00.000Z",
|
||||
"current": {"temp": 22.5},
|
||||
},
|
||||
"nmc_official_nearby": [
|
||||
{
|
||||
"name": "浦东 (NMC)",
|
||||
"icao": "atcMf",
|
||||
"lat": 31.14,
|
||||
"lon": 121.80,
|
||||
"temp": 17.9,
|
||||
"obs_time": "2026-04-06 06:50",
|
||||
}
|
||||
],
|
||||
"mgm_nearby": [
|
||||
{
|
||||
"name": "Hongqiao",
|
||||
"icao": "ZSSS",
|
||||
"lat": 31.2,
|
||||
"lon": 121.3,
|
||||
"temp": 23.1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
snapshot = build_country_network_snapshot("shanghai", raw)
|
||||
|
||||
assert snapshot["provider_code"] == "china_cma"
|
||||
assert snapshot["official_network_status"]["available"] is True
|
||||
assert snapshot["official_network_status"]["mode"] == "official_active"
|
||||
assert snapshot["official_nearby"][0]["source_code"] == "nmc"
|
||||
assert snapshot["official_nearby"][0]["is_official"] is True
|
||||
|
||||
|
||||
def test_hko_provider_marks_explicit_official_station_as_anchor():
|
||||
raw = {
|
||||
"settlement_current": {
|
||||
"station_code": "LFS",
|
||||
"station_name": "Lau Fau Shan",
|
||||
"observation_time": "2026-04-06T10:00:00+08:00",
|
||||
"current": {"temp": 25.0},
|
||||
}
|
||||
}
|
||||
|
||||
snapshot = build_country_network_snapshot("lau fau shan", raw)
|
||||
|
||||
assert snapshot["provider_code"] == "hongkong_hko"
|
||||
assert snapshot["settlement_station"]["is_official_station_anchor"] is True
|
||||
assert snapshot["official_nearby"][0]["is_settlement_anchor"] is True
|
||||
assert snapshot["official_nearby"][0]["station_code"] == "LFS"
|
||||
|
||||
|
||||
def test_city_detail_payload_exposes_airport_and_official_network_layers():
|
||||
payload = _build_city_detail_payload(
|
||||
{
|
||||
"name": "ankara",
|
||||
"display_name": "Ankara",
|
||||
"local_time": "12:00",
|
||||
"local_date": "2026-04-06",
|
||||
"temp_symbol": "°C",
|
||||
"updated_at": "2026-04-06T04:00:00Z",
|
||||
"current": {
|
||||
"temp": 16.0,
|
||||
"settlement_source": "metar",
|
||||
"settlement_source_label": "METAR",
|
||||
},
|
||||
"risk": {"icao": "LTAC", "airport": "Esenboga", "level": "medium", "warning": ""},
|
||||
"airport_primary": {"temp": 16.0, "source_code": "metar"},
|
||||
"airport_primary_today_obs": [{"time": "10:00", "temp": 16.0}],
|
||||
"official_nearby": [{"station_code": "17128", "temp": 17.2, "source_code": "mgm"}],
|
||||
"official_network_source": "turkey_mgm",
|
||||
"official_network_status": {"provider_code": "turkey_mgm", "available": True},
|
||||
"network_lead_signal": {"available": True, "delta": 1.2},
|
||||
"network_spread_signal": {"available": True, "spread": 2.1},
|
||||
"center_station_candidate": {"station_code": "17128", "temp": 17.2},
|
||||
"airport_vs_network_delta": 1.2,
|
||||
"settlement_station": {
|
||||
"settlement_station_code": "LTAC",
|
||||
"settlement_station_label": "Ankara Esenboga Airport",
|
||||
"is_airport_anchor": True,
|
||||
},
|
||||
"probabilities": {"distribution": []},
|
||||
"multi_model": {},
|
||||
"multi_model_daily": {},
|
||||
"dynamic_commentary": {"summary": "", "notes": []},
|
||||
"taf": {},
|
||||
}
|
||||
)
|
||||
|
||||
assert payload["official"]["airport_primary"]["source_code"] == "metar"
|
||||
assert payload["official"]["official_nearby"][0]["source_code"] == "mgm"
|
||||
assert payload["settlement_station"]["settlement_station_code"] == "LTAC"
|
||||
@@ -0,0 +1,99 @@
|
||||
import threading
|
||||
|
||||
from src.data_collection.nmc_sources import NmcSourceMixin
|
||||
|
||||
|
||||
class _DummyResponse:
|
||||
def __init__(self, *, text="", payload=None, status_code=200):
|
||||
self.text = text
|
||||
self._payload = payload
|
||||
self.status_code = status_code
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise RuntimeError(f"HTTP {self.status_code}")
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
class _DummySession:
|
||||
def __init__(self, mapping):
|
||||
self.mapping = mapping
|
||||
|
||||
def get(self, url, timeout=None):
|
||||
if url not in self.mapping:
|
||||
raise AssertionError(f"unexpected url {url}")
|
||||
return self.mapping[url]
|
||||
|
||||
|
||||
class _DummyCollector(NmcSourceMixin):
|
||||
CITY_REGISTRY = {
|
||||
"shanghai": {"lat": 31.1434, "lon": 121.8052},
|
||||
}
|
||||
|
||||
def __init__(self, mapping):
|
||||
self.session = _DummySession(mapping)
|
||||
self.timeout = 5
|
||||
self.nmc_cache_ttl_sec = 300
|
||||
self._nmc_cache = {}
|
||||
self._nmc_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
def test_fetch_nmc_region_current_parses_rest_payload():
|
||||
collector = _DummyCollector(
|
||||
{
|
||||
"https://www.nmc.cn/rest/real/atcMf": _DummyResponse(
|
||||
payload={
|
||||
"station": {"code": "atcMf", "city": "浦东"},
|
||||
"publish_time": "2026-04-06 06:50",
|
||||
"weather": {
|
||||
"temperature": 17.9,
|
||||
"humidity": 83.0,
|
||||
"rain": 0.0,
|
||||
"airpressure": 9999.0,
|
||||
"info": "多云",
|
||||
},
|
||||
"wind": {"direct": "东北风", "power": "3级"},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
out = collector.fetch_nmc_region_current("shanghai")
|
||||
|
||||
assert out is not None
|
||||
assert out["source"] == "nmc"
|
||||
assert out["station_code"] == "atcMf"
|
||||
assert out["current"]["temp"] == 17.9
|
||||
assert out["current"]["humidity"] == 83.0
|
||||
assert out["current"]["airpressure"] is None
|
||||
|
||||
|
||||
def test_fetch_nmc_official_nearby_returns_normalized_row():
|
||||
collector = _DummyCollector(
|
||||
{
|
||||
"https://www.nmc.cn/rest/real/atcMf": _DummyResponse(
|
||||
payload={
|
||||
"station": {"code": "atcMf", "city": "浦东"},
|
||||
"publish_time": "2026-04-06 06:50",
|
||||
"weather": {
|
||||
"temperature": 17.9,
|
||||
"humidity": 83.0,
|
||||
"rain": 0.0,
|
||||
"airpressure": 9999.0,
|
||||
"info": "多云",
|
||||
},
|
||||
"wind": {"direct": "东北风", "power": "3级"},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
rows = collector.fetch_nmc_official_nearby("shanghai")
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["source"] == "nmc"
|
||||
assert rows[0]["temp"] == 17.9
|
||||
assert rows[0]["name"] == "浦东 (NMC)"
|
||||
assert rows[0]["lat"] == 31.1434
|
||||
@@ -32,6 +32,7 @@ def test_system_status_returns_summary_shape():
|
||||
assert 'rollout' in payload['probability']
|
||||
assert payload['probability']['rollout']['decision']['decision'] in {'hold', 'observe', 'promote'}
|
||||
assert 'training_data' in payload
|
||||
assert 'station_networks' in payload
|
||||
assert 'truth_records' in payload['training_data']
|
||||
assert 'training_features' in payload['training_data']
|
||||
assert 'city_coverage' in payload['training_data']
|
||||
@@ -52,6 +53,7 @@ def test_cities_endpoint_uses_denver_display_name_for_aurora_market():
|
||||
payload = response.json()
|
||||
aurora = next(item for item in payload["cities"] if item["name"] == "aurora")
|
||||
assert aurora["display_name"] == "Denver"
|
||||
assert aurora["network_provider"] == "global_metar"
|
||||
assert aurora["deb_recent_tier"] in {"high", "medium", "low", "other"}
|
||||
assert "deb_recent_sample_count" in aurora
|
||||
|
||||
@@ -244,3 +246,5 @@ def test_city_history_is_read_only_and_uses_sqlite_truth_and_features(monkeypatc
|
||||
assert row["deb"] == 16.4
|
||||
assert row["mu"] == 16.2
|
||||
assert row["forecasts"]["Open-Meteo"] == 15.8
|
||||
assert row["settlement_station_code"] == "LTAC"
|
||||
assert row["truth_version"] == "v1"
|
||||
|
||||
Reference in New Issue
Block a user