From 3984e12c44c23ee4d05981cb025a574177f9705d Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 15 Jun 2026 23:25:50 +0800 Subject: [PATCH] Clear stale Ankara source fields --- tests/test_web_observability.py | 26 ++++++++ web/services/city_api.py | 111 +++++++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/tests/test_web_observability.py b/tests/test_web_observability.py index c0dfc825..2f852f0d 100644 --- a/tests/test_web_observability.py +++ b/tests/test_web_observability.py @@ -1843,18 +1843,34 @@ def test_stale_ankara_chart_data_overlays_latest_mgm_canonical(monkeypatch): "local_time": "13:12", "current": { "temp": 16.0, + "max_temp_time": "12:50", + "raw_max_so_far": 16.0, + "wu_settlement": 16, "source_code": "metar", "settlement_source": "metar", "settlement_source_label": "METAR", "observed_at": "2026-06-14T09:50:00+00:00", + "report_time": "2026-06-14T09:50:00+00:00", + "raw_metar": "METAR LTAC 140950Z 06013KT 9999 16/11 Q1015", }, "airport_primary": { "temp": 16.0, "source_code": "metar", "source_label": "METAR", "obs_time": "2026-06-14T09:50:00+00:00", + "report_time": "2026-06-14T09:50:00+00:00", + "raw_metar": "METAR LTAC 140950Z 06013KT 9999 16/11 Q1015", }, "airport_primary_today_obs": [{"time": "12:50", "temp": 16.0}], + "metar_today_obs": [{"time": "12:50", "temp": 16.0}], + "metar_recent_obs": [{"time": "12:50", "temp": 16.0}], + "metar_status": { + "available_for_today": True, + "stale_for_today": False, + "last_observation_local_date": "2026-06-14", + "current_local_date": "2026-06-14", + }, + "mgm": {"temp": 16.7, "time": "12:50", "hourly": [{"time": "12:00", "temp": 16.5}]}, "hourly": {"times": ["2026-06-14T09:00:00Z"], "temps": [16.0]}, "deb": {"prediction": 23.0}, }, @@ -1901,6 +1917,16 @@ def test_stale_ankara_chart_data_overlays_latest_mgm_canonical(monkeypatch): assert payload["local_date"] == "2026-06-15" assert payload["local_time"] == "17:20" assert payload["airport_primary_today_obs"] == [{"time": "17:20", "temp": 19.0}] + assert "raw_metar" not in payload["current"] + assert "report_time" not in payload["airport_primary"] + assert payload["metar_today_obs"] == [] + assert payload["metar_recent_obs"] == [] + assert payload["metar_status"]["available_for_today"] is False + assert payload["metar_status"]["stale_for_today"] is True + assert payload["metar_status"]["current_local_date"] == "2026-06-15" + assert payload["mgm"]["temp"] == 19.0 + assert payload["mgm"]["time"] == "17:20" + assert payload["mgm"].get("hourly") == [] assert payload["deb"]["prediction"] == 23.0 diff --git a/web/services/city_api.py b/web/services/city_api.py index 53ef4a92..bd65346c 100644 --- a/web/services/city_api.py +++ b/web/services/city_api.py @@ -341,6 +341,31 @@ def _payload_observation_epoch(payload: Dict[str, Any]) -> Optional[int]: return max(epochs) if epochs else None +_SOURCE_BOUND_OBSERVATION_FIELDS = { + "altim", + "cloud_desc", + "clouds", + "clouds_raw", + "current_local_date", + "humidity", + "last_observation_local_date", + "max_temp_time", + "obs_time_epoch", + "pressure_hpa", + "raw_max_so_far", + "raw_metar", + "receipt_time", + "report_time", + "stale_for_today", + "visibility_km", + "visibility_mi", + "wind_dir", + "wind_speed_kt", + "wu_settlement", + "wx_desc", +} + + def _float_or_none(value: Any) -> Optional[float]: try: if value is None or value == "": @@ -385,6 +410,29 @@ def _latest_airport_primary_point( return [{"time": local_time, "temp": round(float(temp), 1)}] +def _observation_source_code(block: Any) -> str: + if not isinstance(block, dict): + return "" + for key in ("source_code", "settlement_source", "source"): + value = str(block.get(key) or "").strip().lower() + if value: + return value + return "" + + +def _merge_latest_observation_block(base_block: Any, latest_block: Dict[str, Any]) -> Dict[str, Any]: + base = dict(base_block) if isinstance(base_block, dict) else {} + latest_source = _observation_source_code(latest_block) + base_source = _observation_source_code(base) + if base_source and latest_source and base_source != latest_source: + base = { + key: value + for key, value in base.items() + if key not in _SOURCE_BOUND_OBSERVATION_FIELDS + } + return {**base, **latest_block} + + def _replace_airport_primary_today_obs( payload: Dict[str, Any], points: List[Dict[str, Any]], @@ -395,6 +443,60 @@ def _replace_airport_primary_today_obs( official["airport_primary_today_obs"] = points +def _clear_previous_day_observation_series(payload: Dict[str, Any], *, local_date: str) -> None: + for key in ("metar_today_obs", "metar_recent_obs", "settlement_today_obs"): + if key in payload: + payload[key] = [] + timeseries = payload.get("timeseries") + if isinstance(timeseries, dict): + for key in ("metar_today_obs", "metar_recent_obs", "settlement_today_obs"): + if key in timeseries: + timeseries[key] = [] + metar_status = payload.get("metar_status") + if isinstance(metar_status, dict): + metar_status["available_for_today"] = False + metar_status["stale_for_today"] = True + metar_status["current_local_date"] = local_date + + +def _sync_latest_mgm_summary( + payload: Dict[str, Any], + latest_payload: Dict[str, Any], + *, + local_time: str, +) -> None: + latest_current = latest_payload.get("current") if isinstance(latest_payload.get("current"), dict) else {} + latest_airport = ( + latest_payload.get("airport_primary") + if isinstance(latest_payload.get("airport_primary"), dict) + else {} + ) + latest_source = _observation_source_code(latest_current) or _observation_source_code(latest_airport) + if latest_source != "mgm": + return + temp = _float_or_none(latest_airport.get("temp") if latest_airport else None) + if temp is None: + temp = _float_or_none(latest_current.get("temp") if latest_current else None) + if temp is None: + return + payload["mgm"] = { + "temp": round(float(temp), 1), + "time": local_time, + "feels_like": round(float(temp), 1), + "humidity": None, + "wind_dir": None, + "wind_speed_ms": None, + "pressure": None, + "cloud_cover": None, + "rain_24h": None, + "today_high": None, + "today_low": None, + "station_code": latest_airport.get("station_code") or latest_current.get("station_code"), + "station_name": latest_airport.get("station_name") or latest_current.get("station_name"), + "hourly": [], + } + + def _merge_latest_observation_payload( city: str, payload: Dict[str, Any], @@ -414,8 +516,7 @@ def _merge_latest_observation_payload( latest_block = latest_payload.get(key) if not isinstance(latest_block, dict) or not latest_block: continue - base_block = next_payload.get(key) if isinstance(next_payload.get(key), dict) else {} - next_payload[key] = {**base_block, **latest_block} + next_payload[key] = _merge_latest_observation_block(next_payload.get(key), latest_block) if isinstance(latest_payload.get("canonical_temperature"), dict): next_payload["canonical_temperature"] = latest_payload["canonical_temperature"] if latest_payload.get("updated_at"): @@ -435,6 +536,12 @@ def _merge_latest_observation_payload( local_time=local_context["local_time"], ), ) + _clear_previous_day_observation_series(next_payload, local_date=local_context["local_date"]) + _sync_latest_mgm_summary( + next_payload, + latest_payload, + local_time=local_context["local_time"], + ) return next_payload