Add Environment Canada as a third weather provider
Build and push server image / test (push) Successful in 29s
Build and push server image / build-and-push (push) Successful in 4m32s
Build and push server image / deploy (push) Successful in 49s

app/weather/ec.py -- api.weather.gc.ca's MSC GeoMet OGC API
(citypageweather-realtime collection), the modern replacement for the
old dd.weatheroffice.gc.ca XML feed (that host no longer resolves).
Unlike Open-Meteo/NWS's simple lat/lon REST, this collection is only
queryable by bounding box, so _nearest_site widens the box
progressively and picks the closest of the ~844 sites by straight-line
distance -- capped at 300km, calibrated against a real bug caught in
development where an unconditional "nearest site, however far" matched
a Miami, FL query to a site in Ontario 1824km away once the box widened
to cover the whole country.

EC's own numeric icon codes get a small confirmed-against-live-data
mapping table plus the same keyword-on-condition-text fallback NWS
already uses for anything unmapped. Daily periods are named ("Today"/
"Tonight"/"Tuesday"/...) rather than dated, so dates are inferred by
walking them in issued order.

Verified end-to-end against the real live API (Toronto, rural
Saskatchewan, a US border city, and a rejected far-away match) and
through the browser (daily mode, composited panel preview). Test
fixtures mirror the actual response shapes captured live. docs/
widgets.md and CLAUDE.md's TODO updated -- EC is no longer a documented
gap.
This commit is contained in:
2026-07-27 16:50:40 +00:00
parent 52ebafab78
commit 270979949f
6 changed files with 425 additions and 28 deletions
+12 -9
View File
@@ -1,11 +1,10 @@
"""Weather provider registry -- the app/widgets/ "dispatch registry over
pluggable implementations" pattern applied to weather data sources
instead of widget types. Open-Meteo (app/weather/open_meteo.py, no API
key, worldwide) and NWS (app/weather/nws.py, no API key, US-only) are the
two providers wired up now; Environment Canada is a documented next step
(docs/widgets.md), not included yet -- its free API is built around
station/grid lookups (the MSC GeoMet OGC service), not simple lat/lon
REST like these two, and would have meaningfully expanded this pass.
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
@@ -33,7 +32,7 @@ class WeatherFetchError(Exception):
endpoints, get_or_refresh_*_for_widget) decide what to do."""
from . import nws, open_meteo # noqa: E402 -- after WeatherFetchError, which both submodules import
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
@@ -45,8 +44,12 @@ from .open_meteo import ( # noqa: E402,F401
weather_category,
)
PROVIDERS = {"open_meteo": open_meteo, "nws": nws}
PROVIDER_LABELS = {"open_meteo": "Open-Meteo", "nws": "National Weather Service (US)"}
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: