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 dataretrieval/waterdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
get_latest_daily,
get_monitoring_locations,
get_peaks,
get_queryables,
get_reference_table,
get_samples,
get_samples_summary,
Expand Down Expand Up @@ -62,6 +63,7 @@
"get_monitoring_locations",
"get_nearest_continuous",
"get_peaks",
"get_queryables",
"get_ratings",
"get_reference_table",
"get_samples",
Expand Down
153 changes: 153 additions & 0 deletions dataretrieval/waterdata/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import httpx
import pandas as pd

from dataretrieval.ogc.engine import OGC_API_URL
from dataretrieval.ogc.filters import FILTER_LANG
from dataretrieval.utils import (
HTTPX_DEFAULTS,
Expand Down Expand Up @@ -74,6 +75,7 @@ def get_daily(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Daily data provide one data value to represent water conditions for the
day.
Expand Down Expand Up @@ -206,6 +208,13 @@ def get_daily(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -295,6 +304,7 @@ def get_continuous(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""
Continuous data provide instantaneous water conditions.
Expand Down Expand Up @@ -421,6 +431,13 @@ def get_continuous(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -520,6 +537,7 @@ def get_monitoring_locations(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Location information is basic information about the monitoring location
including the name, identifier, agency responsible for data collection, and
Expand Down Expand Up @@ -738,6 +756,13 @@ def get_monitoring_locations(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -808,6 +833,7 @@ def get_time_series_metadata(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Daily data and continuous measurements are grouped into time series,
which represent a collection of observations of a single parameter,
Expand Down Expand Up @@ -975,6 +1001,13 @@ def get_time_series_metadata(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1080,6 +1113,7 @@ def get_combined_metadata(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Get combined monitoring-location and time-series metadata.

Expand Down Expand Up @@ -1182,6 +1216,13 @@ def get_combined_metadata(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1277,6 +1318,7 @@ def get_latest_continuous(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""This endpoint provides the most recent observation for each time series
of continuous data. Continuous data are collected via automated sensors
Expand Down Expand Up @@ -1406,6 +1448,13 @@ def get_latest_continuous(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1478,6 +1527,7 @@ def get_latest_daily(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Daily data provide one data value to represent water conditions for the
day.
Expand Down Expand Up @@ -1609,6 +1659,13 @@ def get_latest_daily(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1682,6 +1739,7 @@ def get_field_measurements(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Field measurements are physically measured values collected during a
visit to the monitoring location. Field measurements consist of measurements
Expand Down Expand Up @@ -1804,6 +1862,13 @@ def get_field_measurements(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1873,6 +1938,7 @@ def get_field_measurements_metadata(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Get field-measurement metadata: one row per (location, parameter) series.

Expand Down Expand Up @@ -1926,6 +1992,13 @@ def get_field_measurements_metadata(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -1998,6 +2071,7 @@ def get_peaks(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""Get the annual peak streamflow / stage record for a monitoring location.

Expand Down Expand Up @@ -2056,6 +2130,13 @@ def get_peaks(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down Expand Up @@ -2198,6 +2279,70 @@ def get_reference_table(
)


def get_queryables(collection: str) -> tuple[pd.DataFrame, BaseMetadata]:
"""List the queryable properties of a Water Data API collection.

Every OGC collection (``daily``, ``continuous``, ``monitoring-locations``,
...) advertises the set of properties that can be filtered on -- exposed as
the typed keyword arguments of the matching ``get_*`` function, and usable
directly in a CQL2 ``filter``. This returns that set, so the available
filters can be discovered programmatically and monitored for upstream
additions.

Parameters
----------
collection : string
The collection id, e.g. ``"daily"``, ``"continuous"``,
``"monitoring-locations"``, or ``"time-series-metadata"``. See
:data:`dataretrieval.waterdata.types.WATERDATA_SERVICES` for the data
collections; reference collections (e.g. ``"parameter-codes"``) work
too.

Returns
-------
df : ``pandas.DataFrame``
One row per queryable, sorted by name, with columns ``queryable`` (the
property name), ``type``, ``title``, and ``description``.
md : :class:`dataretrieval.utils.BaseMetadata`
Metadata describing the request (URL, query time, response headers).

Raises
------
DataRetrievalError
On an HTTP error response (e.g. an unknown ``collection`` yields a 404),
the typed subclass for the status.

Examples
--------
.. doctest::
:skipif: True # network

>>> from dataretrieval import waterdata
>>> df, md = waterdata.get_queryables("daily")
>>> df.set_index("queryable").loc["state_name", "type"]
'string'
"""
url = f"{OGC_API_URL}/collections/{collection}/queryables"
response = _get(url, headers=_default_headers(), **HTTPX_DEFAULTS)
_raise_for_non_200(response)
# The OGC queryables document is a JSON Schema whose ``properties`` map each
# filterable property name to a ``{title, type, description}`` definition.
properties: dict[str, Any] = response.json().get("properties", {})
df = pd.DataFrame(
[
{
"queryable": name,
"type": prop.get("type"),
"title": prop.get("title"),
"description": (prop.get("description") or "").strip(),
}
for name, prop in sorted(properties.items())
],
columns=["queryable", "type", "title", "description"],
)
return df, BaseMetadata(response)


def get_codes(code_service: CODE_SERVICES) -> tuple[pd.DataFrame, BaseMetadata]:
"""Return codes from a Samples code service.

Expand Down Expand Up @@ -2916,6 +3061,7 @@ def get_channel(
filter: str | None = None,
filter_lang: FILTER_LANG | None = None,
convert_type: bool = True,
**queryables: Any,
) -> tuple[pd.DataFrame, BaseMetadata]:
"""
Channel measurements taken as part of streamflow field measurements.
Expand Down Expand Up @@ -3045,6 +3191,13 @@ def get_channel(
and the lexicographic-comparison pitfall.
convert_type : boolean, optional
If True, converts columns to appropriate types.
**queryables : string or iterable of strings, optional
Any other queryable property of this collection, passed through as a
server-side filter (e.g. ``state_name="Wisconsin"``,
``site_type_code="ST"``). See :func:`get_queryables` for a collection's
queryable properties; an unknown name is rejected by the service with a
``DataRetrievalError`` (HTTP 400). This passthrough is provisional and
may be superseded by explicit per-property keyword arguments.

Returns
-------
Expand Down
10 changes: 10 additions & 0 deletions dataretrieval/waterdata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,17 @@ def _get_args(
params such as ``water_year``, ``thresholds``, ``boundingBox``) so they
keep their element types. See :func:`engine._get_args` for the full
normalization contract.

A getter's ``**queryables`` passthrough kwargs are collected by ``locals()``
under the ``queryables`` key; they are flattened in here, so an extra
server-side filter such as ``state_name="Wisconsin"`` is normalized and sent
exactly like a named param. See
:func:`dataretrieval.waterdata.get_queryables` for each collection's
filterable properties (the service rejects an unknown one with a 400).
"""
queryables = local_vars.pop("queryables", None)
if queryables:
local_vars.update(queryables)
return _engine_get_args(local_vars, exclude, no_normalize=_NO_NORMALIZE_PARAMS)


Expand Down
Loading