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.mdin 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.pyis the only module that importshttpx2(D-01).iter_pages()is the primitive andcollect()builds on it (D-02).- Every service pages through
nextlinks, 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
OGCServicerequest goes through_send(), which adds the service's headers and refuses a URL outside the scheme, host, and port ofbase_url(D-34, D-40). - A request is identified by its method, URL as sent, and body, and every
nextlink gets the first request's method and body (D-38). - A resumed query sends the pending requests of its
Checkpointand 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 ofiter_pages()andcollect(), never a method per form (D-39). - Caller input errors raise
ValidationError, aValueError(D-45), and a query checks its collection and names against cached metadata unlessvalidate=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.aiothe async ones. Each blocking class owns its async service and runs it on one background event-loop thread, neverasyncio.run()ornest_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-Keyheader, never as a query parameter (D-18). - Log records never carry request headers, and the
water_ogcapilogger 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 annotationsin every file. - ruff with 100-character lines and
select = ["ALL"]; pyright in strict mode. Both are configured inpyproject.toml. - NumPy-convention docstrings.
- Source in
src/water_ogcapi/, tests intests/. - Offline tests mock HTTP with
httpx2.MockTransport. Tests that reach a real server carry@pytest.mark.network, and CI never requires them.
Gotchas¶
addoptssets--cov-appendand coverage runs in parallel mode, so coverage accumulates across runs. Delete.coverageand any.coverage.*files before trusting a number.reportandhtmldepend ontest-all, so they also run the network-marked tests.- pyright checks only
src/, so a greentypechecksays nothing abouttests/. pixi r lintruns 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-filessees only git-tracked files, sopixi r lintrunsscripts/lint_untracked.pyafter it to lint untracked, non-ignored files by name. docs/index.mdonly includesREADME.md, anddocs/contributing.mdonly 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 jupytextpy:percentscript; edit either and commit both, the notebook with its outputs.pixi r nb-runsyncs and executes them in thedevenvironment, which calls the live APIs, so follow the live-api skill. pixi r docsconverts each notebook into a Markdown page beside it without running it, so docs builds never call the live APIs. The generated.mdfiles and*_files/folders are git-ignored, apart fromindex.md. The site also publishes the.ipynband.pyfiles, so a link toother.ipynbdownloads the notebook; linkother.mdto reach its page.pixi r docsalso writes the git-ignoreddocs/roadmap.mdfrom the GitHub milestones and issues, so a docs build fails without network access. Past the anonymous limit of 60 requests an hour, setGITHUB_TOKEN.- The API page documents the blocking methods through
scripts/griffe_twins.py, which copies each async method that a_blockingor_iteratingcall in_sync.pywraps. A new wrapper name goes in itsWRAPPERS. - Each notebook gets a card in
docs/examples/index.mdwith a thumbnail indocs/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/skillsis a symlink to it for Claude Code, which does not read.agents/. - The ruleset on
mainnames its required checks by job:Test (test312),Test (test315),typecheck, andlintfromci.yml, andbuildfromdocs.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 underSettings > Rules > Rulesets > Protect mainas that PR merges.
Git¶
- Conventional Commits:
<type>[optional scope]: <description>, with type one offeat,fix,docs,style,refactor,perf,test,build,ci,chore. - Subject under 72 characters, imperative mood.
- Every change reaches
mainthrough a PR from its own branch, named<type>/<short-description>(fix/params-keep-url-query). A ruleset blocks direct pushes, force pushes, and deletion ofmain, 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 withCloses #Nin 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.mdby 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.