Extract forecast source bundle from collector
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def _open_meteo_cache_key(
|
||||
lat: float,
|
||||
lon: float,
|
||||
*,
|
||||
forecast_days: int = 14,
|
||||
use_fahrenheit: bool = False,
|
||||
) -> str:
|
||||
return (
|
||||
f"{round(float(lat), 4)}:{round(float(lon), 4)}:"
|
||||
f"{forecast_days}:{'f' if use_fahrenheit else 'c'}"
|
||||
)
|
||||
|
||||
|
||||
def _multi_model_cache_key(
|
||||
collector: Any,
|
||||
city: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
*,
|
||||
use_fahrenheit: bool = False,
|
||||
) -> str:
|
||||
cache_city = str(city or "").strip().lower()
|
||||
return (
|
||||
f"{round(float(lat), 4)}:{round(float(lon), 4)}:{cache_city}:"
|
||||
f"{'f' if use_fahrenheit else 'c'}:{collector.multi_model_cache_version}"
|
||||
)
|
||||
|
||||
|
||||
def _read_open_meteo_bundle_from_cache(
|
||||
collector: Any,
|
||||
*,
|
||||
city: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
use_fahrenheit: bool,
|
||||
include_multi_model: bool,
|
||||
) -> Dict[str, Any]:
|
||||
collector._maybe_reload_open_meteo_disk_cache()
|
||||
results: Dict[str, Any] = {}
|
||||
om_key = _open_meteo_cache_key(lat, lon, use_fahrenheit=use_fahrenheit)
|
||||
with collector._open_meteo_cache_lock:
|
||||
om_cached = collector._open_meteo_cache.get(om_key)
|
||||
|
||||
if not om_cached or not isinstance(om_cached.get("data"), dict):
|
||||
return results
|
||||
|
||||
results["open-meteo"] = dict(om_cached["data"])
|
||||
if include_multi_model:
|
||||
mm_key = _multi_model_cache_key(
|
||||
collector,
|
||||
city,
|
||||
lat,
|
||||
lon,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
with collector._multi_model_cache_lock:
|
||||
mm_cached = collector._multi_model_cache.get(mm_key)
|
||||
if mm_cached and isinstance(mm_cached.get("data"), dict):
|
||||
results["multi_model"] = dict(mm_cached["data"])
|
||||
return results
|
||||
|
||||
|
||||
def fetch_open_meteo_forecast_bundle(
|
||||
collector: Any,
|
||||
*,
|
||||
city: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
use_fahrenheit: bool,
|
||||
include_multi_model: bool = True,
|
||||
cache_only: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Fetch non-high-frequency Open-Meteo forecast payloads for a city.
|
||||
|
||||
Observation-only refreshes can set *cache_only* so high-frequency callers
|
||||
reuse existing model data without making outbound Open-Meteo requests.
|
||||
"""
|
||||
if lat is None or lon is None:
|
||||
return {}
|
||||
|
||||
if cache_only:
|
||||
return _read_open_meteo_bundle_from_cache(
|
||||
collector,
|
||||
city=city,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
include_multi_model=include_multi_model,
|
||||
)
|
||||
|
||||
results: Dict[str, Any] = {}
|
||||
# Populate the richer multi-model cache before the regular forecast
|
||||
# endpoint can trip the shared Open-Meteo cooldown.
|
||||
if include_multi_model:
|
||||
multi_model_data = collector.fetch_multi_model(
|
||||
lat,
|
||||
lon,
|
||||
city=city,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if multi_model_data:
|
||||
results["multi_model"] = multi_model_data
|
||||
|
||||
open_meteo = collector.fetch_from_open_meteo(
|
||||
lat,
|
||||
lon,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if open_meteo:
|
||||
results["open-meteo"] = open_meteo
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def ensure_multi_model_hourly_payload(
|
||||
collector: Any,
|
||||
current: Any,
|
||||
*,
|
||||
city: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
use_fahrenheit: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a multi-model payload with hourly curves when available."""
|
||||
current_payload = current if isinstance(current, dict) else {}
|
||||
if current_payload.get("hourly_times"):
|
||||
return current_payload
|
||||
|
||||
hourly_payload = collector.fetch_multi_model(
|
||||
lat,
|
||||
lon,
|
||||
city=city,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if hourly_payload and hourly_payload.get("hourly_times"):
|
||||
return {**current_payload, **hourly_payload}
|
||||
return current_payload
|
||||
@@ -31,6 +31,7 @@ from src.data_collection.ncm_sources import NcmSourceMixin
|
||||
from src.data_collection.aeroweb_sources import AerowebSourceMixin
|
||||
from src.data_collection.wunderground_sources import WundergroundHistoricalMixin
|
||||
from src.data_collection.city_time import get_city_utc_offset_seconds
|
||||
from src.data_collection.forecast_source_bundle import fetch_open_meteo_forecast_bundle
|
||||
from src.database.db_manager import DBManager
|
||||
|
||||
|
||||
@@ -1794,48 +1795,17 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
self._attach_wunderground_historical(results, city_lower, use_fahrenheit)
|
||||
|
||||
if lat and lon:
|
||||
# When force_refresh_observations_only is set by an explicit
|
||||
# observation refresh caller, skip the OM fetch entirely if cached
|
||||
# data exists. Stale model data is fine; the caller only needs
|
||||
# fresh METAR / AMOS observations.
|
||||
om_from_cache_only = force_refresh_observations_only
|
||||
if om_from_cache_only:
|
||||
self._maybe_reload_open_meteo_disk_cache()
|
||||
base = f"{round(float(lat), 4)}:{round(float(lon), 4)}"
|
||||
unit = "f" if use_fahrenheit else "c"
|
||||
cache_city = city_lower
|
||||
om_key = f"{base}:14:{unit}"
|
||||
with self._open_meteo_cache_lock:
|
||||
om_cached = self._open_meteo_cache.get(om_key)
|
||||
if om_cached and isinstance(om_cached.get("data"), dict):
|
||||
open_meteo = dict(om_cached["data"])
|
||||
if include_multi_model:
|
||||
mm_key = f"{base}:{cache_city}:{unit}:{self.multi_model_cache_version}"
|
||||
with self._multi_model_cache_lock:
|
||||
mm_cached = self._multi_model_cache.get(mm_key)
|
||||
if mm_cached and isinstance(mm_cached.get("data"), dict):
|
||||
results["multi_model"] = dict(mm_cached["data"])
|
||||
else:
|
||||
open_meteo = None
|
||||
else:
|
||||
# Prioritize the model cluster before the regular Open-Meteo
|
||||
# forecast. The regular forecast endpoint can set the shared
|
||||
# Open-Meteo 429 cooldown; if that happens first, cities with no
|
||||
# existing multi-model cache (notably Ankara after a deploy) fall
|
||||
# back to a single Open-Meteo/DEB line and the decision card loses
|
||||
# most of its model support. Fetching the multi-model payload
|
||||
# first gives the richer, longer-lived model cache the first chance
|
||||
# to populate; the regular forecast can still use its stale cache
|
||||
# if Open-Meteo rate-limits the cycle.
|
||||
if include_multi_model:
|
||||
multi_model_data = self.fetch_multi_model(
|
||||
lat, lon, city=city, use_fahrenheit=use_fahrenheit
|
||||
)
|
||||
if multi_model_data:
|
||||
results["multi_model"] = multi_model_data
|
||||
open_meteo = self.fetch_from_open_meteo(
|
||||
lat, lon, use_fahrenheit=use_fahrenheit
|
||||
)
|
||||
forecast_bundle = fetch_open_meteo_forecast_bundle(
|
||||
self,
|
||||
city=city_lower,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
include_multi_model=include_multi_model,
|
||||
cache_only=force_refresh_observations_only,
|
||||
)
|
||||
results.update(forecast_bundle)
|
||||
open_meteo = forecast_bundle.get("open-meteo")
|
||||
if open_meteo:
|
||||
results["open-meteo"] = open_meteo
|
||||
# 获取时区偏移以过滤 METAR
|
||||
|
||||
Reference in New Issue
Block a user