Contributing#
Bug reports, new data products and documentation fixes are all welcome. The issue tracker is at egagli/easysnowdata#issues.
Setting up#
git clone https://github.com/egagli/easysnowdata.git
cd easysnowdata
pixi install # resolves from pixi.lock; conda-forge
pixi run -e test-py313 test-unit # should be green before you change anything
Test in test-py313, not dev. pixi.lock pins every environment, but the
per-interpreter environments have no solve-group, so they resolve
independently of dev and land on a different stack — at one point dev had
rasterio 1.5.0 / GDAL 3.12.3 while test-py313 had 1.5.1 / 3.13.3. That is
deliberate (testing three interpreters against their own best resolutions is
the point of the matrix) but it means dev is for interactive work — a
notebook, a REPL, a docs build — and test-py3XX is what CI runs. CI now has
a dev leg too, so the gap cannot silently reopen (#20).
Never hand-edit the lock; pixi install regenerates it when pyproject.toml
changes, and CI runs with locked: true so a stale lock fails the build rather
than silently resolving something else. One caveat worth knowing: pixi lock
exits 0 even when it drops a dependency it cannot satisfy, so check that a new
dependency is actually in pixi.lock rather than trusting the exit code.
The tasks#
task |
what it does |
|---|---|
|
the whole offline tier, sockets blocked |
|
just the cassette-replayed tests |
|
offline tier with coverage (fails under 95%) |
|
live tier: one smoke test per source |
|
re-record the cassettes (needs network) |
|
ruff check / ruff format |
|
build the site without running the gallery (~17 s) |
|
build and run the credential-free gallery (~4 min) |
|
build and run the whole gallery (needs every credential) |
|
serve |
|
run the health probes and update the history |
|
print the issues the last health run would open |
|
run the upstream watch and print the digest, writing nothing |
The four test tiers#
Each tier answers a different question, and they are deliberately separate so that a provider outage cannot turn a pull request red.
unit — pure functions, no I/O. Sockets are blocked with pytest-socket
(--allow-unix-socket, because zarr 3’s asyncio loop needs a socketpair), so a
test that quietly reaches the network fails instead of passing by accident.
recorded — searches and reads replayed from tests/cassettes/ with
pytest-recording, plus the tiny generated rasters in tests/fixtures/. Also
offline, also socket-blocked, --record-mode=none so a missing cassette is a
failure rather than a live request. Re-record with record-cassettes when a
search changes, and check the diff: cassettes are scrubbed of cookies and
tokens but you are still committing someone’s API response.
live — one smoke test per source against the real provider, marked live
and deselected everywhere else. Runs weekly in CI and on demand. Tests marked
requires_earthdata / requires_earthengine / requires_planet /
requires_nve skip when the credential is absent, so a contributor without
an Earth Engine account still gets a meaningful run.
docs — the gallery is a test tier in disguise: every credential-free example is executed on every pull request, and the scheduled build executes all 24 against live data with the secrets.
Run the offline tiers before every commit and the live tier before claiming a data route works.
Adding a data product#
The catalog entry is the contract: it generates the docs page, the health row and the credential table, so the artefacts cannot drift apart. In order:
Put it in easysnowdata/<theme>/<product>.py with module-level search and
load functions. Take aoi first and time second; accept source=; return
lazy xarray. Build the return value with processing.contract.finalize() so
the dims, CRS, nodata policy and provenance attributes come out right without
you re-deciding them. Read Concepts once before you start.
A Product with one Source per access route — the first is the default.
Every source needs a location, an extent, a notes line saying what
differs from the other routes, and a health=Probe(...). Register it at the
bottom of the module with catalog.register(PRODUCT, replace=True), and
import the module from the theme’s __init__.py so registration happens.
Give every variable its units, dtype and nodata. Categorical variables need
flag_values, flag_meanings and flag_colors of equal length — the offline
test checks that, and the plotting helpers read them.
One offline: a recorded search or a tiny fixture raster, asserting the shape
of the contract (dims, CRS, dtype, attrs) rather than the values. One live:
marked @pytest.mark.live, plus requires_* if it needs credentials, doing
the smallest real request that proves the route works.
The minimal request that shows the route is alive: a first-byte GET for a
static file, a one-item STAC search, a getInfo() for Earth Engine, an
earthaccess search for NSIDC. Reuse the helpers in
easysnowdata.catalog.health. GET first, not HEAD — some servers (GRDC)
answer HEAD with a 400 that looks exactly like a 404.
Give the probe a stable label. It is the key the weekly history is stored
under, so renaming one starts its history over.
docs/gallery/<theme>/plot_<product>.py: load it for one AOI, draw one
figure, say one thing that is true about the product and not obvious. Use the
sphinx-gallery format — a docstring title with an === underline of exactly
the right length, then # %% cells. List it in the catalog entry’s
examples= tuple.
If it needs credentials, put # esd-requires: earthdata (comma-separated, one
or more providers) on the first line, above the docstring. That keeps it
out of the credential-free pull-request build; the marker itself never appears
on the rendered page.
pixi run -e test-py313 test-unit — tests/test_docs_pages.py fails if the product
has no example, if the example file is missing, or if a gallery script no
product claims has appeared. pixi run -e docs docs-fast then shows you the
generated page.
Documentation#
Nothing under docs/catalog/, docs/api/, docs/auto_examples/,
docs/generated/, docs/gen_modules/, docs/credentials.md or
docs/status.md is written by hand — they are generated at build time from
the registry and the gallery, and they are all in .gitignore. Editing a
catalog page means editing the catalog entry.
What is hand-written: index.md, installation.md, concepts.md,
faq.md, this page, and the gallery scripts.
The README’s header image is generated too: scripts/make_montage.py tiles
the thumbnails of the executed gallery into _static/gallery.webp, which the
scheduled docs build publishes and the README links to. It skips examples that
did not run, so a build without credentials does not turn seven products into
blank squares. Its status table and catalog summary come from
scripts/update_readme_status.py, between the sentinel comments — edit the
catalog entry, not the README.
The legacy notebooks under docs/examples/ are historical. They are rendered
as-is, never re-executed, and excluded from ruff. Do not reformat them.
Style#
ruff formatandruff checkmust pass:pixi run -e lint lint. Line length 88, double quotes, isort witheasysnowdataas first-party.NumPy-style docstrings. The API pages are generated from
__all__, so a function without a docstring shows up as a blank page.logging, neverprint— the one sanctioned print is the import-time credential summary.No import-time side effects, and no global
xr.set_optionsor GDAL configuration.Metric units everywhere.
The network clients under easysnowdata/stations/clients/ arrived here as a
git subtree of
global_snow_networks. That
repo deleted its copy in 2026-09 and imports these instead, so this is now
the only copy: fix a client here. They are still excluded from ruff, which
is no longer necessary and only avoids a 7 000-line reformat inside the
migration; formatting them is a fine follow-up. Their contract is still
global_snow_networks’ DESIGN.md §3, and their tests are in
tests/stations/.
Pull requests#
Branch off
main, one topic per pull request.The PR build runs ruff, the offline tiers on three interpreters and three operating systems, coverage, and a docs build with the credential-free gallery. None of it needs secrets.
Include tests. Coverage of the new modules must stay above 95%.
If you changed a public function’s behaviour, say so in the PR body; the changelog is generated from commit messages with
git-changelog.