"""Open-Meteo wrapper. Free, no key needed. Resolves to (lat, lon) — block, district, or state centroid — depending on what the caller has. """ from __future__ import annotations import logging from functools import lru_cache from typing import Optional import httpx from kcc_core import config logger = logging.getLogger(__name__) @lru_cache(maxsize=4096) def forecast(lat: float, lon: float, days: int = 7) -> dict: """7-day forecast cached per (lat, lon) at ~3 decimal resolution.""" url = (f"{config.OPEN_METEO_BASE}" f"?latitude={lat:.3f}&longitude={lon:.3f}" f"&daily=temperature_2m_max,temperature_2m_min,relative_humidity_2m_mean," f"precipitation_sum,wind_speed_10m_max,weathercode" f"¤t_weather=true&timezone=Asia%2FKolkata" f"&forecast_days={min(max(days,1),16)}") try: r = httpx.get(url, timeout=12.0) r.raise_for_status() return r.json() except Exception as e: logger.warning(f"[weather] failed for ({lat},{lon}): {e}") return {} def summary(lat: float, lon: float) -> dict: """Compact summary: temp_mean, humidity_mean, rain_mm_7d, hot_days, rain_days, dominant_weathercode.""" fc = forecast(lat, lon, days=7) daily = fc.get("daily", {}) if not daily: return {} tmax = daily.get("temperature_2m_max", []) tmin = daily.get("temperature_2m_min", []) hum = daily.get("relative_humidity_2m_mean", []) rain = daily.get("precipitation_sum", []) if not tmax: return {} n = len(tmax) tmean = [(a + b) / 2 for a, b in zip(tmax, tmin) if a is not None and b is not None] return { "temp_mean": round(sum(tmean) / max(len(tmean), 1), 1), "temp_max": max(tmax) if tmax else None, "temp_min": min(tmin) if tmin else None, "humidity_mean": round(sum(hum) / max(len(hum), 1), 1) if hum else None, "rain_mm_7d": round(sum(rain), 1) if rain else 0.0, "rain_days": sum(1 for r in (rain or []) if (r or 0) > 1.0), "hot_days": sum(1 for t in tmax if t and t >= 35), "days": n, }