Skip to content
Draft
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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**08/17/2026:** Added raw access to the complete public USGS Water Data STAC surface through the `waterdata.stac` namespace: `get_catalog`, `get_conformance`, `get_collections`, `get_collection`, `get_items`, `get_item`, `get_queryables`, and `search`. Search supports both advertised GET and POST representations; collection and item listing helpers expose every advertised filter and pagination parameter. Each helper returns the unchanged STAC/GeoJSON/JSON Schema document plus `BaseMetadata`, preserving standard links instead of flattening heterogeneous documents into a DataFrame. Calls share Water Data API-key host scoping, configured base-URL redirection, typed HTTP errors, and bounded retries. Internal `/_mgmt` health routes are intentionally not exposed as STAC capabilities.

**08/13/2026:** Warning categories now say what they mean. Two advisories about *upstream data* were emitted as `DeprecationWarning` and as an uncategorized `warnings.warn` respectively; both are now `DataCurrencyWarning` (a `UserWarning` subclass, exported as `dataretrieval.DataCurrencyWarning`). **Behavior change:** WQP's legacy-WQX notice moves from `DeprecationWarning` to `DataCurrencyWarning`. Because `legacy=True` is the default on every WQP getter and the notice is unconditional, a downstream project running `-W error::DeprecationWarning` previously could not call any WQP getter with default arguments; it now can. The flip side is visibility — `DeprecationWarning` is silent by default outside `__main__`, so this notice will now print to stderr for library and notebook callers who never saw it. Silence it with `warnings.filterwarnings("ignore", category=dataretrieval.DataCurrencyWarning)`, or set `legacy=False` where a WQX3.0 profile exists. The NWIS qw-endpoint retirement notice gains the same category (it previously had none, arriving as a bare `UserWarning` that deprecation filters ignored). `DeprecationWarning` now means only that a name in this package is going away, always with a replacement and, where published, a removal date; those horizons are declared once in `dataretrieval._deprecation.REMOVALS` rather than spelled at each call site.

