Skip to content

Contributing

Conventions for human contributors and AI coding agents working in this repository. The docs site renders this file as its contributing guide.

Project

water-ogcapi is a Python client for USGS water data services (NWIS, FabricData, GeoConnex) over OGC API Features, with blocking service classes at the package root and async twins in water_ogcapi.aio. README.md lists the endpoints.

USGS operates these APIs. Before any request to a live endpoint, read .agents/skills/live-api/SKILL.md.

Design record

docs/design.md holds every design decision (D-), the lessons from past mistakes (L-), and the open questions (Q-), each with its rationale and rejected alternatives.

  • Before designing a component or proposing an alternative to a rule below, search it. Do not reopen an accepted decision without evidence it does not already account for.
  • When a change makes or reverses a decision, answers a question, or exposes a mistake worth not repeating, update docs/design.md in the same change, following the rules at its top.

Design rules

One-line summaries of the decisions any code change can break. The IDs point into docs/design.md.

  • src/water_ogcapi/_transport.py is the only module that imports httpx2 (D-01).
  • iter_pages() is the primitive and collect() builds on it (D-02).
  • Every service pages through next links, and only NWIS fans out, on request, by splitting a query by calendar period or site list into pieces run concurrently (D-35, D-56).
  • Every OGCService request goes through _send(), which adds the service's headers and refuses a URL outside the scheme, host, and port of base_url (D-34, D-40).
  • A request is identified by its method, URL as sent, and body, and every next link gets the first request's method and body (D-38).
  • A resumed query sends the pending requests of its Checkpoint and never a request the checkpoint holds (D-52).
  • Query methods return payload plus evidence (Page, QueryResult), never a bare GeoJSON dict (D-03).
  • Query forms (bbox, geometry, filter, ids) are keyword arguments of iter_pages() and collect(), never a method per form (D-39).
  • Caller input errors raise ValidationError, a ValueError (D-45), and a query checks its collection and names against cached metadata unless validate=False (D-47).
  • Any type that carries request headers or params redacts credentials at construction, using an allowlist (D-04).
  • The package root exports the blocking service classes and water_ogcapi.aio the async ones. Each blocking class owns its async service and runs it on one background event-loop thread, never asyncio.run() or nest_asyncio (D-08, D-46, D-48).
  • No pandas, geopandas, xarray, Arrow, shapely, or STAC on the core import path (D-09).
  • Construction performs no HTTP (D-10), and redirects are never followed (D-12).
  • The USGS API key never goes to a non-USGS host such as GeoConnex (D-13).
  • Service classes take no base_url, so a key goes only to its profile's host (D-42).
  • The API key travels only in the X-Api-Key header, never as a query parameter (D-18).
  • Log records never carry request headers, and the water_ogcapi logger does not propagate to the root logger (D-20).
  • A public name is removed only after a minor release in which it raises a DeprecationWarning (D-21).
  • Python packages come from PyPI. [tool.pixi.dependencies] holds only system libraries such as GDAL (D-27).

Commands

Install pixi and run everything through it; each environment is created on first use. Never call bare pytest, python, or pyright.

Task Command
Lint (pre-commit) pixi r lint
Dependency audit pixi r audit
Type check pixi r typecheck
Test, offline (3.12) pixi r -e test312 test
Test, offline (3.15) pixi r -e test315 test
Test, network-marked only pixi r -e test315 test-network
Test, everything pixi r -e test315 test-all
Coverage report / HTML pixi r -e test315 report / html
Run Python pixi r -e dev python ...
Spell check (writes fixes) pixi r spell
Changelog preview pixi r changelog
Docs, strict build pixi r docs
Docs, live preview pixi r docs-serve
Notebooks, sync and run pixi r nb-run
Notebooks, pair a new one pixi r nb-pair

Environments: dev, test312, test315, typecheck, lint, docs.

A change is done when lint, typecheck, and the offline tests on both test312 and test315 pass, plus pixi r docs when it touches docs/, README.md, or zensical.toml. .github/workflows/ci.yml runs the first three, and .github/workflows/docs.yml builds the site on every PR and deploys main to GitHub Pages.

Code style

  • Python >= 3.12, with from __future__ import annotations in every file.
  • ruff with 100-character lines and select = ["ALL"]; pyright in strict mode. Both are configured in pyproject.toml.
  • NumPy-convention docstrings.
  • Source in src/water_ogcapi/, tests in tests/.
  • Offline tests mock HTTP with httpx2.MockTransport. Tests that reach a real server carry @pytest.mark.network, and CI never requires them.

