-
Notifications
You must be signed in to change notification settings - Fork 63
Make failures actionable and harden validation, APIs, and CI #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thodson-usgs
wants to merge
10
commits into
DOI-USGS:main
Choose a base branch
from
thodson-usgs:docs/refresh-agents-context
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
19321f9
docs: refresh contributor and agent guidance
thodson-usgs b4d73f6
feat(validation): make argument failures actionable
thodson-usgs 410ef02
feat(cql): add a total-row cap to get_cql
thodson-usgs 77fd19d
test: enforce a branch-coverage ratchet
thodson-usgs a118086
fix: align branch documentation with behavior
thodson-usgs 4e8dbb5
ci: enforce coverage across supported interpreters
thodson-usgs e01ed4d
refactor(validation): complete shared check adoption
thodson-usgs a839f9a
docs: record behavior changes and remove stale commentary
thodson-usgs 60a0c6d
feat(states): support US territories
thodson-usgs 5d7ff50
refactor(validation): simplify checks and retire legacy exceptions
thodson-usgs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,157 @@ | ||
| # AGENTS.md | ||
|
|
||
| ## Scope | ||
| - Python code is in `dataretrieval/`; `dataretrieval/waterdata/` is the modern USGS Water Data API, `dataretrieval/nwis.py` is legacy/deprecated. | ||
| - `R/dataRetrieval/` is the R project copy; leave it alone unless the task asks for R work. | ||
| - Exclude `.claude/worktrees/` from searches and edits; it contains stale worktrees that pollute results. | ||
| ## Start here | ||
| - **`CONTEXT.md` is the shared vocabulary** — getter, query, chunk, plan, fan-out, | ||
| page, adapter, facade, leaf, transport, collection, profile, effective | ||
| configuration, and the legacy names that are deliberately not renamed. Read it | ||
| before writing code, docstrings, or commit messages; when a term there | ||
| conflicts with a name in the code, the term wins. | ||
| - Architectural decisions and their rationale: `docs/source/architecture/decisions/` | ||
| (ADRs, referenced by number throughout the code and by `.importlinter`). | ||
| - Contributor workflow, style, and the quality gates in detail: `CONTRIBUTING.md`. | ||
|
|
||
| ## Example Notebooks | ||
| - `demos/*.ipynb` — top-level Water Data tour: `USGS_WaterData_Introduction_Examples.ipynb` is the entry point; `_ContinuousData_`, `_DailyStatistics_`, `_DiscreteSamples_`, `_ReferenceLists_` cover individual collections; `WaterData_demo.ipynb`, `peak_streamflow_trends.ipynb`, `USGS_WaterUse_Examples.ipynb` (NWDC water-use data via `nwdc.get_wateruse`), and `R Python Vignette equivalents.ipynb` are standalone walkthroughs. | ||
| - `demos/hydroshare/*.ipynb` — per-service HydroShare examples (NLDI, NWIS WaterUse, and Water Data DailyValues / GroundwaterLevels / Measurements / ParameterCodes / Peaks / Ratings / Samples / SiteInfo / SiteInventory / Statistics / UnitValues). Mirror these when adding examples for a new collection. | ||
| - `demos/nwqn_data_pull/` — non-notebook example: a lithops/Docker batch pipeline (`retrieve_nwqn_samples.py`, `retrieve_nwqn_streamflow.py`) with its own `README.md`. | ||
| - Any `Untitled*.ipynb`, `*_test.ipynb`, or notebooks not listed here are untracked local scratch; ignore them. | ||
| ## How the tree is organized | ||
| Use `ls`/`grep` for the file list; what follows is the placement logic, so you | ||
| can predict where a thing lives. | ||
|
|
||
| - `dataretrieval/` — the public surface is one *adapter* module per service, | ||
| named for the service (`nldi`, `nwdc`, `ngwmn`, `streamstats`, `wqp`, and | ||
| legacy `nwis`); each owns that service's URLs, parameters, and response | ||
| quirks. `waterdata/` is the one adapter large enough to be a package, split by | ||
| collection family; its `api.py` is a compatibility facade holding no logic. | ||
| Everything else in the package is shared machinery the adapters sit on top of | ||
| — configuration, credentials, progress, exceptions, code tables, response | ||
| formats. Shared machinery below the adapter layer must not know about any | ||
| particular service. | ||
| - `dataretrieval/ogc/` — the OGC API protocol machinery (chunk planning, | ||
| filters, request building, response shaping). Shared by the two OGC services | ||
| only; `.importlinter` refuses any other importer. | ||
| - `dataretrieval/transport/` — service-neutral request machinery (HTTP, retry, | ||
| pagination, fan-out). It names no service and no protocol, and is not public API. | ||
| - Leading-underscore top-level modules are private; the dependency-free *leaves* | ||
| sit at the floor of the stack so anything may use them without pulling in the | ||
| rest of the package. Check for an existing leaf before writing a small helper. | ||
| - **`.importlinter` is the map.** Its `layers` contract lists every top-level | ||
| module in dependency order and is `exhaustive = True`, so it is both the | ||
| authoritative statement of where a module sits and the thing that fails when a | ||
| new module has no home. Read it before adding a module or an import. | ||
| - `tests/` — flat, one `*_test.py` per module or concern, organized into four | ||
| dependency-oriented layers (public contract, adapter contract, component, | ||
| cross-component) that `tests/contracts/README.md` defines and assigns files to. | ||
| `architecture_test.py` holds the fitness functions a boundary checker cannot | ||
| express (symbols, `__all__`, AST shape, cycles); pure module-to-module | ||
| direction belongs in `.importlinter` instead. | ||
| - `docs/source/` — `reference/` (one page per public module), `userguide/` | ||
| (prose topics), `architecture/` (overview + ADRs), `meta/` (project docs), | ||
| `examples/` (`.nblink` files pointing at `demos/`; the docs build executes them). | ||
| - `demos/` — one notebook per Water Data collection or topic, plus | ||
| `hydroshare/` mirroring them per service for HydroShare, and | ||
| `nwqn_data_pull/` as a non-notebook batch-pipeline example. When adding a | ||
| collection, add a demo alongside the existing ones and an `.nblink` in | ||
| `docs/source/examples/`. | ||
|
|
||
| ## Not part of the repo | ||
| - `R/`, `experiments/`, `build/`, `dist/`, `.kiro/`, and any `Untitled*.ipynb` or | ||
| `*_test.ipynb` at the top level are untracked local scratch — don't edit, | ||
| commit, or cite them. | ||
| - Exclude `.claude/worktrees/` from searches and edits; stale worktrees there | ||
| pollute results. | ||
|
|
||
| ## Environment | ||
| - Use `pip install .[test,nldi]` (CI uses pip, not uv despite `uv.lock`). Docs: `pip install .[doc,nldi]`. | ||
| - `pip install .[test,nldi]` (CI uses pip, not uv, despite `uv.lock`). | ||
| Docs: `pip install .[doc,nldi]`. Gates: `pip install -e .[metrics]`. | ||
| - Python >= 3.10; the CI test matrix is 3.10, 3.13, 3.14. | ||
|
|
||
| ## Commands | ||
| - Lint: `ruff check .` and `ruff format --check .`. | ||
| - Tests: `coverage run -m pytest tests/ && coverage report -m`, or focused like `pytest tests/waterdata_test.py::test_mock_get_samples`. | ||
| - Docs: install docs deps, `ipython kernel install --name "python3" --user`, then `make html` from `docs/`. `make docs` adds doctest+linkcheck (network-dependent). | ||
|
|
||
| ## Testing Gotchas | ||
| - Tests mock HTTP with `pytest-httpx`'s `httpx_mock` fixture and fixtures under `tests/data/`; keep new API tests offline. `tests/conftest.py` relaxes the fixture's strict-mode defaults (unused mocks and unmocked requests are tolerated) so rerun-on-failure works. | ||
| - `tests/nwis_test.py::test_nwis_service_live` hits live NWIS. | ||
| - `tests/waterdata_test.py` and `tests/waterdata_ratings_test.py` skip on Python <3.10, so a 3.9 run does not cover them. | ||
|
|
||
| ## Implementation Notes | ||
| - HTTP client is `httpx` (migrated from `requests` in #289); new code should use `httpx` and tests should mock with `httpx_mock`. | ||
| - Public download helpers return `(DataFrame, metadata)`. | ||
| - `dataretrieval/__init__.py` star-imports service modules; `dataretrieval/waterdata/__init__.py` controls Water Data exports via `__all__`. | ||
| - `dataretrieval.waterdata.utils._default_headers()` adds `X-Api-Key` from `API_USGS_PAT`; never hard-code tokens in examples or tests. | ||
| - Water Data request builders translate Python kwargs to API spellings (`skip_geometry` -> `skipGeometry`, `filter_lang` -> `filter-lang`); tests assert exact URLs/query params. | ||
| - Multi-value OGC params are comma-joined GETs, except `monitoring-locations` which POSTs CQL2 JSON. The OGC edge WAF caps total request bytes (URL + body) at ~8200, so `dataretrieval/waterdata/chunking.py` auto-splits oversized queries across sub-requests (both GET and POST paths); preserve this when adding new list-shaped kwargs. | ||
| - NLDI requires `geopandas` at import time (`pip install .[nldi]`); other modules fall back to pandas when geopandas is absent. | ||
| - Lint: `ruff check .` and `ruff format --check .` (pinned to the version in | ||
| `.pre-commit-config.yaml` and the CI lint job — keep them aligned). | ||
| - Tests: `coverage run -m pytest tests/ && coverage report`, or focused like | ||
| `pytest tests/waterdata_test.py::test_mock_get_samples`. `coverage report` is | ||
| a merge gate: branch coverage with a `fail_under` ratchet in | ||
| `[tool.coverage.report]`. Chase the uncovered *branch*, not the number -- a | ||
| test written to colour a line green catches nothing and costs a maintenance | ||
| slot. If a path is genuinely unreachable, add it to `exclude_also` with a | ||
| reason, or leave the ratchet alone. | ||
| - Types: `mypy` (`strict = true` in `pyproject.toml`; CI runs it over the | ||
| PR-merged-into-main, so bare `dict`/`list` annotations fail there even if they | ||
| pass on your branch). | ||
| - Structure: `lint-imports`, `xenon --max-absolute C --max-modules B --max-average A dataretrieval`, | ||
| and `complexipy dataretrieval` (max complexity 10). All three gate merges. | ||
| - Docs: install docs deps, `ipython kernel install --name "python3" --user`, then | ||
| `make html` from `docs/`. `make docs` adds doctest+linkcheck (network-dependent). | ||
|
|
||
| ## Testing gotchas | ||
| - The suite is offline by default: `addopts = "-m 'not live'"`. Tests marked | ||
| `@pytest.mark.live` hit real USGS services and run on a schedule | ||
| (`.github/workflows/live-api.yml`); run them locally with `pytest tests/ -m live`. | ||
| - HTTP is mocked with `pytest-httpx`'s `httpx_mock` fixture plus fixtures under | ||
| `tests/data/`; keep new API tests offline. | ||
| - `tests/conftest.py` relaxes the fixture's strict-mode defaults and pins the | ||
| fan-out env (`API_USGS_CONCURRENT=1`, `API_USGS_RETRIES=0`, | ||
| `API_USGS_STALL_TIMEOUT=0`) plus a nonexistent `DATARETRIEVAL_CONFIG`, so tests | ||
| are deterministic and never read a developer's real config. Concurrency and | ||
| retry tests opt back in via `monkeypatch.setenv` inside the test body. | ||
|
|
||
| ## Error messages | ||
| Most callers here are programs — a script, a pipeline stage, an agent — so a | ||
| message is the only channel through which a caller can correct itself. Every | ||
| raise states the problem and then the move that fixes it, in that order. | ||
|
|
||
| - Name the remedy, not just the fault. `"Service not recognized"` gives a caller | ||
| nothing to try next; listing the services it does accept does. For a transport | ||
| failure the remedy is whether to retry, and `transport.pagination. | ||
| paginated_failure_message()` is the model: cause, then `To recover: …`. | ||
| - Don't invent a phrasing for a check that recurs. `dataretrieval/_validation.py` | ||
| owns the wording for the shared shapes — bad value in a closed vocabulary | ||
| (`require_one_of`), missing argument (`require_argument`), incomplete group | ||
| (`require_together`), no filter at all (`require_any_of`), and conflicting | ||
| arguments (`require_exactly_one`, `reject_together`). Reach for one before | ||
| hand-writing a message. A service-specific pointer is not a reason to | ||
| hand-write: every check takes a `remedy=` for the move it cannot derive. Every | ||
| check raises `ValueError` -- one class for a bad argument value, so a caller | ||
| catches by shape rather than by which module rejected it. | ||
| - `require_argument` returns the narrowed value and `require_exactly_one` the | ||
| winning `(name, value)` pair, so use their results rather than re-testing for | ||
| `None` to satisfy mypy — a second, unreachable message beside the first is | ||
| how the two drift apart. | ||
| - **Paste the remedy back before trusting it.** Whatever a message names must be | ||
| a real parameter of the function the *caller* called — not a private helper's | ||
| local, not a prose label — and following it literally must produce a working | ||
| call. Messages that read well have failed all three: `datetime_input` was a | ||
| private local no getter accepts, `configure(Configuration(...))` was a silent | ||
| no-op because `configure` is a context manager, `pip install | ||
| dataretrieval[nldi]` globs in zsh, and a navigation missing its `data_source` | ||
| spelled `None` into the URL and returned an empty frame. Run the corrected | ||
| call against the real service; wording review does not catch these. | ||
| - Shared checks take the caller's spelling. `_validate_data_source`, | ||
| `_format_api_dates`, and `require_one_of` all accept a `name=` so the subject | ||
| of the message is the argument that was actually passed. A helper that hard-codes | ||
| one noun reports the wrong parameter the moment a second call site reuses it. | ||
| - Prefer raising over returning something empty when the library cannot tell | ||
| "no data" from "the service misbehaved": a caller that gets an empty frame has | ||
| no signal to act on. `nldi._query_nldi` is the deliberate exception — a 200 | ||
| with a non-JSON body becomes an empty GeoDataFrame by design. | ||
|
|
||
| ## Implementation notes | ||
| - HTTP client is `httpx` (migrated from `requests` in #289); new code uses | ||
| `httpx` and tests mock with `httpx_mock`. | ||
| - Public getters return `(DataFrame, metadata)`. | ||
| - `dataretrieval/__init__.py` imports the service modules by name and lists them | ||
| in `__all__`; it does not star-import them, so a getter is reached through its | ||
| module (`dataretrieval.nwis.get_record`), never from the top level. `nldi` is | ||
| deliberately absent — it needs `geopandas` at import time, so it is imported on | ||
| demand. `dataretrieval/waterdata/__init__.py` controls Water Data exports via | ||
| `__all__`. | ||
| - The `API_USGS_PAT` credential is owned by the `credentials` leaf and applied as | ||
| the `X-Api-Key` header by `transport.http.default_headers()`, which sends it | ||
| only to the host it belongs to. Never hard-code tokens in examples or tests. | ||
| - Water Data request builders translate Python kwargs to API spellings | ||
| (`skip_geometry` -> `skipGeometry`, `filter_lang` -> `filter-lang`); tests | ||
| assert exact URLs and query params. | ||
| - Multi-value OGC params are comma-joined GETs, except `monitoring-locations` | ||
| which POSTs CQL2 JSON. The OGC edge WAF caps total request bytes (URL + body) | ||
| at ~8200, so `dataretrieval/ogc/chunking.py` auto-splits oversized queries | ||
| across chunks (both GET and POST paths); preserve this when adding new | ||
| list-shaped kwargs. | ||
| - NLDI requires `geopandas` at import time (`pip install .[nldi]`); other modules | ||
| fall back to pandas when geopandas is absent. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these tests run with Linux on GitHub?