Async Python client for the USACE NWD Dataquery 2.0 hydrologic timeseries endpoint.
Project description
nwd-dataquery
Async Python client for the USACE Northwestern Division Dataquery 2.0 hydrologic timeseries endpoint.
The underlying endpoint (https://www.nwd-wc.usace.army.mil/dd/common/web_service/webexec/getjson) is undocumented. This package was reverse-engineered from the Dataquery 2.0 UI and targets NWD-only data (e.g. Lake Washington Ship Canal, Howard Hanson, Mud Mountain). For districts that are migrated to the modern CWMS Data API, use cwms-python instead.
Install
pip install nwd-dataquery # core: pyarrow output + CLI
pip install nwd-dataquery[polars] # adds polars frame support
pip install nwd-dataquery[pandas] # adds pandas frame support
Python ≥3.12.
A note on SSL
The USACE host serves only the leaf certificate, omitting the DigiCert intermediate. Python's stdlib ssl doesn't perform AIA chasing, so default verification fails (unable to get local issuer certificate) on any platform whose TLS stack doesn't fetch missing intermediates on its own. The client transparently fetches the intermediate via the leaf's AIA extension (using aia-chaser) the first time a session is opened to an HTTPS endpoint, and reuses the resulting SSLContext thereafter. No global SSL stack mutation occurs on import.
Alternate endpoint
A public mirror reportedly exists at public.crohms.org (Columbia River Operational Hydromet Management System, a multi-agency partnership) using the same URL paths. Point to it via --endpoint / AsyncDataQueryClient(endpoint=...) if the primary host is unreachable. Unverified.
Quick start (Python)
import asyncio
from datetime import datetime, timezone
from nwd_dataquery import AsyncDataQueryClient
async def main():
async with AsyncDataQueryClient() as client:
# Default: last 7 days, pyarrow.Table out
table = await client.fetch("LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW")
print(table.to_pandas().head())
# Decade backfill in one request
backfill = await client.fetch(
"LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW",
start=datetime(2016, 1, 1, tzinfo=timezone.utc),
end=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
# Metadata only
meta = await client.describe("LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW")
asyncio.run(main())
Switch frame types:
tbl = await client.fetch(tsid) # pyarrow.Table (default)
df = await client.fetch(tsid, backend="polars") # requires nwd-dataquery[polars]
df = await client.fetch(tsid, backend="pandas") # requires nwd-dataquery[pandas]
Quick start (CLI)
nwd-dq fetch LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW --lookback 30d
nwd-dq fetch LWSC.Flow-In.Ave.~1Day.1Day.CENWS-COMPUTED-RAW \
--start 2016-01-01 --format parquet --out flows.pq
nwd-dq describe LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW | jq
Exit codes: 0 success, 1 transport error, 2 server/data-query error, 3 empty result with --strict.
Output schema
fetch() returns a long-format frame with columns:
| column | type | meaning |
|---|---|---|
timestamp |
timestamp[us, tz=UTC] |
observation time |
value |
float64 |
measurement |
quality |
int64 |
server quality flag (may be null) |
tsid |
string |
CWMS timeseries id |
location |
string |
location code (LWSC, …) |
parameter |
string |
parameter name (Elev-Lake, Flow-In, …) |
units |
string |
server-reported units (FT, CFS, …) |
TSID anatomy
The 6-part CWMS identifier: LOC.PARAMETER.TYPE.INTERVAL.DURATION.VERSION.
TYPE—Inst(instantaneous) orAve(interval-averaged).INTERVAL—0,15Minutes,1Hour,~1Day,1Day. A leading~marks irregular cadence.DURATION—0for point observations, or an interval for aggregations.VERSION—SOURCE-QUALITY. Sources observed:NWSRADIO,IRIDIUM,GOES,USGS,USBR,CENWS-COMPUTED,CENWP-COMPUTED,CENWW-COMPUTED,CBT,RFC-NOS,NOAA,MIXED-COMPUTED. Quality isRAWorREV. The special versionBestis an alias for whichever source/quality is canonical for that series — prefer it for downstream consumption and keep the raw-version tsids for provenance.
Known tsids
| tsid | location | description | period of record |
|---|---|---|---|
LWSC.Elev-Lake.Ave.1Hour.0.NWSRADIO-RAW |
LWSC | Pool elevation — hourly average, NWS radio DCP, raw | 2001–present |
LWSC.Elev-Lake.Inst.1Hour.0.NWSRADIO-REV |
LWSC | Pool elevation — hourly instantaneous, NWS radio DCP, reviewed | — |
LWSC.Elev-Lake.Ave.1Hour.1Hour.IRIDIUM-REV |
LWSC | Pool elevation — hourly average, Iridium satellite, reviewed | — |
LWSC.Flow-In.Ave.~1Day.1Day.CENWS-COMPUTED-RAW |
LWSC | Daily average inflow — computed by Seattle District | — |
See Discovering tsids for how to grow this list.
Discovering tsids
This package covers only the getjson endpoint — there is no catalog or search API from its point of view. Practical paths:
1. The Dataquery 2.0 UI. Open https://www.nwd-wc.usace.army.mil/dd/common/dataquery/, navigate to a station, and watch the Network tab in DevTools. XHR requests to webexec/getjson include the tsid in the query= parameter — copy it out.
2. Grammar-based expansion from a seed. Given one tsid you already know, enumerate plausible variants by swapping parts — Ave↔Inst, different INTERVALs, different VERSIONs like NWSRADIO-RAW → IRIDIUM-REV → Best — and probe each with fetch(). An empty payload (triggers UnknownTsidWarning) means the variant doesn't exist or has no data; a non-empty payload gives you a new confirmed tsid. See TSID anatomy for the vocabulary.
3. Track your own list. The endpoint has no "list all" verb. Most users accumulate a curated list of tsids as they explore.
Gotchas
- Empty payload is ambiguous. The server returns
{}for "unknown tsid," "no data in the requested window," and for seasonal tsids that aren't currently deployed (e.g. temporary summer gauges). The client always emitsUnknownTsidWarning; you can't distinguish the cases without out-of-band knowledge. - Everything comes back as
text/plain. Both successful responses and server-side errors useContent-Type: text/plain; charset=UTF-8and always respond HTTP 200. The client parses the body, checks for a top-level"error"key, and raisesDataQueryErrorif present. - Wildcards don't work.
["LWSC.*"]returns{}. Query the UI to discover tsids. - Seasonal stations move in and out of the catalog. Some station codes only appear in the Dataquery UI while the underlying gauge is physically deployed. Treat your tsid list as a moving target, not a fixed registry.
Development
uv sync --all-extras --group dev
prek install
uv run pytest # unit + integration (cassettes)
uv run pytest -m live # live smoke (requires network)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nwd_dataquery-0.2.0.tar.gz.
File metadata
- Download URL: nwd_dataquery-0.2.0.tar.gz
- Upload date:
- Size: 86.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6083057bb7519f156cd544cc36c1770478e1b3eb2c9dec291e2c1b2a4f9a0a0
|
|
| MD5 |
a8eb3e2bf7677f80f779db6449f5e321
|
|
| BLAKE2b-256 |
58ca33c6173023ba6025cba1fa492a4ad974a7fdd3c87e2b1a6a308c5db0a732
|
Provenance
The following attestation bundles were made for nwd_dataquery-0.2.0.tar.gz:
Publisher:
release.yml on briandconnelly/nwd-dataquery
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nwd_dataquery-0.2.0.tar.gz -
Subject digest:
d6083057bb7519f156cd544cc36c1770478e1b3eb2c9dec291e2c1b2a4f9a0a0 - Sigstore transparency entry: 1383099068
- Sigstore integration time:
-
Permalink:
briandconnelly/nwd-dataquery@90b677c8b6c5665490d11e7076f0f6476290cd7e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/briandconnelly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@90b677c8b6c5665490d11e7076f0f6476290cd7e -
Trigger Event:
push
-
Statement type:
File details
Details for the file nwd_dataquery-0.2.0-py3-none-any.whl.
File metadata
- Download URL: nwd_dataquery-0.2.0-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7336857ffcf6a97db91950edd99409b438e4475978d914f8dd7c9f51e1228943
|
|
| MD5 |
2b205a67d2c32b848338725289c46d0f
|
|
| BLAKE2b-256 |
c24b603217417a3f1ce8fb001f95d5b0ed7ac6b9a3adebb7f369d5ce1f38c783
|
Provenance
The following attestation bundles were made for nwd_dataquery-0.2.0-py3-none-any.whl:
Publisher:
release.yml on briandconnelly/nwd-dataquery
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nwd_dataquery-0.2.0-py3-none-any.whl -
Subject digest:
7336857ffcf6a97db91950edd99409b438e4475978d914f8dd7c9f51e1228943 - Sigstore transparency entry: 1383099099
- Sigstore integration time:
-
Permalink:
briandconnelly/nwd-dataquery@90b677c8b6c5665490d11e7076f0f6476290cd7e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/briandconnelly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@90b677c8b6c5665490d11e7076f0f6476290cd7e -
Trigger Event:
push
-
Statement type: