165 lines
5.8 KiB
Python
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()
|