Skip to content

Commit cdf2295

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix-waterdata-get-multivalue
2 parents 634c450 + dd70cfa commit cdf2295

16 files changed

Lines changed: 1984 additions & 198 deletions

NEWS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
**05/01/2026:** The `nadp` module is now deprecated. Calling any of `get_annual_MDN_map`, `get_annual_NTN_map`, or `get_zip` will emit a `DeprecationWarning`. The module is scheduled for removal on or after **2026-11-01**. NADP is not a USGS data source; users should retrieve NADP data directly from https://nadp.slh.wisc.edu/.
2+
3+
**04/23/2026:** Added `waterdata.get_nearest_continuous(targets, ...)` — for each of N target timestamps, fetches the single continuous observation closest to that timestamp in one HTTP round-trip (auto-chunked when the resulting CQL filter is long, via the facility added in #238). The helper is designed for workflows that pair many discrete-measurement timestamps with surrounding instantaneous data, which the OGC `time` parameter can't express since it only accepts one instant or one interval per request. Ties at window midpoints are resolved per a configurable `on_tie` ∈ {`"first"`, `"last"`, `"mean"`}; the default `window="PT7M30S"` matches a 15-minute continuous gauge.
4+
5+
**04/22/2026:** Highlights since the `v1.1.0` release (2025-11-26), which shipped the `waterdata` module:
6+
7+
- Added `get_channel` for channel-measurement data (#218) and `get_stats_por` / `get_stats_date_range` for period-of-record and daily statistics (#207).
8+
- Added `get_reference_table` (and made it considerably simpler and faster in #209), then extended it to accept arbitrary collections-API query parameters (#214).
9+
- Removed the deprecated `waterwatch` module (#228) and several defunct NWIS stubs (#222, #225), and added `py.typed` so `dataretrieval` ships type information to downstream users (#186).
10+
- Now supports `pandas` 3.x (#221).
11+
- The OGC `waterdata` getters (`get_continuous`, `get_daily`, `get_field_measurements`, and the six others built on the same OGC collections) now accept `filter` and `filter_lang` kwargs that are passed through to the service's CQL filter parameter. This enables advanced server-side filtering that isn't expressible via the other kwargs — most commonly, OR'ing multiple time ranges into a single request. A long expression made up of a top-level `OR` chain is transparently split into multiple requests that each fit under the server's URI length limit, and the results are concatenated.
12+
113
**12/04/2025:** The `get_continuous()` function was added to the `waterdata` module, which provides access to measurements collected via automated sensors at a high frequency (often 15 minute intervals) at a monitoring location. This is an early version of the continuous endpoint and should be used with caution as the API team improves its performance. In the future, we anticipate the addition of an endpoint(s) specifically for handling large data requests, so it may make sense for power users to hold off on heavy development using the new continuous endpoint.
214

315
**11/24/2025:** `dataretrieval` is pleased to offer a new module, `waterdata`, which gives users access USGS's modernized [Water Data APIs](https://api.waterdata.usgs.gov/). The Water Data API endpoints include daily values, instantaneous values, field measurements (modernized groundwater levels service), time series metadata, and discrete water quality data from the Samples database. Though there will be a period of overlap, the functions within `waterdata` will eventually replace the `nwis` module, which currently provides access to the legacy [NWIS Water Services](https://waterservices.usgs.gov/). More example workflows and functions coming soon. Check `help(waterdata)` for more information.

dataretrieval/nadp.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,22 @@
3131

3232
import io
3333
import re
34+
import warnings
3435
import zipfile
3536

3637
import requests
3738

39+
_DEPRECATION_MESSAGE = (
40+
"The `nadp` module is deprecated and will be removed from `dataretrieval` "
41+
"on or after 2026-11-01. NADP is not a USGS data source; please retrieve "
42+
"NADP data directly from https://nadp.slh.wisc.edu/."
43+
)
44+
45+
46+
def _warn_deprecated():
47+
warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)
48+
49+
3850
NADP_URL = "https://nadp.slh.wisc.edu"
3951
NADP_MAP_EXT = "filelib/maps"
4052

@@ -107,6 +119,8 @@ def get_annual_MDN_map(measurement_type, year, path):
107119
... )
108120
109121
"""
122+
_warn_deprecated()
123+
110124
url = f"{NADP_URL}/{NADP_MAP_EXT}/MDN/grids/"
111125

112126
filename = f"Hg_{measurement_type}_{year}.zip"
@@ -160,6 +174,8 @@ def get_annual_NTN_map(measurement_type, measurement=None, year=None, path="."):
160174
... )
161175
162176
"""
177+
_warn_deprecated()
178+
163179
url = f"{NADP_URL}/{NADP_MAP_EXT}/NTN/grids/{year}/"
164180

165181
filename = f"{measurement_type}_{year}.zip"
@@ -195,6 +211,8 @@ def get_zip(url, filename):
195211
finish docstring
196212
197213
"""
214+
_warn_deprecated()
215+
198216
req = requests.get(url + filename)
199217
req.raise_for_status()
200218

dataretrieval/nldi.py

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
NLDI_API_BASE_URL = "https://api.water.usgs.gov/nldi/linked-data"
1414
_AVAILABLE_DATA_SOURCES = None
1515
_CRS = "EPSG:4326"
16+
_VALID_NAVIGATION_MODES = ("UM", "DM", "UT", "DD")
1617

1718

1819
def _query_nldi(url, query_params, error_message):
@@ -79,19 +80,14 @@ def get_flowlines(
7980
... comid=13294314, navigation_mode="UM"
8081
... )
8182
"""
82-
# validate the navigation mode
83-
_validate_navigation_mode(navigation_mode)
84-
# validate the feature source and comid
83+
navigation_mode = _validate_navigation_mode(navigation_mode)
8584
_validate_feature_source_comid(feature_source, feature_id, comid)
8685
if feature_source:
87-
# validate the feature source
8886
_validate_data_source(feature_source)
89-
9087
url = f"{NLDI_API_BASE_URL}/{feature_source}/{feature_id}/navigation"
91-
query_params = {"distance": str(distance), "trimStart": str(trim_start).lower()}
9288
else:
9389
url = f"{NLDI_API_BASE_URL}/comid/{comid}/navigation"
94-
query_params = {"distance": str(distance)}
90+
query_params = {"distance": str(distance), "trimStart": str(trim_start).lower()}
9591

9692
url += f"/{navigation_mode}/flowlines"
9793
if stop_comid is not None:
@@ -232,43 +228,35 @@ def get_features(
232228
>>> gdf = dataretrieval.nldi.get_features(lat=43.073051, long=-89.401230)
233229
"""
234230

235-
# check only one origin is provided
236-
if (lat and long is None) or (long and lat is None):
231+
if (lat is None) != (long is None):
237232
raise ValueError("Both lat and long are required")
238233

239-
if lat:
240-
if comid:
234+
if lat is not None:
235+
if comid is not None:
241236
raise ValueError(
242237
"Provide only one origin type - comid cannot be provided"
243238
" with lat or long"
244239
)
245-
if feature_source or feature_id:
240+
if feature_source is not None or feature_id is not None:
246241
raise ValueError(
247242
"Provide only one origin type - feature_source and feature_id cannot"
248243
" be provided with lat or long"
249244
)
250-
251-
if not lat:
252-
if (comid or data_source) and navigation_mode is None:
245+
url = f"{NLDI_API_BASE_URL}/comid/position"
246+
query_params = {"coords": f"POINT({long} {lat})"}
247+
err_msg = f"Error getting features for lat '{lat}' and long '{long}'"
248+
else:
249+
if (comid is not None or data_source is not None) and navigation_mode is None:
253250
raise ValueError(
254251
"navigation_mode is required if comid or data_source is provided"
255252
)
256-
# validate the feature source and comid
257253
_validate_feature_source_comid(feature_source, feature_id, comid)
258-
# validate the data source
259-
if data_source:
254+
if data_source is not None:
260255
_validate_data_source(data_source)
261-
# validate feature source
262-
_validate_data_source(feature_source)
263-
# validate the navigation mode
264-
if navigation_mode:
265-
_validate_navigation_mode(navigation_mode)
266-
267-
if lat:
268-
url = f"{NLDI_API_BASE_URL}/comid/position"
269-
query_params = {"coords": f"POINT({long} {lat})"}
270-
else:
256+
if feature_source is not None:
257+
_validate_data_source(feature_source)
271258
if navigation_mode:
259+
navigation_mode = _validate_navigation_mode(navigation_mode)
272260
if feature_source:
273261
url = f"{NLDI_API_BASE_URL}/{feature_source}/{feature_id}/navigation"
274262
else:
@@ -280,19 +268,7 @@ def get_features(
280268
else:
281269
url = f"{NLDI_API_BASE_URL}/{feature_source}/{feature_id}"
282270
query_params = {}
283-
284-
if lat:
285-
err_msg = f"Error getting features for lat '{lat}' and long '{long}'"
286-
elif feature_source:
287-
err_msg = (
288-
f"Error getting features for feature source '{feature_source}'"
289-
f" and feature_id '{feature_id}, and data source '{data_source}'"
290-
)
291-
else:
292-
err_msg = (
293-
f"Error getting features for comid '{comid}'"
294-
f" and data source '{data_source}'"
295-
)
271+
err_msg = _features_err_msg(feature_source, feature_id, comid, data_source)
296272

297273
feature_collection = _query_nldi(url, query_params, err_msg)
298274
if as_json:
@@ -413,28 +389,26 @@ def search(
413389
... )
414390
415391
"""
416-
if (lat and long is None) or (long and lat is None):
392+
if (lat is None) != (long is None):
417393
raise ValueError("Both lat and long are required")
418394

419-
# validate find
420395
find = find.lower()
421396
if find not in ("basin", "flowlines", "features"):
422397
raise ValueError(
423398
f"Invalid value for find: {find} - allowed values are:"
424399
f" 'basin', 'flowlines', or 'features'"
425400
)
426-
if lat and find != "features":
401+
if lat is not None and find != "features":
427402
raise ValueError(
428403
f"Invalid value for find: {find} - lat/long is to get features not {find}"
429404
)
430-
if comid and find == "basin":
405+
if comid is not None and find == "basin":
431406
raise ValueError(
432407
"Invalid value for find: basin - comid is to get features"
433408
" or flowlines not basin"
434409
)
435410

436-
if lat:
437-
# get features by hydrologic location
411+
if lat is not None:
438412
return get_features(lat=lat, long=long, as_json=True)
439413

440414
if find == "basin":
@@ -443,6 +417,11 @@ def search(
443417
)
444418

445419
if find == "flowlines":
420+
if navigation_mode is None:
421+
raise ValueError(
422+
"navigation_mode is required for find='flowlines';"
423+
f" allowed values are {_VALID_NAVIGATION_MODES}"
424+
)
446425
return get_flowlines(
447426
navigation_mode=navigation_mode,
448427
distance=distance,
@@ -475,18 +454,36 @@ def _validate_data_source(data_source: str):
475454
url, {}, "Error getting available data sources"
476455
)
477456
_AVAILABLE_DATA_SOURCES = [ds["source"] for ds in available_data_sources]
478-
if data_source not in _AVAILABLE_DATA_SOURCES:
479-
err_msg = (
480-
f"Invalid data source '{data_source}'."
481-
f" Available data sources are: {_AVAILABLE_DATA_SOURCES}"
482-
)
483-
raise ValueError(err_msg)
457+
458+
if data_source not in _AVAILABLE_DATA_SOURCES:
459+
err_msg = (
460+
f"Invalid data source '{data_source}'."
461+
f" Available data sources are: {_AVAILABLE_DATA_SOURCES}"
462+
)
463+
raise ValueError(err_msg)
484464

485465

486-
def _validate_navigation_mode(navigation_mode: str):
487-
navigation_mode = navigation_mode.upper()
488-
if navigation_mode not in ("UM", "DM", "UT", "DD"):
489-
raise TypeError(f"Invalid navigation mode '{navigation_mode}'")
466+
def _features_err_msg(feature_source, feature_id, comid, data_source) -> str:
467+
if feature_source is not None:
468+
return (
469+
f"Error getting features for feature source '{feature_source}'"
470+
f" and feature_id '{feature_id}', and data source '{data_source}'"
471+
)
472+
return f"Error getting features for comid '{comid}' and data source '{data_source}'"
473+
474+
475+
def _validate_navigation_mode(navigation_mode: str | None) -> str:
476+
if navigation_mode is None:
477+
raise ValueError(
478+
f"navigation_mode is required; allowed values are {_VALID_NAVIGATION_MODES}"
479+
)
480+
normalized = navigation_mode.upper()
481+
if normalized not in _VALID_NAVIGATION_MODES:
482+
raise ValueError(
483+
f"Invalid navigation mode '{navigation_mode}';"
484+
f" allowed values are {_VALID_NAVIGATION_MODES}"
485+
)
486+
return normalized
490487

491488

492489
def _validate_feature_source_comid(

dataretrieval/waterdata/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
get_stats_por,
2626
get_time_series_metadata,
2727
)
28+
from .filters import FILTER_LANG
29+
from .nearest import get_nearest_continuous
2830
from .types import (
2931
CODE_SERVICES,
3032
PROFILE_LOOKUP,
@@ -34,6 +36,7 @@
3436

3537
__all__ = [
3638
"CODE_SERVICES",
39+
"FILTER_LANG",
3740
"PROFILES",
3841
"PROFILE_LOOKUP",
3942
"SERVICES",
@@ -45,6 +48,7 @@
4548
"get_latest_continuous",
4649
"get_latest_daily",
4750
"get_monitoring_locations",
51+
"get_nearest_continuous",
4852
"get_reference_table",
4953
"get_samples",
5054
"get_stats_date_range",

0 commit comments

Comments
 (0)