From 20d64a880a3b05bea71885cecbefe8a218395fbe Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 16 Jun 2026 16:57:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=9F=8E=E5=B8=82=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=AE=9E=E6=97=B6=E8=A7=82=E6=B5=8B=E5=8F=A0=E5=8A=A0?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_web_observability.py | 121 +++++++++++++++++++++++++++ web/services/city_api.py | 139 +++++++++++--------------------- 2 files changed, 170 insertions(+), 90 deletions(-) diff --git a/tests/test_web_observability.py b/tests/test_web_observability.py index e5f1b4a0..1dce203c 100644 --- a/tests/test_web_observability.py +++ b/tests/test_web_observability.py @@ -1518,6 +1518,127 @@ def test_chart_data_cache_hit_overlays_latest_cwa_from_airport_obs_log(monkeypat assert payload["airport_current"]["source_code"] == "cwa" +def test_full_detail_batch_overlays_latest_official_observations_from_airport_obs_log(monkeypatch): + import asyncio + + class FakeCache: + def get_city_cache(self, kind, city): + assert kind == "full" + if city == "tokyo": + return { + "payload": { + "name": city, + "display_name": "Tokyo", + "temp_symbol": "°C", + "local_date": "2026-06-14", + "local_time": "19:00", + "current": { + "temp": 23.0, + "source_code": "metar", + "obs_time": "2026-06-14T10:00:00+00:00", + }, + "airport_current": { + "temp": 23.0, + "source_code": "metar", + "obs_time": "2026-06-14T10:00:00+00:00", + }, + }, + } + return { + "payload": { + "name": city, + "display_name": "Taipei", + "temp_symbol": "°C", + "local_date": "2026-06-14", + "local_time": "18:00", + "current": { + "temp": 26.0, + "source_code": "cwa", + "obs_time": "2026-06-14T10:00:00+00:00", + }, + "airport_current": { + "temp": 26.0, + "source_code": "cwa", + "obs_time": "2026-06-14T10:00:00+00:00", + }, + }, + } + + def get_latest_raw_observation(self, source, city): + return None + + def get_airport_obs_recent(self, icao, minutes=30): + if icao == "44166": + return [ + { + "icao": "44166", + "city": "tokyo", + "temp_c": 24.0, + "obs_time": "2026-06-16T06:00:00+09:00", + "created_at": "2026-06-15T21:00:15+00:00", + } + ] + if icao == "466920": + return [ + { + "icao": "466920", + "city": "taipei", + "temp_c": 29.4, + "obs_time": "2026-06-16T15:30:00+08:00", + "created_at": "2026-06-16T07:30:15+00:00", + } + ] + return [] + + class FakeWeather: + def fetch_jma_amedas_official_nearby(self, city, use_fahrenheit=False): + return [] + + def fetch_jma_amedas_current(self, city, use_fahrenheit=False): + return None + + def fetch_cwa_taipei_settlement_current(self): + return None + + city_api._CITY_DETAIL_PAYLOAD_CACHE.clear() + city_api._CITY_DETAIL_PAYLOAD_CACHE_TS.clear() + monkeypatch.setattr(city_api.legacy_routes, "_CACHE_DB", FakeCache()) + monkeypatch.setattr(city_api.legacy_routes, "_weather", FakeWeather()) + monkeypatch.setattr( + city_api.legacy_routes, + "_overlay_latest_wunderground_current", + lambda city, payload: payload, + ) + + _, tokyo = asyncio.run( + city_api._build_city_detail_batch_item_async( + "tokyo", + force_refresh=False, + market_slug=None, + target_date=None, + resolution="10m", + detail_scope="full", + ) + ) + _, taipei = asyncio.run( + city_api._build_city_detail_batch_item_async( + "taipei", + force_refresh=False, + market_slug=None, + target_date=None, + resolution="10m", + detail_scope="full", + ) + ) + + assert tokyo["overview"]["local_date"] == "2026-06-16" + assert tokyo["airport_current"]["source_code"] == "jma_amedas" + assert tokyo["airport_current"]["temp"] == 24.0 + assert taipei["overview"]["local_date"] == "2026-06-16" + assert taipei["airport_current"]["source_code"] == "cwa" + assert taipei["airport_current"]["temp"] == 29.4 + + def test_chart_data_returns_cached_payload_when_optional_overlay_times_out(monkeypatch): import asyncio diff --git a/web/services/city_api.py b/web/services/city_api.py index f9072c2d..24bd0c7d 100644 --- a/web/services/city_api.py +++ b/web/services/city_api.py @@ -138,6 +138,52 @@ async def _run_latest_observation_city_chart_overlay( return payload +async def _overlay_latest_observation_sources(city: str, payload: Dict[str, Any]) -> Dict[str, Any]: + latest_payload = payload + latest_payload = await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="amsc_latest_raw", + payload=latest_payload, + fn=overlay_latest_amsc_observation, + args=(legacy_routes._CACHE_DB, city, latest_payload), + ) + latest_payload = await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="jma_amedas_latest", + payload=latest_payload, + fn=overlay_latest_jma_amedas_observation, + args=(legacy_routes._weather, city, latest_payload, legacy_routes._CACHE_DB), + ) + latest_payload = await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="amos_latest_raw", + payload=latest_payload, + fn=overlay_latest_amos_observation, + args=(legacy_routes._CACHE_DB, city, latest_payload), + ) + latest_payload = await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="mgm_latest_raw", + payload=latest_payload, + fn=overlay_latest_mgm_observation, + args=(legacy_routes._CACHE_DB, city, latest_payload), + ) + latest_payload = await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="hko_latest_raw", + payload=latest_payload, + fn=overlay_latest_hko_observation, + args=(legacy_routes._CACHE_DB, city, latest_payload), + ) + return await _run_latest_observation_city_chart_overlay( + city=city, + overlay_name="cwa_taipei", + payload=latest_payload, + fn=overlay_latest_cwa_observation, + args=(legacy_routes._weather, city, latest_payload, legacy_routes._CACHE_DB), + ) + + async def _get_cached_city_payload(city: str, kind: str) -> Dict[str, Any]: cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, kind, city) if not isinstance(cached_entry, dict): @@ -327,12 +373,7 @@ def _start_city_cache_stale_refresh( async def _overlay_cached_wunderground(city: str, payload: Dict[str, Any]) -> Dict[str, Any]: latest_payload = await _overlay_cached_canonical_observation(city, payload) - latest_payload = await run_in_threadpool( - overlay_latest_amsc_observation, - legacy_routes._CACHE_DB, - city, - latest_payload, - ) + latest_payload = await _overlay_latest_observation_sources(city, latest_payload) return await run_in_threadpool( legacy_routes._overlay_latest_wunderground_current, city, @@ -681,48 +722,7 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=_overlay_cached_runway_history_from_db, args=(city, payload), ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="amsc_latest_raw", - payload=payload, - fn=overlay_latest_amsc_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="jma_amedas_latest", - payload=payload, - fn=overlay_latest_jma_amedas_observation, - args=(legacy_routes._weather, city, payload, legacy_routes._CACHE_DB), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="amos_latest_raw", - payload=payload, - fn=overlay_latest_amos_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="mgm_latest_raw", - payload=payload, - fn=overlay_latest_mgm_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="hko_latest_raw", - payload=payload, - fn=overlay_latest_hko_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="cwa_taipei", - payload=payload, - fn=overlay_latest_cwa_observation, - args=(legacy_routes._weather, city, payload, legacy_routes._CACHE_DB), - ) + payload = await _overlay_latest_observation_sources(city, payload) return await _run_optional_city_chart_overlay( city=city, overlay_name="wunderground_current", @@ -745,48 +745,7 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=_overlay_cached_runway_history_from_db, args=(city, payload), ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="amsc_latest_raw", - payload=payload, - fn=overlay_latest_amsc_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="jma_amedas_latest", - payload=payload, - fn=overlay_latest_jma_amedas_observation, - args=(legacy_routes._weather, city, payload, legacy_routes._CACHE_DB), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="amos_latest_raw", - payload=payload, - fn=overlay_latest_amos_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="mgm_latest_raw", - payload=payload, - fn=overlay_latest_mgm_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="hko_latest_raw", - payload=payload, - fn=overlay_latest_hko_observation, - args=(legacy_routes._CACHE_DB, city, payload), - ) - payload = await _run_latest_observation_city_chart_overlay( - city=city, - overlay_name="cwa_taipei", - payload=payload, - fn=overlay_latest_cwa_observation, - args=(legacy_routes._weather, city, payload, legacy_routes._CACHE_DB), - ) + payload = await _overlay_latest_observation_sources(city, payload) return await _run_optional_city_chart_overlay( city=city, overlay_name="wunderground_current",