Gotchas

  • addopts sets --cov-append and coverage runs in parallel mode, so coverage accumulates across runs. Delete .coverage and any .coverage.* files before trusting a number.
  • report and html depend on test-all, so they also run the network-marked tests.
  • pyright checks only src/, so a green typecheck says nothing about tests/.
  • pixi r lint runs vulture at 80% confidence, which catches only unused imports, unused function arguments, and unreachable code. Unused functions, classes, and local variables score 60% and pass.
  • ruff auto-inserts from __future__ import annotations.
  • mdformat rewraps markdown to 88 columns during pixi r lint. pre-commit's --all-files sees only git-tracked files, so pixi r lint runs scripts/lint_untracked.py after it to lint untracked, non-ignored files by name.
  • docs/index.md only includes README.md, and docs/contributing.md only includes this file. Edit the source files. Links in both must be absolute URLs: a relative link resolves against the site, where its target does not exist, and the strict build does not catch it.
  • Each example in docs/examples/ is a notebook paired with a jupytext py:percent script; edit either and commit both, the notebook with its outputs. pixi r nb-run syncs and executes them in the dev environment, which calls the live APIs, so follow the live-api skill.
  • pixi r docs converts each notebook into a Markdown page beside it without running it, so docs builds never call the live APIs. The generated .md files and *_files/ folders are git-ignored, apart from index.md. The site also publishes the .ipynb and .py files, so a link to other.ipynb downloads the notebook; link other.md to reach its page.
  • pixi r docs also writes the git-ignored docs/roadmap.md from the GitHub milestones and issues, so a docs build fails without network access. Past the anonymous limit of 60 requests an hour, set GITHUB_TOKEN.
  • The API page documents the blocking methods through scripts/griffe_twins.py, which copies each async method that a _blocking or _iterating call in _sync.py wraps. A new wrapper name goes in its WRAPPERS.
  • Each notebook gets a card in docs/examples/index.md with a thumbnail in docs/examples/images/, following the existing card. The strict build fails if a card links to a notebook that does not exist.
  • Skills live in .agents/skills/. .claude/skills is a symlink to it for Claude Code, which does not read .agents/.
  • The ruleset on main names its required checks by job: Test (test312), Test (test315), typecheck, and lint from ci.yml, and build from docs.yml. Renaming a job or a matrix environment leaves the old name unreported, so the renaming PR and every PR after it wait forever. Swap the name under Settings > Rules > Rulesets > Protect main as that PR merges.

Git

  • Conventional Commits: <type>[optional scope]: <description>, with type one of feat, fix, docs, style, refactor, perf, test, build, ci, chore.
  • Subject under 72 characters, imperative mood.
  • Every change reaches main through a PR from its own branch, named <type>/<short-description> (fix/params-keep-url-query). A ruleset blocks direct pushes, force pushes, and deletion of main, and merges a PR only after its CI and docs checks pass.
  • PRs fill in .github/pull_request_template.md. A PR names the issue it finishes with Closes #N in its description, which closes the issue and moves its card on the project board to Done when the PR merges.
  • git-cliff generates the changelog from commit messages. Do not edit CHANGELOG.md by hand.
  • pixi r release v<version> [notes-file] tags and pushes that tag, which publishes to PyPI (D-32). Run it only when a maintainer asks.

Writing style

These rules cover code comments, docstrings, commit messages, PR descriptions, issues, and docs.

  • American spelling. Fix British spellings on sight.
  • Active voice, present tense, concise.
  • Use an em or en dash only when no comma, colon, semicolon, parentheses, or new sentence works.
  • Be concrete. A number, name, or mechanism beats an abstraction: "handles errors robustly" becomes "retries 429 and 5xx three times, then raises".
  • No contrastive framing. Say what a thing is and stop, without a negated counterpart added for rhythm or emphasis: "the launch is checked rather than trusted" reads "the launch is checked". The ban covers "rather than", "not X but Y", "instead of", and "X, not Y" in any position. A real either/or, where both options are live and the reader needs to know which was picked, stays.
  • Keep terms consistent. Once something has a name, reuse it, because a reader takes a new word for a new thing. If the text calls it a Page, later sentences do not switch to "chunk" or "batch".
  • Cut filler ("it's worth noting", "in order to", "when it comes to"), puffery ("plays a vital role", "stands as a testament"), and closing summaries that restate what came before.
  • Banned words: delve, foster, empower, streamline, facilitate, cutting-edge, seamless, multifaceted, nuanced, intricate, meticulous, paramount, transformative, elevate, embark, supercharge.

Comments and commits:

  • Comment only what the code does not say: a decision a reader would not guess, or behavior a reader would not see from reading it.
  • Keep a comment or commit body to a line or two. Cut the rationale to the sentence that stops the next reader from undoing the change.
  • Comments describe the code as it stands. History ("used to", "the old code", the bug that prompted the change) goes in the commit message.

Reviews and reports, including API findings:

  • A code review follows REVIEW.md: what failure looks like here, what to skip, how to report, and how deep to go.
  • Say which claims you checked against the source and which you infer.
  • Reserve prose for what changes the conclusion. Minor issues go in a numbered list at the end.