Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Context

The shared vocabulary for `dataretrieval`. This is a glossary, not a
specification: it fixes what words mean so that code, docstrings, ADRs, and
conversation use them the same way. Architectural decisions live in
`docs/source/architecture/decisions/`.

When a term here conflicts with a name in the code, the term wins and the name
is legacy. Legacy names are called out below rather than quietly tolerated.

## Retrieval

**Getter** — A public function that retrieves data and returns
`(DataFrame, metadata)`. The package's unit of public API. `waterdata.get_daily`
and `wqp.get_results` are getters; the helpers they call are not.

**Query** — One logical request a caller makes by calling a getter. A query may
reach the service as several requests; it is still one query.

**Chunk** — One of the requests a query was split into. Following Dask, where
chunks describe how an array is split into sub-arrays, a chunk is a piece of the
whole, named for *being a piece* rather than for why it was made one.

A query needing no split has exactly one chunk, not zero.

**Chunking** — How a query is split into chunks. A query may be chunked because
the service forces it — a URL over the byte limit, or an API accepting one
location per request — or because the caller asked for it. Both produce chunks;
the reason is not part of the term.

**Plan** — An enumeration of a query's chunks: how many there are, and what each
one is. A plan says how a query divides; it does not execute. Computing a plan
is protocol-specific — a byte budget, a per-location rule — while executing one
is not, which is why the two live apart.

**Fan-out** — Executing a query's chunks concurrently. Chunking is how the work
divides; fan-out is how it is distributed. The two are independent, and only
chunking depends on the service's protocol.

**Page** — One response in a cursor-followed sequence from a single chunk. A
page is *not* a chunk: chunks divide a query, pages divide a chunk's response.
A chunk of a large query commonly spans many pages.

## Failure and resumption

**Transient failure** — A failure a later attempt could survive: a rate limit, a
service error, a timeout. Distinguished from a **deterministic failure**, which
would fail identically every time — an unresolvable hostname, an unsupported
scheme, a malformed request. Only transient failures are retried, and only
transient failures produce a resumable interruption. The two answers are one
judgement about what a failure means, and must agree.

**Interruption** — A transient failure that stopped a fan-out partway, raised
with the completed chunks preserved. The caller may wait for the condition to
clear and resume.

**Resume** — Continuing an interrupted query by re-issuing only the chunks that
did not complete. Completed chunks are never re-fetched. A chunk that failed
partway through its pages is not complete, so resuming re-walks its pages from
the start.

## Services

Each is an external system this package retrieves from. They are separate
services with separate conventions, not one API with modes.

**Water Data** — The modern USGS API at `api.waterdata.usgs.gov`, covering
monitoring locations, time series, field measurements, samples, ratings, and
statistics. The package's primary target.

**NGWMN** — The National Ground-Water Monitoring Network, a distinct OGC API
covering sites, water levels, lithology, well construction, and providers.

**NWDC** — The National Water Availability Assessment Data Companion, providing
modeled national-scale water-use data.

**WQP** — The Water Quality Portal, a multi-agency water-quality clearinghouse.

**NLDI** — The Network Linked Data Index, which navigates the hydrologic network
from an origin to connected features, flowlines, or basins.

**NWIS** — The legacy USGS waterservices interface. Deprecated: it is retained
for compatibility and is not where new work goes.

**StreamStats** — Basin characteristics and delineation for a point on a stream.

## Data

**Collection** — One named set of records a service offers — `daily`,
`monitoring-locations`, `time-series-metadata`. The unit a getter targets.

A collection is not a service. Water Data is a service; `daily` is one of its
collections. The distinction matters because the OGC machinery is shared: the
same code path retrieves a Water Data collection and an NGWMN one, and only the
service differs.

**Collection family** — A group of collections sharing a shape and therefore a
getter signature. Their getters deliberately resemble one another; the
resemblance is the public contract, not duplication to be removed.

**Monitoring location** — A place where measurements are recorded. The canonical
term. Legacy: the deprecated NWIS getters and the WQP profiles call this a
*site*, and their parameters keep that spelling.

**Metadata** — The second half of every getter's return: the request URL, the
elapsed time, and the response headers. Describes the *retrieval*, not the data.

