"""Weather provider registry -- the app/widgets/ "dispatch registry over pluggable implementations" pattern applied to weather data sources instead of widget types. Three providers, all free/no API key: Open-Meteo (app/weather/open_meteo.py, worldwide), NWS (app/weather/ nws.py, US-only), and Environment Canada (app/weather/ec.py, Canada-only, its own bbox/nearest-site lookup shape rather than simple lat/lon REST -- see that module's own docstring). geocode_city stays Open-Meteo-backed regardless of which provider is chosen to actually fetch forecasts -- it's just free-text-name-to-lat/lon resolution, done once when a location is added, and Open-Meteo's geocoder covers the whole world where NWS's own data plainly doesn't. Every provider module exposes the same four functions: geocode_city(name) -> {"label", "latitude", "longitude"} (open_meteo only, see above) fetch_current(lat, lon, units) -> {"temp", "category"} fetch_hourly(lat, lon, units, hours) -> [{"time", "temp", "category"}, ...] fetch_daily(lat, lon, units, days) -> {"YYYY-MM-DD": {"high", "low", "category"}, ...} "category" is always one of the shared set (clear/partly_cloudy/cloudy/ fog/rain/snow/thunderstorm) that app/weather_render.py's icon-drawing knows how to draw -- callers never need to know which provider supplied an entry. """ from __future__ import annotations class WeatherFetchError(Exception): """Geocoding or forecast fetch failed -- network, no match, an unexpected response shape, or (NWS) a location outside its US coverage. Raised loudly; callers (the dialog's location-set/preview endpoints, get_or_refresh_*_for_widget) decide what to do.""" from . import ec, nws, open_meteo # noqa: E402 -- after WeatherFetchError, which all three submodules import # Re-exported for existing call sites (routers/common.py, routers/ # api_widgets.py, calendar_render.py) -- all Open-Meteo-only and # untouched by the provider abstraction below. from .open_meteo import ( # noqa: E402,F401 CHECK_INTERVAL_S, fetch_daily_forecast, geocode_city, weather_category, ) PROVIDERS = {"open_meteo": open_meteo, "nws": nws, "ec": ec} PROVIDER_LABELS = { "open_meteo": "Open-Meteo", "nws": "National Weather Service (US)", "ec": "Environment Canada", } def fetch_current(provider: str, latitude: float, longitude: float, units: str) -> dict: return PROVIDERS[provider].fetch_current(latitude, longitude, units) def fetch_hourly(provider: str, latitude: float, longitude: float, units: str, hours: int = 48) -> list[dict]: return PROVIDERS[provider].fetch_hourly(latitude, longitude, units, hours) def fetch_daily(provider: str, latitude: float, longitude: float, units: str, days: int) -> dict[str, dict]: return PROVIDERS[provider].fetch_daily(latitude, longitude, units, days)