ECCC GDPS 2 m temperature — direct MSC Datamart fetch¶
The headline change of the nwp-polish sprint: ECCC's deterministic global model
(gdps) is now fetched as per-variable GRIB2 straight from MSC Datamart
(dd.weather.gc.ca), via the new eccc-msc backend. No Herbie plugin, no SDK,
no .idx byte-range (Datamart serves whole files per variable).
This notebook shows the new path end-to-end: catalog row → centre fetch → on-disk GRIB2 → quick raster read. It runs against the live Datamart and is gated to skip if the cycle's not yet published.
Setup¶
import datetime as dt
from pathlib import Path
import requests
from earthlens.nwp import Catalog
from earthlens.nwp.centres.eccc import ECCCCentre
1 — The gdps catalog row¶
Inspect the row that used to dispatch through Herbie and now ships as a
first-party direct-HTTPS row. Note the new license, retention_days, and
grid_kind metadata.
model = Catalog().get_model('gdps')
print(f'provider : {model.provider}')
print(f'backend : {model.backend}')
print(f'licence : {model.license}')
print(f'retention : {model.retention_days} day(s)')
print(f'grid_kind : {model.grid_kind}')
print(f'cycles UTC : {model.cycles_utc}')
print(f'horizon (h) : {model.horizon_h}')
print(f'step cadence : {model.step_cadence_h} h')
print(f'band count : {len(model.bands)}')
print(f't2m token : {model.bands["temperature_2m"]!r}')
provider : eccc-msc backend : eccc-msc licence : OGL-Canada-2.0 retention : 30 day(s) grid_kind : regular-latlon cycles UTC : [0, 12] horizon (h) : 240 step cadence : 3 h band count : 77 t2m token : 'AirTemp_AGL-2m'
2 — Pick a published cycle¶
Datamart keeps GDPS for ~30 days. We probe yesterday's 00Z run first; if it's not visible (the run hasn't been published yet, the network is down, or the Datamart layout has drifted), the rest of the notebook is skipped cleanly.
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
cycle = now.replace(hour=0, minute=0, second=0, microsecond=0) - dt.timedelta(days=1)
step = 3 # 3-hour lead time
url_template = model.url_template
probe_url = url_template.format(
cycle=cycle, date=cycle, step=step, var=model.bands['temperature_2m']
)
print(f'cycle: {cycle:%Y-%m-%d %HZ} step: {step:03d}h')
print(f'probe: {probe_url}')
head = requests.head(probe_url, timeout=15, allow_redirects=True)
available = head.status_code == 200
print(f'HTTP {head.status_code} -> available={available}')
cycle: 2026-06-16 00Z step: 003h probe: https://dd.weather.gc.ca/20260616/WXO-DD/model_gdps/15km/00/003/20260616T00Z_MSC_GDPS_AirTemp_AGL-2m_LatLon0.15_PT003H.grib2
HTTP 200 -> available=True
3 — Fetch via ECCCCentre¶
Call the centre directly to keep the example focused on the new code path
(rather than going through the EarthLens facade + crop pipeline). The
centre downloads the GRIB2 to a temporary directory and returns its path.
out_dir = Path('out/eccc-gdps')
out_dir.mkdir(parents=True, exist_ok=True)
if available:
centre = ECCCCentre(out_dir)
grib_path = centre.fetch_one(
model=model, cycle=cycle, step=step,
params=['temperature_2m'], mirror='origin',
)
print(f'wrote {grib_path.name} ({grib_path.stat().st_size / 1024:.1f} KiB)')
else:
grib_path = None
print('cycle not yet published on Datamart; skipping the fetch')
wrote gdps_2026061600_f003.grib2 (990.9 KiB)
4 — Read the GRIB2 as a pyramids Dataset¶
The new centre lands a plain GRIB2 file — pyramids.grib.open_grib reads it
via GDAL's GRIB driver. We then print the resulting array's shape and
summary statistics in Kelvin.
if grib_path is not None:
from pyramids.grib import open_grib
# GDPS GRIB2 messages are JPEG2000-packed. Reading them needs
# a GDAL JP2 driver; the PyPI pyramids-gis wheel currently ships
# a vendored GDAL without one (tracked upstream as
# https://github.com/serapeum-org/pyramids/issues/600). The fetch
# itself (the new code path this notebook demonstrates) is
# unaffected -- we degrade cleanly here.
try:
ds = open_grib(str(grib_path))
arr = ds.read_array()
print(f'shape : {arr.shape}')
print(f'min K : {float(arr.min()):.2f}')
print(f'mean K : {float(arr.mean()):.2f}')
print(f'max K : {float(arr.max()):.2f}')
print(f'min C : {float(arr.min()) - 273.15:.2f}')
print(f'mean C : {float(arr.mean()) - 273.15:.2f}')
print(f'max C : {float(arr.max()) - 273.15:.2f}')
except Exception as exc:
msg = str(exc)
if 'JP2' in msg or 'JPEG2000' in msg or 'jpc' in msg:
print('GDAL JPEG2000 plugin missing -- fetched OK,'
' read step skipped (tracked upstream:'
' serapeum-org/pyramids#600).')
else:
raise
else:
print('(skipped - no GRIB2 to read)')
2026-06-17 22:51:28 | INFO | pyramids.base.config | Logging is configured.
GDAL JPEG2000 plugin missing -- fetched OK, read step skipped (tracked upstream: serapeum-org/pyramids#600).
Recap¶
gdpsships withbackend: eccc-mscand a Datamarturl_template.ECCCCentre.fetch_oneissues per-variable HTTPS GETs (withSessionreuse and chunked streaming, so a multi-GB GEPS request is bounded).- The resulting GRIB2 reads cleanly through
pyramids.grib.open_grib. - See
nwp_polish_features.ipynbfor the catalog-metadata half of the polish work (licence, retention, grid_kind, RetentionWarning).