## Boundaries

**Adapter** — A module owning one service's conventions: its URLs, parameters,
error shapes, and response quirks. Adapters may use shared machinery; shared
machinery may not know about adapters.

**Facade** — A module that re-exports a subsystem's public surface and contains
no logic of its own, so callers depend on a stable name rather than on internal
layout.

**Leaf** — A module with no dependencies inside the package beyond other leaves,
holding one general mechanism so that anything may use it without acquiring the
rest of the package. Before writing a small helper, check whether a leaf already
generalizes it.

**Transport** — The service-neutral machinery for issuing requests: timeouts,
retry, pagination, fan-out, aggregation. It names no service and no protocol,
and is not public API.

## Known legacy names

Recorded so they are not mistaken for the canonical term, and not re-litigated:

- `completed_chunks` / `total_chunks` on interruptions, and `set_chunks()` /
`start_chunk()` on the progress reporter, count chunks as defined above and
are consistent with this glossary. They predate it; the agreement is real
rather than coincidental.
- `ChunkInterrupted` is a permanent alias of `FanOutInterrupted` — the same
class object under the name it was first published as. Both spellings are
correct; neither is scheduled for removal.
- `site` appears in deprecated NWIS and WQP parameter names where *monitoring
location* is meant. These are frozen public surfaces and will not be renamed.
Where the Water Data API itself names a thing `site-types` or
`site_type_code`, that is the service's vocabulary and is reproduced
faithfully rather than translated.
- `service` named a collection throughout the OGC machinery. Resolved: the
OGC internals, the Water Data wrappers, and all eleven typed getters now say
`collection`; `waterdata.get_cql` takes `collection`; and the type alias is
`WATERDATA_COLLECTIONS`. `service=` on `get_cql` and the `WATERDATA_SERVICES`
alias remain — a deprecated keyword and a permanent alias respectively.

`service` still means the external system in `transport` and `progress`,
where it labels a progress line. That usage is correct.
- `waterdata.get_samples(service=)` names a *resource*, not a service. The
Samples OpenAPI document declares `results`, `locations`, `activities`,
`projects` and `organizations` as tags and titles itself the "Resource
Center"; it never calls them services. They are also not collections --
they share 22 of 23 query parameters, so they are five projections of one
query rather than five sets of data, and the OGC definition of *collection*
is scoped to "access mechanisms defined by OGC API standard(s)", which
Samples does not implement. Kept as-is by decision: renaming a public
keyword costs a deprecation cycle for a term with no better-evidenced
replacement in reach.
- `waterdata.get_codes(code_service=)` is correct and stays. The Samples
documentation calls it a "code service" in prose and serves it from
`/codeservice/`, so this reproduces the service's own vocabulary, like
`site-types`.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**08/09/2026:** `waterdata.get_cql` takes `collection` rather than `service`. OGC API - Features (17-069r4) normatively names this value the `collectionId`: Requirement 20 fixes the path template `/collections/{collectionId}/items`, and Requirement 18 defines `collectionId` as each `id` in the collections response -- which is literally how the package builds the URL, and what the live API returns. *Service* names the API itself (Water Data, NGWMN). **Deprecation:** `service=` still works and resolves to `collection`, with a `DeprecationWarning`; it will be removed on or after 2027-08-09. Positional callers (`get_cql("daily", cql)`) are unaffected. The `WATERDATA_SERVICES` type alias is now `WATERDATA_COLLECTIONS`, with `WATERDATA_SERVICES` retained as a permanent alias for the same object. Terms are defined in `CONTEXT.md`.