**08/11/2026:** Settings resolve through a layered chain instead of the environment alone, and a *configuration profile* is a named set of settings for **one adapter**. The new `dataretrieval.configuration` module resolves every setting in one order, highest first: a configuration passed to an active `dataretrieval.configure(...)` block, a profile that block selected, the setting's `API_USGS_*` environment variable, the adapter's `[<adapter>]` table in `~/.dataretrieval/config.toml` (or `DATARETRIEVAL_CONFIG`), the file's top-level keys, the adapter's own built-in preference, then the package default. Precedence applies **per setting**, so a file that sets only `concurrency` leaves an environment `API_USGS_PAT` in effect, and a `[ngwmn]` table still inherits every top-level key it does not name. `configure()` takes configuration objects positionally, at most one per adapter and nothing else: `configure(Configuration(api_key=vault.read("usgs/pat")), WaterdataConfiguration.load("bulk"), NgwmnConfiguration(concurrency=4))`. The adapter a configuration targets is a property of its class, so a caller never restates it, and each adapter owns its class in the module that *reads* those settings (`waterdata.WaterdataConfiguration`, `ngwmn.NgwmnConfiguration`, `nwdc.NwdcConfiguration`, `wqp.WqpConfiguration`, `nldi.NldiConfiguration`, `streamstats.StreamstatsConfiguration`) — an adapter accepts only the settings it reads, so `[streamstats] parallel_chunks = 8` is an error rather than a line that quietly does nothing. The block is delivered through a `ContextVar`, so a credential set inside it cannot leak across threads or asyncio tasks, which is what makes it safe for a server or notebook handling several users' keys and is the thing assigning to `os.environ` could never do (issue #352). The file gains named profiles beside each adapter's default profile: `[waterdata]` is always in effect, `[waterdata.bulk]` only when a caller selects it with `WaterdataConfiguration.load("bulk")`, and a selected profile still inherits the default profile and the package-wide keys per setting. A profile named in code outranks the setting's environment variable — the one place the ladder inverts the environment-above-file rule, because losing a deliberate selection to a stale shell export is what a caller would file a bug about. An adapter's configuration may also carry a `base_url`, which redirects that adapter's requests for the duration of the block — a staging instance, a mirror, a recording proxy — and no other adapter's; for Water Data one value moves the OGC collections, the Samples database, the statistics service and the STAC catalog together. It is settable in a `configure()` block only: a `base_url` key in the file and an exported `API_USGS_BASE_URL` each raise rather than being read, since a redirect a config file or a shell profile can set is one no reader of the script can see. The API key does not follow a redirect — it is scoped to the single host that honors it — and is deliberately not per-adapter: it authenticates to the gateway fronting a host, and Water Data and NGWMN share that host, one key, and one hourly quota. `dataretrieval.show_configuration()` reports each setting's effective value and where it came from, naming the profile behind each value (`configure() block [waterdata.bulk]` rather than a bare block), listing the profiles a file defines whether or not this run selected any, and naming any adapter this process has not imported rather than omitting it — without ever printing the key. One parser per setting owns its grammar, so a value means the same thing whichever source wrote it. **Breaking change:** `RetryPolicy.from_env()` is now `RetryPolicy.from_configuration()` and resolves through the whole chain rather than the environment alone. **Behavior change:** a credential-shaped keyword passed to a getter's `**kwargs` query passthrough — Water Data's `**queryables` and every WQP getter's search filters — now raises `TypeError` naming `configure(Configuration(api_key=...))` instead of putting a secret in a URL that clients, proxies and logs retain. The names refused are `api_key=`, `token=`, `x_api_key=`, `password=`, `auth=`, `pat=` and similar spellings; a filter the server actually defines is unaffected. **Bug fix:** `API_USGS_STALL_TIMEOUT` was read straight from `os.environ`, so it could not be set by a `configure()` block or the config file and never appeared in `show_configuration()`; it now resolves through the chain like every other setting. Rationale in ADRs 0009, 0010 and 0011; terms in `CONTEXT.md`.
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ df, metadata = waterdata.get_continuous(
print(f"Retrieved {len(df)} continuous gage height measurements")
```

#### Search the Water Data STAC catalog

Use the raw STAC helpers when you need catalog metadata or item documents rather
than parsed rating tables. Responses preserve the standard STAC links and are
returned with request metadata:

```python
collections, metadata = waterdata.stac.get_collections()
items, metadata = waterdata.stac.search(collections=["ratings"], limit=5)

print([collection["id"] for collection in collections["collections"]])
print([item["id"] for item in items["features"]])
```

#### Speeding up large downloads with `parallel_chunks`

By default the getters split a multi-value request only as far as the server's
Expand Down
34 changes: 31 additions & 3 deletions dataretrieval/transport/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"get",
"network_error",
"open_async_client",
"request",
"strip_api_key_from_untrusted_host",
"strip_api_key_from_untrusted_host_async",
]
Expand Down Expand Up @@ -83,8 +84,30 @@ def network_error(url: str | httpx.URL, exc: httpx.TransportError) -> NetworkErr
return NetworkError(f"Could not reach the service at {url}: {detail}")


def get(url: str | httpx.URL, **kwargs: Any) -> httpx.Response:
"""Issue one guarded synchronous GET and map transport failures."""
def request(method: str, url: str | httpx.URL, **kwargs: Any) -> httpx.Response:
"""Issue one guarded synchronous HTTP request.

Parameters
----------
method : str
HTTP method, such as ``"GET"`` or ``"POST"``.
url : str or httpx.URL
Request destination.
**kwargs : Any
Request arguments accepted by :meth:`httpx.Client.request`. Client
options such as ``verify`` and ``timeout`` are applied to the guarded
client instead.

Returns
-------
httpx.Response
The completed response.

Raises
------
NetworkError
If no HTTP response is received.
"""
client_options: dict[str, Any] = {
key: kwargs.pop(key)
for key in ("follow_redirects", "timeout", "transport", "verify")
Expand All @@ -93,11 +116,16 @@ def get(url: str | httpx.URL, **kwargs: Any) -> httpx.Response:
client_options["event_hooks"] = {"request": [strip_api_key_from_untrusted_host]}
try:
with httpx.Client(**client_options) as client:
return client.get(url, **kwargs)
return client.request(method, url, **kwargs)
except httpx.TransportError as exc:
raise network_error(url, exc) from exc


def get(url: str | httpx.URL, **kwargs: Any) -> httpx.Response:
"""Issue one guarded synchronous GET and map transport failures."""
return request("GET", url, **kwargs)


@asynccontextmanager
async def open_async_client(**overrides: Any) -> AsyncIterator[httpx.AsyncClient]:
"""Open a short-lived async client with redirect-safe shared defaults."""
Expand Down
2 changes: 2 additions & 0 deletions dataretrieval/waterdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dataretrieval.ogc.filters import FILTER_LANG

# Public API exports
from . import stac
from .api import (
get_channel,
get_codes,
Expand Down Expand Up @@ -53,6 +54,7 @@
"SERVICES",
"WATERDATA_SERVICES",
"parallel_chunks",
"stac",
"get_channel",
"get_codes",
"get_combined_metadata",
Expand Down
Loading