Skip to content

Commit ffe2059

Browse files
thodson-usgsclaude
andcommitted
chore!: require Python >=3.10; remove deprecated NWIS_Metadata.variable_info
Ahead of the breaking 1.2.0 release. Require Python >=3.10 - 3.9 support was already effectively broken: `waterdata`'s `anyio` dependency and the test stack (`pytest-httpx`) require 3.10+, and the `waterdata` test modules already skip on <3.10 — so the CI 3.9 leg was a hollow shell. - `requires-python = ">=3.10"`; ruff `target-version = "py310"`; mypy `python_version = "3.10"`. Bumping mypy to 3.10 also lets it parse `anyio`'s 3.10 source (the `mypy<2` pin existed only to keep targeting 3.9, now dropped). - Declare `anyio>=4.0` as a direct dependency — it's imported directly by `waterdata` (`start_blocking_portal`), previously relied on transitively via httpx. - CI test matrix `["3.9", "3.13", "3.14"]` -> `["3.10", "3.13", "3.14"]`. - Fill in the Trove classifiers (per-version 3.10-3.14, dev-status, audience, topic). - The py310 ruff target enables `B905`; add explicit `strict=False` to the four pre-existing `zip()` calls (`nwis`, `waterdata/chunking`, `waterdata/nearest`) — identical to the prior implicit behavior. Remove deprecated `NWIS_Metadata.variable_info` - It only emitted a `DeprecationWarning` and returned `None` (it relied on the defunct `get_pmcodes`). Accessing it now raises `NotImplementedError` via the `utils.BaseMetadata` abstract contract. Drops the obsolete unit test. BREAKING CHANGE: Python 3.9 is no longer supported; `pip install dataretrieval>=1.2.0` requires Python >=3.10. `NWIS_Metadata.variable_info` is removed (it always returned None). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sjb14HkwuCydKSKMsaXsgd
1 parent dff162c commit ffe2059

7 files changed

Lines changed: 29 additions & 36 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
fail-fast: false
5050
matrix:
5151
os: [ubuntu-latest, windows-latest]
52-
python-version: ["3.9", "3.13", "3.14"]
52+
python-version: ["3.10", "3.13", "3.14"]
5353

5454
steps:
5555
- uses: actions/checkout@v6

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**06/23/2026:** **Breaking change (1.2.0):** the minimum supported Python is now **3.10** (`requires-python = ">=3.10"`). 3.9 support was already effectively broken — the `waterdata` module's dependencies (`anyio`, the test stack) require 3.10+, and the `waterdata` test modules already skipped on <3.10. `anyio` is now declared as a direct dependency (it is imported directly by `waterdata`), and the CI/ruff/mypy targets move to 3.10. Also removed the deprecated `NWIS_Metadata.variable_info` property (it only warned and returned `None`, relying on the defunct `get_pmcodes`); accessing it now raises `NotImplementedError` via the base class.
2+
13
**06/03/2026:** The request-error hierarchy is now unified. Every module (`nwis`, `wqp`, `nldi`, `waterdata`, `nadp`, `streamstats`) raises a subclass of `dataretrieval.DataRetrievalError` on a failed request, so a single `except dataretrieval.DataRetrievalError` spans them all. An HTTP error status surfaces as an `HTTPError` carrying `.status_code` (inspect it to branch on a specific code); the retryable 429/5xx subset is `TransientError` (`RateLimited` / `ServiceUnavailable`, carrying `.retry_after`); and a request too large to satisfy is a `RequestTooLarge` (`URLTooLong` for an over-long single request, `Unchunkable` when the Water Data chunker cannot split a call small enough). Connection-level failures (timeouts, DNS, refused connections) are wrapped as a `NetworkError`, with the underlying `httpx` exception on `__cause__`. Every `DataRetrievalError` also exposes `.status_code` (`None` when there is no HTTP status), `.retry_after`, and `.retryable`, so a single `except dataretrieval.DataRetrievalError as e` clause can branch on the status or retry transient failures without knowing the concrete subclass. **Breaking change:** these exceptions no longer multiply-inherit a built-in — code that caught request failures with `except ValueError` or `except RuntimeError` should switch to `except dataretrieval.DataRetrievalError` (or a specific subclass). A no-data result is **not** an error: the modern getters (`waterdata`, `wqp`, `nldi`) return an empty DataFrame when nothing matches. Only the deprecated `nwis` (waterservices) path still raises `NoSitesError` on no data.
24

35
**05/17/2026:** The OGC `waterdata` getters (`get_daily`, `get_continuous`, `get_field_measurements`, and the rest of the multi-value-capable functions) now transparently chunk requests whose URLs would otherwise exceed the server's ~8 KB byte limit.

dataretrieval/nwis.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,11 +1033,15 @@ def _read_json(json: dict[str, Any]) -> pd.DataFrame:
10331033
# for example, [0, 21, 22] would be the first and last indices
10341034
index_list = [0]
10351035
index_list.extend(
1036-
[i + 1 for i, (a, b) in enumerate(zip(site_list[:-1], site_list[1:])) if a != b]
1036+
[
1037+
i + 1
1038+
for i, (a, b) in enumerate(zip(site_list[:-1], site_list[1:], strict=False))
1039+
if a != b
1040+
]
10371041
)
10381042
index_list.append(len(site_list))
10391043