**08/09/2026:** Every retrieval path now runs through one executor. `waterdata.get_cql` (via the OGC `fetch_ogc_request`) and `waterdata.get_stats_por` / `get_stats_date_range` (via the Statistics page walk) previously bypassed `dataretrieval.transport.fanout.FanOut` through a private sync bridge, which meant they were the only getters in the package with **no retry**: a mid-page-walk 429 or 503 failed the whole call while every typed getter and Water Use rode it out. Both now run as a one-item fan-out and the 25-line `transport/sync.py` is gone. **Behavior change:** those three getters now retry transient failures (`API_USGS_RETRIES`, default 4) and, when the retries are exhausted, raise the resumable `ServiceInterrupted` / `QuotaExhausted` rather than `ServiceUnavailable` / `RateLimited` / `NetworkError` — all remain `DataRetrievalError`, so broad handlers are unaffected, but narrow handlers around those calls must widen, and `.call.resume()` is now available on the interruption. A failure that retrying cannot fix (bad scheme, a hostname that does not resolve) still surfaces as `NetworkError` immediately. The progress line moved with it: `FanOut.resume()` opens the reporter it ticks into, so a driver can no longer run the shared executor and silently print nothing, and a `.call.resume()` fired long after the interruption now reports progress instead of running mute. Internal tidying with no public effect: the WQX3 / legacy-WQP CSV datetime shaping moved out of `dataretrieval.utils` (whose docstring reserves it for non-service-specific shaping) into the `dataretrieval._wqx` leaf; the five Water Data endpoint URLs are declared once in `dataretrieval.waterdata.endpoints` instead of being derived in three modules; the OGC queryables document is parsed by `dataretrieval.ogc.schema` so every OGC adapter can offer the table, with `waterdata.get_queryables` unchanged as its documented wrapper; and `ogc/engine.py` imports each symbol from the module that defines it.

**08/09/2026:** Internal structure cleanup, no public API change. Validating a server-supplied next-page link is now one policy in `dataretrieval.transport.links` instead of three divergent copies (the OGC engine, the ratings STAC walk, and Water Use). Two of those copies were fixed by the merge: the OGC page walk now resolves a *relative* `next` href against the page it came from (it previously handed the unresolved reference back as the pagination cursor) and refuses an unparseable one rather than following it unchecked. Cross-host refusal, credential stripping, and Water Use's host-alias rewrite are unchanged, as is the error type each walk raises. `parse_retry_after` moved to `dataretrieval.exceptions`, next to the `DataRetrievalError.retry_after` field it exists to produce. The one-shot HTTP query path (`query`, `to_str`, and their helpers) moved out of `dataretrieval.utils` into the private `dataretrieval._querying`; `dataretrieval.utils.query` and `dataretrieval.utils.to_str` remain the documented public paths, as `Ambient` and `BaseMetadata` already do. `waterdata` profile validation moved next to the tables it validates in `waterdata.types`, and `nwis.get_dv`/`get_iv` now share one body.
Expand Down
6 changes: 3 additions & 3 deletions dataretrieval/combining.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Result recombination: merge per-chunk frames and responses (no I/O).

These utilities assemble the output of a chunked/fan-out call from its
individual per-sub-request results. They have no event-loop, retry, or
individual per-chunk results. They have no event-loop, retry, or
network state — they're pure data transforms shared by protocol-specific
chunk execution, service fan-out, and cursor-driven pagination.

Expand Down Expand Up @@ -157,7 +157,7 @@ def _combine_chunk_responses(
responses: list[httpx.Response], canonical_url: str | None
) -> httpx.Response:
"""
Fold per-sub-request responses into a single aggregated response.
Fold per-chunk responses into a single aggregated response.

For a multi-response input, returns a shallow copy of
``responses[0]`` with ``.headers`` set to those of the response reporting
Expand All @@ -174,7 +174,7 @@ def _combine_chunk_responses(
Parameters
----------
responses : list[httpx.Response]
One response per completed sub-request, in caller-provided order.
One response per completed chunk, in caller-provided order.
canonical_url : str or None
URL of the unchunked original request. ``None`` skips the URL
override — used by the passthrough path (the fetcher's
Expand Down
4 changes: 2 additions & 2 deletions dataretrieval/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ class Unchunkable(RequestTooLarge):
"""No chunking plan fits the URL byte limit.

Raised by the Water Data chunker when even the smallest reducible plan
(every list axis at one atom per sub-request, the filter at one clause per
sub-request) still exceeds the server's byte limit. Unlike
(every list axis at one atom per chunk, the filter at one clause per
chunk) still exceeds the server's byte limit. Unlike
:class:`URLTooLong`, then, automatic splitting has already been tried and
exhausted. Shrink the input lists, simplify the filter, or split the call
manually.
Expand Down
Loading