Skip to content
Merged
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
22 changes: 10 additions & 12 deletions python/waterdata_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,33 @@ The following example demonstrates how one might use `hydrotools.waterdata_clien
### Code

```python
# Import client and geopandas for easy parsing
import geopandas as gpd
from hydrotools.waterdata_client import LatestContinuousClient
from hydrotools.waterdata_client.transformers import to_geodataframe

# Instantiate client
client = LatestContinuousClient()
client = LatestContinuousClient(transformer=to_geodataframe)

# Retrieve data
data = client.get(
observations = client.get(
monitoring_location_id="USGS-02146470"
)

# Look at values
observations = gpd.GeoDataFrame.from_features(data[0])
print(observations)
print(observations.columns)
```

### Output

```console
geometry id time_series_id ... approval_status qualifier last_modified
0 POINT (-80.85306 35.16444) 1b39aa0f-6adb-40ea-ab4e-453f88b8b16c 6ec29c85c72246ea83912c6f02f6fd63 ... Provisional None 2026-04-24T16:06:22.291045+00:00
1 POINT (-80.85306 35.16444) b3ce8113-2a1a-4d13-9d88-8a0237c11df6 27d30d4d1a4749bb887d1435da9fd278 ... Provisional None 2026-04-24T16:06:35.455215+00:00
2 POINT (-80.85306 35.16444) e56f5a19-8a93-489d-a2a0-98cae31f9529 b1f149deee984fcea8768c17076b1d7f ... Provisional None 2026-04-24T16:06:17.732760+00:00
geometry id time_series_id ... approval_status qualifiers last_modified
0 POINT (-80.85306 35.16444) 3373c03d-ef10-41b9-be8f-afcd1511dc27 27d30d4d1a4749bb887d1435da9fd278 ... Provisional None 2026-05-01T15:47:32.759180+00:00
1 POINT (-80.85306 35.16444) 48ba1d22-3278-449f-b767-a08f3ccffb2c b1f149deee984fcea8768c17076b1d7f ... Provisional None 2026-05-01T15:52:15.484900+00:00
2 POINT (-80.85306 35.16444) c387f069-f5f4-4954-8810-38b1ad3ba1ab 6ec29c85c72246ea83912c6f02f6fd63 ... Provisional None 2026-05-01T15:52:20.586492+00:00

[3 rows x 12 columns]
Index(['geometry', 'id', 'time_series_id', 'monitoring_location_id',
'parameter_code', 'statistic_id', 'time', 'value', 'unit_of_measure',
'approval_status', 'qualifier', 'last_modified'],
Index(['geometry', 'id', 'time_series_id', 'usgs_site_code', 'parameter_code',
'statistic_id', 'value_time', 'value', 'measurement_unit',
'approval_status', 'qualifiers', 'last_modified'],
dtype='str')
```
3 changes: 2 additions & 1 deletion python/waterdata_client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ dependencies = [
"platformdirs",
"jsonref",
"diskcache",
"PyYAML"
"PyYAML",
"geopandas"
]
dynamic = ["version"]

Expand Down
5 changes: 3 additions & 2 deletions python/waterdata_client/scripts/build_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
from datetime import datetime, UTC
import click
from jinja2 import Environment, FileSystemLoader
from schema_extract import get_template_data

from hydrotools.waterdata_client.schema import get_schema
from hydrotools.waterdata_client.client_config import SETTINGS
from hydrotools.waterdata_client._version import __version__
from schema_extract import get_template_data

@click.command()
@click.argument("templates", type=click.Path(exists=True, file_okay=False,
Expand Down Expand Up @@ -88,7 +89,7 @@ def write_clients_module(
lstrip_blocks=True
)

# Render and save
# Render and save clients.py
template = env.get_template(name=name)
content = template.render(
timestamp=timestamp,
Expand Down
1 change: 0 additions & 1 deletion python/waterdata_client/scripts/schema_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import re
from typing import Any, Optional, TypedDict
import builtins
from hydrotools.waterdata_client._version import __version__

OPENAPI_TO_PYTHON_TYPES: dict[str, str] = {
"string": "str",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
clients. It handles configuration state and the low-level request pipeline.
"""
import ssl
from typing import Any, Optional, Sequence
from typing import Any, Optional, Sequence, Generic
from functools import partial

from yarl import URL
Expand All @@ -19,8 +19,9 @@
build_request_batch_from_feature_ids,
QueryType
)
from .transformers import TransformedResponseT_co, ResponseTransformer, check_features

class BaseClient:
class BaseClient(Generic[TransformedResponseT_co]):
"""Base class for USGS OGC API clients. Specific child classes may overwrite
private attributes: _server, _api, _endpoint, _path, _content_type,
_max_pages.
Expand All @@ -31,6 +32,9 @@ class BaseClient:
max_retries: Number of times to attempt a failed request.
timeout_seconds: Total request timeout in seconds.
ssl_context: Custom SSL context for requests.
transformer: Callable object that takes a list[dict[str, Any]] and returns
a transformed result. Defaults to no transformation with validation
that some data were returned. Set to None to omit validation.
"""
_server: URL = SETTINGS.usgs_base_url
_api: OGCAPI = SETTINGS.default_api
Expand All @@ -44,12 +48,14 @@ def __init__(
concurrency_limit: int = SETTINGS.default_concurrency,
max_retries: int = SETTINGS.default_retries,
timeout_seconds: int = SETTINGS.timeout_seconds,
ssl_context: Optional[ssl.SSLContext] = None
ssl_context: Optional[ssl.SSLContext] = None,
transformer: Optional[ResponseTransformer[TransformedResponseT_co]] = check_features
) -> None:
self.concurrency_limit = concurrency_limit
self.max_retries = max_retries
self.timeout_seconds = timeout_seconds
self.ssl_context = ssl_context
self.transformer = transformer

# Setup request builder
self._builder = partial(
Expand Down Expand Up @@ -174,3 +180,20 @@ def _get_next_url(self, response: dict[str, Any]) -> URL:
if link.get("rel") == "next":
return URL(link["href"])
raise KeyError("response does not contain 'next' link")

def _handle_response(
self,
data: list[dict[str, Any]]
) -> TransformedResponseT_co | list[dict[str, Any]]:
"""Handle JSON response and optionally transform.

Args:
data: A list of deserialized GeoJSON responses from an OGC-compliant API.

Returns:
If self.transformer is None, returns untransformed data, else returns
transformed responses.
"""
if self.transformer is None:
return data
return self.transformer(data)
Loading