Add safe Open-Meteo cache diagnostic
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user