Skip to content

Commit 634c450

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 DOI-USGS#210. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c4d0f84 commit 634c450

2 files changed

Lines changed: 62 additions & 32 deletions

File tree

dataretrieval/waterdata/utils.py

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,11 @@ def _construct_api_requests(
349349
"""
350350
Constructs an HTTP request object for the specified water data API service.
351351
352-
Depending on the input parameters (whether there's lists of multiple
353-
argument values), the function determines whether to use a GET or POST
354-
request, formats parameters appropriately, and sets required headers.
352+
For most services, list parameters are comma-joined and sent as a single
353+
GET request (e.g. ``parameter_code=["00060","00010"]`` becomes
354+
``parameter_code=00060,00010`` in the URL). For services that do not
355+
support comma-separated values (currently only ``monitoring-locations``),
356+
a POST request with CQL2 JSON is used instead.
355357
356358
Parameters
357359
----------
@@ -377,51 +379,57 @@ def _construct_api_requests(
377379
Notes
378380
-----
379381
- Date/time parameters are automatically formatted to ISO8601.
380-
- If multiple values are provided for non-single parameters, a POST request
381-
is constructed.
382-
- The function sets appropriate headers for GET and POST requests.
383382
"""
384383
service_url = f"{OGC_API_URL}/collections/{service}/items"
385384

386-
# Single parameters can only have one value
385+
# The monitoring-locations endpoint does not support comma-separated values
386+
# for multi-value GET parameters; CQL2 POST is required for that service.
387+
_cql2_required_services = {"monitoring-locations"}
387388
single_params = {"datetime", "last_modified", "begin", "end", "time"}
388389

389-
# Identify which parameters should be included in the POST content body
390-
post_params = {
391-
k: v
392-
for k, v in kwargs.items()
393-
if k not in single_params and isinstance(v, (list, tuple)) and len(v) > 1
394-
}
390+
if service in _cql2_required_services:
391+
# Legacy path: POST with CQL2 for multi-value params
392+
post_params = {
393+
k: v
394+
for k, v in kwargs.items()
395+
if k not in single_params and isinstance(v, (list, tuple)) and len(v) > 1
396+
}
397+
params = {k: v for k, v in kwargs.items() if k not in post_params}
398+
else:
399+
# Format date/time parameters to ISO8601 before comma-joining.
400+
time_periods = {"last_modified", "datetime", "time", "begin", "end"}
401+
for key in time_periods:
402+
if key in kwargs:
403+
kwargs[key] = _format_api_dates(
404+
kwargs[key],
405+
date=(service == "daily" and key != "last_modified"),
406+
)
407+
post_params = {}
408+
# Join list/tuple values with commas for multi-value GET parameters.
409+
params = {
410+
k: ",".join(str(x) for x in v) if isinstance(v, (list, tuple)) else v
411+
for k, v in kwargs.items()
412+
}
395413

396-
# Everything else goes into the params dictionary for the URL
397-
params = {k: v for k, v in kwargs.items() if k not in post_params}
398-
# Set skipGeometry parameter (API expects camelCase)
399414
params["skipGeometry"] = skip_geometry
400-
401-
# If limit is none or greater than 50000, then set limit to max results. Otherwise,
402-
# use the limit
403415
params["limit"] = 50000 if limit is None or limit > 50000 else limit
404416

405-
# Indicate if function needs to perform POST conversion
406-
POST = bool(post_params)
407-
408-
# Convert dates to ISO08601 format
409-
time_periods = {"last_modified", "datetime", "time", "begin", "end"}
410-
for i in time_periods:
411-
if i in params:
412-
dates = service == "daily" and i != "last_modified"
413-
params[i] = _format_api_dates(params[i], date=dates)
417+
# Convert dates to ISO8601 for the legacy (CQL2) path.
418+
if service in _cql2_required_services:
419+
time_periods = {"last_modified", "datetime", "time", "begin", "end"}
420+
for i in time_periods:
421+
if i in params:
422+
dates = service == "daily" and i != "last_modified"
423+
params[i] = _format_api_dates(params[i], date=dates)
414424

415-
# String together bbox elements from a list to a comma-separated string,
416-
# and string together properties if provided
417425
if bbox:
418426
params["bbox"] = ",".join(map(str, bbox))
419427
if properties:
420428
params["properties"] = ",".join(properties)
421429

422430
headers = _default_headers()
423431

424-
if POST:
432+
if post_params:
425433
headers["Content-Type"] = "application/query-cql-json"
426434
request = requests.Request(
427435
method="POST",

tests/waterdata_test.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
get_stats_por,
2222
get_time_series_metadata,
2323
)
24-
from dataretrieval.waterdata.utils import _check_profiles
24+
from dataretrieval.waterdata.utils import _check_profiles, _construct_api_requests
2525

2626

2727
def mock_request(requests_mock, request_url, file_path):
@@ -65,6 +65,28 @@ def test_check_profiles():
6565
_check_profiles(service="results", profile="foo")
6666

6767

68+
def test_construct_api_requests_multivalue_get():
69+
"""Multi-value params use GET with comma-separated values for daily service."""
70+
req = _construct_api_requests(
71+
"daily",
72+
monitoring_location_id=["USGS-05427718", "USGS-05427719"],
73+
parameter_code=["00060", "00065"],
74+
)
75+
assert req.method == "GET"
76+
assert "monitoring_location_id=USGS-05427718%2CUSGS-05427719" in req.url
77+
assert "parameter_code=00060%2C00065" in req.url
78+
79+
80+
def test_construct_api_requests_monitoring_locations_post():
81+
"""monitoring-locations uses POST+CQL2 for multi-value params (API limitation)."""
82+
req = _construct_api_requests(
83+
"monitoring-locations",
84+
hydrologic_unit_code=["010802050102", "010802050103"],
85+
)
86+
assert req.method == "POST"
87+
assert req.body is not None
88+
89+
6890
def test_samples_results():
6991
"""Test results call for proper columns"""
7092
df, _ = get_samples(

0 commit comments

Comments
 (0)