Skip to content

Commit 7fe353b

Browse files
thodson-usgsclaude
andcommitted
Use GET with comma-separated values for multi-value waterdata queries
The OGC API now supports comma-separated values for fields like monitoring_location_id, parameter_code, and statistic_id, making POST+CQL2 unnecessary for most services. Update _construct_api_requests to join list params with commas and use GET for daily, continuous, latest-daily, latest-continuous, field-measurements, time-series-metadata, and channel-measurements. The monitoring-locations endpoint does not yet support comma-separated GET parameters (returns 400); it retains the POST+CQL2 path. Closes #210. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c755f6b commit 7fe353b

2 files changed

Lines changed: 58 additions & 28 deletions

File tree

dataretrieval/waterdata/utils.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,11 @@ def _construct_api_requests(
417417
"""
418418
Constructs an HTTP request object for the specified water data API service.
419419
420-
Depending on the input parameters (whether there's lists of multiple
421-
argument values), the function determines whether to use a GET or POST
422-
request, formats parameters appropriately, and sets required headers.
420+
For most services, list parameters are comma-joined and sent as a single
421+
GET request (e.g. ``parameter_code=["00060","00010"]`` becomes
422+
``parameter_code=00060,00010`` in the URL). For services that do not
423+
support comma-separated values (currently only ``monitoring-locations``),
424+
a POST request with CQL2 JSON is used instead.
423425
424426
Parameters
425427
----------
@@ -445,36 +447,41 @@ def _construct_api_requests(
445447
Notes
446448
-----
447449
- Date/time parameters are automatically formatted to ISO8601.
448-
- If multiple values are provided for non-single parameters, a POST request
449-
is constructed.
450-
- The function sets appropriate headers for GET and POST requests.
451450
"""
452451
service_url = f"{OGC_API_URL}/collections/{service}/items"
453452

454-
# Identify which parameters should be included in the POST content body
455-
post_params = {
456-
k: v
457-
for k, v in kwargs.items()
458-
if k not in _DATE_RANGE_PARAMS and isinstance(v, (list, tuple)) and len(v) > 1
459-
}
460-
461-
# Everything else goes into the params dictionary for the URL
462-
params = {k: v for k, v in kwargs.items() if k not in post_params}
463-
# Set skipGeometry parameter (API expects camelCase)
464-
params["skipGeometry"] = skip_geometry
453+
# The monitoring-locations endpoint does not support comma-separated values
454+
# for multi-value GET parameters; CQL2 POST is required for that service.
455+
_cql2_required_services = {"monitoring-locations"}
465456

466-
# If limit is none or greater than 50000, then set limit to max results. Otherwise,
467-
# use the limit
468-
params["limit"] = 50000 if limit is None or limit > 50000 else limit
457+
# Format date/time parameters to ISO8601 first — both routing paths need it.
458+
for key in _DATE_RANGE_PARAMS:
459+
if key in kwargs:
460+
kwargs[key] = _format_api_dates(
461+
kwargs[key],
462+
date=(service == "daily" and key != "last_modified"),
463+
)
469464

470-
# Indicate if function needs to perform POST conversion
471-
POST = bool(post_params)
465+
if service in _cql2_required_services:
466+
# Legacy path: POST with CQL2 for multi-value params
467+
post_params = {
468+
k: v
469+
for k, v in kwargs.items()
470+
if k not in _DATE_RANGE_PARAMS
471+
and isinstance(v, (list, tuple))
472+
and len(v) > 1
473+
}
474+
params = {k: v for k, v in kwargs.items() if k not in post_params}
475+
else:
476+
# Join list/tuple values with commas for multi-value GET parameters.
477+
post_params = {}
478+
params = {
479+
k: ",".join(str(x) for x in v) if isinstance(v, (list, tuple)) else v
480+
for k, v in kwargs.items()
481+
}
472482

473-
# Convert dates to ISO08601 format
474-
for i in _DATE_RANGE_PARAMS:
475-
if i in params:
476-
dates = service == "daily" and i != "last_modified"
477-
params[i] = _format_api_dates(params[i], date=dates)
483+
params["skipGeometry"] = skip_geometry
484+
params["limit"] = 50000 if limit is None or limit > 50000 else limit
478485

479486
# `len()` instead of truthiness: a numpy ndarray would raise on `if bbox:`.
480487
if bbox is not None and len(bbox) > 0:
@@ -490,7 +497,7 @@ def _construct_api_requests(
490497

491498
headers = _default_headers()
492499

493-
if POST:
500+
if post_params:
494501
headers["Content-Type"] = "application/query-cql-json"
495502
request = requests.Request(
496503
method="POST",

tests/waterdata_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from dataretrieval.waterdata.utils import (
3131
_check_monitoring_location_id,
3232
_check_profiles,
33+
_construct_api_requests,
3334
_normalize_str_iterable,
3435
)
3536

@@ -111,6 +112,28 @@ def test_check_profiles():
111112
_check_profiles(service="results", profile="foo")
112113

113114

115+
def test_construct_api_requests_multivalue_get():
116+
"""Multi-value params use GET with comma-separated values for daily service."""
117+
req = _construct_api_requests(
118+
"daily",
119+
monitoring_location_id=["USGS-05427718", "USGS-05427719"],
120+
parameter_code=["00060", "00065"],
121+
)
122+
assert req.method == "GET"
123+
assert "monitoring_location_id=USGS-05427718%2CUSGS-05427719" in req.url
124+
assert "parameter_code=00060%2C00065" in req.url
125+
126+
127+
def test_construct_api_requests_monitoring_locations_post():
128+
"""monitoring-locations uses POST+CQL2 for multi-value params (API limitation)."""
129+
req = _construct_api_requests(
130+
"monitoring-locations",
131+
hydrologic_unit_code=["010802050102", "010802050103"],
132+
)
133+
assert req.method == "POST"
134+
assert req.body is not None
135+
136+
114137
def test_samples_results():
115138
"""Test results call for proper columns"""
116139
df, _ = get_samples(

0 commit comments

Comments
 (0)