1040-
for start, end in zip(index_list[:-1], index_list[1:]):
1044+
for start, end in zip(index_list[:-1], index_list[1:], strict=False):
10411045
# grab a block containing timeseries 0:21,
10421046
# which are all from the same site
10431047
site_block = json["value"]["timeSeries"][start:end]
@@ -1133,8 +1137,8 @@ class NWIS_Metadata(BaseMetadata):
11331137
11341138
Notes
11351139
-----
1136-
``site_info`` and ``variable_info`` are exposed as properties (documented
1137-
below) rather than plain attributes.
1140+
``site_info`` is exposed as a property (documented below) rather than a
1141+
plain attribute.
11381142
11391143
"""
11401144

@@ -1196,17 +1200,3 @@ def site_info(self) -> tuple[pd.DataFrame, BaseMetadata] | None:
11961200

11971201
else:
11981202
return None # don't set metadata site_info attribute
1199-
1200-
@property
1201-
def variable_info(self) -> None:
1202-
"""
1203-
Deprecated. Accessing variable_info via NWIS_Metadata is deprecated.
1204-
Returns None.
1205-
"""
1206-
warnings.warn(
1207-
"Accessing variable_info via NWIS_Metadata is deprecated as "
1208-
"it relies on the defunct get_pmcodes function.",
1209-
DeprecationWarning,
1210-
stacklevel=2,
1211-
)
1212-
return None

dataretrieval/waterdata/chunking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ def iter_sub_args(self) -> Iterator[dict[str, Any]]:
976976
chunk_lists = [self.chunks[ax.arg_key] for ax in self.axes]
977977
for combo in itertools.product(*chunk_lists):
978978
sub_args = dict(self.args)
979-
for axis, chunk in zip(self.axes, combo):
979+
for axis, chunk in zip(self.axes, combo, strict=False):
980980
sub_args[axis.arg_key] = axis.render(chunk)
981981
yield sub_args
982982

dataretrieval/waterdata/nearest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ def _build_window_or_filter(targets: pd.DatetimeIndex, window_td: pd.Timedelta)
197197
lowers = (targets - window_td).strftime(fmt)
198198
uppers = (targets + window_td).strftime(fmt)
199199
return " OR ".join(
200-
f"(time >= '{lo}' AND time <= '{up}')" for lo, up in zip(lowers, uppers)
200+
f"(time >= '{lo}' AND time <= '{up}')"
201+
for lo, up in zip(lowers, uppers, strict=False)
201202
)
202203

203204

pyproject.toml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66
name = "dataretrieval"
77
description = "Discover and retrieve water data from U.S. federal hydrologic web services."
88
readme = "README.md"
9-
requires-python = ">=3.9"
9+
requires-python = ">=3.10"
1010
keywords = ["USGS", "water data"]
1111
license = "CC0-1.0"
1212
license-files = ["LICENSE.md"]
@@ -17,11 +17,22 @@ maintainers = [
1717
{name = "Elise Hinman", email = "ehinman@usgs.gov"},
1818
]
1919
classifiers = [
20+
"Development Status :: 5 - Production/Stable",
21+
"Intended Audience :: Science/Research",
22+
"Topic :: Scientific/Engineering :: Hydrology",
2023
"Programming Language :: Python :: 3",
24+
"Programming Language :: Python :: 3.10",
25+
"Programming Language :: Python :: 3.11",
26+
"Programming Language :: Python :: 3.12",
27+
"Programming Language :: Python :: 3.13",
28+
"Programming Language :: Python :: 3.14",
2129
]
2230
dependencies = [
2331
"httpx",
2432
"pandas>=2.0.0,<4.0.0",
33+
# Directly imported by ``waterdata`` (``anyio.from_thread.start_blocking_portal``),
34+
# so declared here rather than relied on transitively via httpx.
35+
"anyio>=4.0",
2536
]
2637
dynamic = ["version"]
2738

@@ -35,7 +46,7 @@ dataretrieval = ["py.typed"]
3546
# Minimal set the CI ``type-check`` job installs — just mypy + the package,
3647
# not the whole test stack.
3748
type-check = [
38-
"mypy<2", # <2 so it can still target Python 3.9 (the project's floor)
49+
"mypy",
3950
]
4051
test = [
4152
"pytest > 5.0.0",
@@ -77,7 +88,7 @@ repository = "https://github.com/DOI-USGS/dataretrieval-python.git"
7788
write_to = "dataretrieval/_version.py"
7889

7990
[tool.ruff]
80-
target-version = "py39"
91+
target-version = "py310"
8192
extend-exclude = ["demos"]
8293

8394
[tool.ruff.lint]
@@ -115,7 +126,7 @@ docstring-code-line-length = 72
115126
# libraries (pandas, geopandas, anyio) are treated as ``Any`` instead of
116127
# requiring stub packages. Dropping that — via pandas-stubs/types-requests and
117128
# per-module overrides — can follow.
118-
python_version = "3.9" # the project's minimum supported version
129+
python_version = "3.10" # the project's minimum supported version
119130
files = ["dataretrieval"]
120131
strict = true
121132
ignore_missing_imports = true

tests/nwis_test.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -405,17 +405,6 @@ def test_set_metadata_info_countyCd(self):
405405
# assert that site_info is implemented
406406
assert md.site_info
407407

408-
def test_variable_info_deprecated(self):
409-
"""Test that variable_info raises a DeprecationWarning and returns None."""
410-
response = mock.MagicMock()
411-
md = NWIS_Metadata(response)
412-
with pytest.warns(
413-
DeprecationWarning,
414-
match="Accessing variable_info via NWIS_Metadata is deprecated",
415-
):
416-
result = md.variable_info
417-
assert result is None
418-
419408

420409
class TestReadRdb:
421410
"""Tests for the NWIS-specific _read_rdb wrapper.

0 commit comments

Comments
 (0)