Files
PolyWeather/scripts/check_open_meteo_cache.py
T
2026-05-18 00:21:18 +08:00

165 lines
5.8 KiB
Python

"""Safely inspect local Open-Meteo cache state.
This diagnostic is intentionally cache-first:
- It does not call Open-Meteo.
- It does not hard-code any auth token.
- Optional backend API probing requires ``--api-detail`` and reads the bearer
token from ``POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN``.
Examples:
python scripts/check_open_meteo_cache.py --city "sao paulo"
python scripts/check_open_meteo_cache.py --city ankara --api-detail
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from src.data_collection.city_registry import CITY_REGISTRY # noqa: E402
from src.data_collection.weather_sources import WeatherDataCollector # noqa: E402
from src.utils.config_loader import load_config # noqa: E402
def _cache_age(entry: dict[str, Any]) -> str:
try:
age = int(time.time() - float(entry.get("t") or 0))
except Exception:
return "unknown"
return f"{age}s"
def _city_meta(city: str) -> dict[str, Any]:
key = city.strip().lower()
meta = CITY_REGISTRY.get(key)
if not meta:
raise SystemExit(f"Unknown city {city!r}. Use a key from CITY_REGISTRY.")
return meta
def _coord_prefix(meta: dict[str, Any]) -> str:
return f"{round(float(meta['lat']), 4)}:{round(float(meta['lon']), 4)}"
def _print_cooldown(collector: WeatherDataCollector) -> None:
refresh = getattr(collector, "_refresh_open_meteo_rate_limit_until", None)
until = float(refresh() if callable(refresh) else collector._open_meteo_rate_limit_until)
now = time.time()
if until > now:
print(f"Open-Meteo cooldown: active ({int(until - now)}s remaining)")
else:
print("Open-Meteo cooldown: inactive")
def _print_forecast_cache(collector: WeatherDataCollector, prefix: str) -> None:
print("\n[forecast cache]")
found = False
for key in sorted(collector._open_meteo_cache.keys()):
if not key.startswith(prefix):
continue
found = True
entry = collector._open_meteo_cache[key]
data = entry.get("data") if isinstance(entry, dict) else {}
hourly = data.get("hourly", {}) if isinstance(data, dict) else {}
daily = data.get("daily", {}) if isinstance(data, dict) else {}
print(
f"- key={key} age={_cache_age(entry)} "
f"hourly={len(hourly.get('time') or [])} daily={len(daily.get('time') or [])}"
)
if not found:
print("- no matching forecast cache")
def _print_multi_model_cache(collector: WeatherDataCollector, prefix: str) -> None:
print("\n[multi-model cache]")
found = False
for key in sorted(collector._multi_model_cache.keys()):
if not key.startswith(prefix):
continue
found = True
entry = collector._multi_model_cache[key]
data = entry.get("data") if isinstance(entry, dict) else {}
forecasts = data.get("forecasts", {}) if isinstance(data, dict) else {}
dates = data.get("dates", []) if isinstance(data, dict) else []
print(
f"- key={key} age={_cache_age(entry)} "
f"models={len(forecasts)} dates={len(dates)}"
)
if not found:
print("- no matching multi-model cache")
def _print_ensemble_cache(collector: WeatherDataCollector, prefix: str) -> None:
print("\n[ensemble cache]")
found = False
for key in sorted(collector._ensemble_cache.keys()):
if not key.startswith(prefix):
continue
found = True
entry = collector._ensemble_cache[key]
data = entry.get("data") if isinstance(entry, dict) else {}
print(
f"- key={key} age={_cache_age(entry)} "
f"members={data.get('members') if isinstance(data, dict) else None}"
)
if not found:
print("- no matching ensemble cache")
def _fetch_api_detail(city: str, base_url: str) -> None:
token = os.getenv("POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN", "").strip()
if not token:
raise SystemExit(
"--api-detail requires POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN in the environment"
)
quoted_city = urllib.parse.quote(city)
url = f"{base_url.rstrip('/')}/api/city/{quoted_city}/detail?force_refresh=false"
request = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.loads(response.read())
hourly = (payload.get("timeseries") or {}).get("hourly") or {}
models = payload.get("models") or {}
deb = payload.get("deb") or {}
print("\n[backend detail]")
print(f"- status=ok hourly={len(hourly.get('times') or [])} models={len(models)}")
print(f"- deb_prediction={deb.get('prediction')}")
def main() -> None:
parser = argparse.ArgumentParser(description="Inspect local Open-Meteo caches safely.")
parser.add_argument("--city", default="sao paulo", help="CITY_REGISTRY key to inspect")
parser.add_argument("--api-detail", action="store_true", help="Also query local backend detail API")
parser.add_argument("--base-url", default="http://localhost:8000", help="Backend base URL")
args = parser.parse_args()
city = args.city.strip().lower()
meta = _city_meta(city)
prefix = _coord_prefix(meta)
print(f"city={city} coord_prefix={prefix}")
collector = WeatherDataCollector(load_config())
_print_cooldown(collector)
_print_forecast_cache(collector, prefix)
_print_multi_model_cache(collector, prefix)
_print_ensemble_cache(collector, prefix)
if args.api_detail:
_fetch_api_detail(city, args.base_url)
if __name__ == "__main__":
main()