Skip to content

Commit 75e56ab

Browse files
thodson-usgsclaude
andauthored
lint(ruff): select the checks no other gate covers, and pin the linter (#398)
* fix(waterdata): write downloaded ratings as UTF-8 `get_ratings(..., file_path=...)` wrote each rating with `open(..., "w")` and no encoding, so the bytes on disk depended on the writing machine's locale rather than on what the service sent. On a non-UTF-8 locale an unrepresentable character raises `UnicodeEncodeError`, which is a `ValueError`, which `_download_all`'s per-feature handler downgrades to a `SkippedRatingWarning` -- so the rating disappears from the returned dict rather than failing loudly, which is the opposite of what the function's own docstring promises. `newline=""` disables the translation that would otherwise rewrite the RDB's line endings on Windows. The regression test asserts the saved file equals the response body byte for byte, using a character absent from cp1252, so it fails on the Windows leg without the fix and cannot fail on Linux or macOS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * fix(waterdata): move the ratings write off the event loop The same `open()` was a blocking call inside `async def _fetch_rating`, which runs dozens-concurrent under a fan-out drive -- so every rating write stalled every other in-flight download for its duration. The write is now a three-line `_write_rating` dispatched through `anyio.to_thread.run_sync`. Nothing in the suite would have caught this: the tests await mocked transports, so a stalled loop still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * fix(nwis): stop setting the index on the caller's frame in place `format_response` set the index with `inplace=True` on the frame it was handed while already returning a new object -- and already left the caller's frame untouched on the `peaks` and GeoDataFrame paths, so the in-place branch was the odd one out. Callers using the return value, which is the only documented use, see no difference. `pandas` is also phasing `inplace=` out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * fix(nwis): warn for the get_record options whose services are defunct `get_record`'s `wide_format`, `datetime_index` and `state` have read nothing since `qwdata`, `gwlevels` and `water_use` were retired -- their only readers went with those branches -- but all three stayed in the signature and in the published docstring. `get_record(sites=..., state="OH")` therefore reads as a state filter while doing nothing at all. Verified against the tree before `491eb5c3` ("remove usage of qwdata"), where `get_record` passed `wide_format` and `datetime_index` to `get_qwdata`, `datetime_index` to `get_gwlevels`, and `state` to `get_water_use` -- the four readers (`if wide_format:`, two `if datetime_index is True:`, and `if state is not None:`) that went with those functions. Nothing in the package reads any of the three today. They now advise through `_deprecation.warn_deprecated`, naming a live replacement each, and are still accepted and still ignored. Naming one at its declared default is silent: the caller is asking for exactly what the dead default already gave them, so only a value the option cannot honour is worth a warning. A test pins that, and with it the agreement between the table's "unset" value and `get_record`'s declared default that the distinction rests on. They are not raised on and not deleted: - A defunct *service* cannot return the data asked for; `get_record( service="dv", wide_format=False)` returns exactly the right data, and only the knob is dead. Raising would retire a documented parameter of a Production/Stable getter ~20 months before `REMOVALS["nwis"]` with no warned release in between. - Deleting `state` from the signature would let it fall through `**kwargs` to `query_waterservices` and onto the wire as a parameter NWIS does not define. Confirmed against the live code path: an unknown *string* kwarg reaches the query string, while an unknown *bool* is dropped, because `to_str` returns `None` for a non-iterable scalar. So the argument for keeping the parameter holds for `state` alone. A call naming all three emits four `DeprecationWarning`s: one from `@_deprecated` plus one per option. They are not duplicates -- each names a distinct subject and a distinct replacement -- but `_warn_defunct_record_options` raises through `warn_deprecated` directly and so does not pass through `_deprecated`'s re-entrancy guard. The count is pinned by a test, as is the attribution the hand-counted `stacklevel=4` encodes. The replacement tripwire is now derived from the deprecation tables themselves rather than from a hand-kept list, and checks the keywords a message names as well as the function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * lint(ruff): fix what the checks in the next commit found Everything the newly selected rules reported that is not a bug in its own right, kept apart from the config change so the config commit is only config: - `D100`: `dataretrieval.codes` and `dataretrieval.waterdata.types` are rendered by `automodule ... :members:`, so a missing module docstring ships to the docs site as a bare signature. `setup.py` and `docs/source/conf.py` get one-liners. - `ARG001`: `_display_api_key` and `_display_progress` never read `adapter`; it is there because the display registry calls every renderer with the same signature, so the name is prefixed rather than removed. - `RUF100`: four `# noqa: BLE001` directives sat on handlers that re-raise and never needed one. Four more carried their reason after a dash or in parentheses -- ruff reads the rest of the line as the directive's description and would delete a co-located `# pylint: disable=` with it -- so they are respelled with a second `#`. - `PLW1514`: the test helpers that read fixture files now name an encoding. - `PD002`: `_parse_parameter_record` returns the renamed frame rather than renaming in place; the frame is local, so nothing outside changes. - `PD003`: `isnull` -> `isna`. - `DTZ007`: `ogc/dates.py`'s `_parse_datetime` is correct by design -- `_DATETIME_FORMATS` deliberately carries both the `%z` and the bare forms, and the docstring promises tz-awareness only for an input that carried an offset. It takes a directive naming the reason rather than a "fix". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * lint(ruff): select the checks no other gate covers, and pin the linter Config only; the findings are fixed in the commits before this one. ## The rules - `PLW1514` -- mypy does not model encodings, and a locale-dependent write misbehaves only on the Windows leg, so a green Linux run proves nothing. Found the silent rating loss. - `ARG001` -- notices when retiring a legacy getter strands a documented keyword. Found the three inert `get_record` options. - `ASYNC` -- the suite awaits mocked transports, so a stalled event loop still passes. Found the blocking ratings write. - `D100`-`D104` -- `automodule ... :members:` renders a docstring-less public symbol as a bare signature. - `RUF100` -- nothing was watching the suppressions. Six `# noqa: BLE001` directives sat in the tree suppressing a rule nobody had selected. - `BLE001` -- a blind `except Exception` skips ADR 0004's transient-versus- fatal judgement. Already half-adopted, via those six directives. - `PD`, `DTZ` -- `inplace=` is on pandas' way out, in a pandas library; a naive `datetime` in a library about time series. - `C4` `LOG` `G` `T20` `FA` `NPY` -- free ratchets at today's setting. The suppression budget goes down, not up: four directives removed, one added. ## The flow - The version was pinned in three places and honoured in none: the pre-commit rev, CI and the `test` extra all said 0.16.1, while the machine that wrote this had 0.16.5 on `$PATH` and 0.15.12 in its venv -- `ruff format --check` disagreed by 28 files between them. It now lives in two: a `[lint]` extra that CI installs the way the `[metrics]` job already installs its own, and the `ruff-pre-commit` rev. The `test` extra reuses `dataretrieval[lint]`, as it already reuses `dataretrieval[type-check]`. - Pinned at 0.16.5, the current release. - `preview = true` enabled 52 rules to get one. `explicit-preview-rules` keeps the rest off: that count was 50 at 0.16.1 and is 52 at 0.16.5, four patch releases apart, and each would have arrived as a CI failure nobody selected. - `E501` was selected redundantly (already inside `E`) under a comment naming a `line-length` setting that did not exist. The value is now set explicitly, where it visibly governs both the checker and the formatter. Unrelated but found the same way: the mypy `anyio` override is dropped. Its rationale was a 3.9 target that a `match` statement would fail to parse, and `python_version` has been 3.10 since 1.2.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * docs(news): record the two ratings fixes The `nwis.get_record` option deprecations and the `format_response` change are not called out: `nwis` is deprecated itself, and the entry covers interface changes and bug fixes to active modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6MVcko4gh68LieGWxUnrR * perf(nwis): keep the caller's frame without duplicating it Dropping `inplace=` from `format_response`'s `set_index` stopped the function from mutating its argument, but it also made the function copy the data. pandas resolves a non-inplace `set_index` to `self.copy(deep=None)`, and `BlockManager.copy` turns `deep=None` into a full deep copy whenever copy-on-write is off -- the default for all of pandas 2.x, which is what a Python 3.10 install gets, since pandas 3.0 requires 3.11. A shallow copy carries the same guarantee: the index lands on our frame and the columns stay shared. `set_index` then runs inplace on that copy, which is what the `noqa` records. The rest of the gap is `_localize_datetime_index`. `DataFrame.tz_localize` relabels one axis by duplicating every column, a copy main paid too. Retagging the index alone is equivalent -- asserted frame-equal against the old call on both the multi-index and single-index branches -- and costs nothing. Peak RSS for one call, measured in a fresh process on pandas 2.2.3: main inplace dropped here 2M rows, 1 site 108.0 123.3 46.2 MiB 2M rows, 8 sites 250.0 394.6 318.3 MiB 1M rows, 20 cols, 8 sites 389.2 456.6 288.7 MiB The narrow multi-site frame stays above main. Building a two-level MultiIndex while the source columns stay materialized is the price of leaving the caller's frame intact; only mutating the argument avoids it. The other two shapes fall below main because the tz_localize copy is gone. CPU improves alongside: 355 -> 319 ms on pandas 2.2.3 and 227 -> 215 ms on 3.0.5, for 2M rows across 8 sites. pandas 3.0.5 memory is unchanged throughout -- copy-on-write already made both copies shallow, which is why the suite cannot see any of this: CI runs 3.13 and 3.14. So the test asserts the property that does hold everywhere -- the argument keeps its columns and its index -- which fails against the in-place version on both branches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVhHGSDkVxWzeUMmHn6oAT * fix(nwis): stop the peaks path writing into the caller's frame A review of the previous commit found that the invariant its test states is broader than what the code delivers. `preformat_peaks_response` derives `datetime` from `peak_dt` with a plain assignment, so `format_response(df, service="peaks")` still added a column to the frame it was handed: the shallow copy the previous commit introduced sits downstream of that call. `preformat_peaks_response` is public in its own right, so a caller reaches the mutation without going through `format_response` at all. It now derives the column on its own frame, and the test class covers the peaks arm alongside the plain ones. That also settles a claim in e773171, which said the function already left the caller's frame untouched on the peaks and GeoDataFrame paths. It held for GeoDataFrame only. It holds for both now. `_localize_datetime_index` loses its frame wrapper. Its one caller hands it a frame it has just shallow-copied, so the second copy -- and the identity guard that existed to avoid it -- protected a caller that no longer exists. As `_localized_datetime_index` it takes an index and returns one, and the caller assigns. Frame-equal to the previous form on pandas 2.2.3 and 3.0.5 across single-site, multi-site, already-aware and no-datetime frames; peak RSS is unchanged at 46.2 / 317.3 / 288.6 MiB on the three shapes the previous commit measured. The `datetime_index` entry told a caller to reach for `waterdata.get_continuous` or `get_daily` "which index by datetime". Neither does: no getter under `waterdata/` sets an index, and the suite pins `time` as an ordinary column. The recommendation is right, the reason given for it was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVhHGSDkVxWzeUMmHn6oAT * fix(merge): restore what main brought to tests/nwis_test.py The merge resolved that file with ``git checkout --ours``, which takes the whole ours-side blob rather than the one conflicted region, so everything main brought to the file was reverted: - ``TestReadRdb::test_malformed_peaks_frame_still_raises``, the regression test from #344 -- the suite went 1154 -> 1153 and nothing flagged it, since a missing test cannot fail. - 36 lines of copy edits from #417 and #418, the passes that replaced figurative wording with literal ("has no opinion about it" -> "does not record it", "borrowing" -> "importing"). Redone as a three-way merge of the file with only the conflicted region resolved by hand. That region is main's copy edit of ``test_named_replacement_exists_in_waterdata``, a test this branch had already replaced with the stronger ``test_named_replacement_resolves`` (ARG001: the old one never read its ``name`` parameter). The replacement stands, with main's edits carried into its docstring -- the "Tripwire:" prefix and the contraction dropped, matching the pass that removed the only other instance of each in ``tests/``. * docs: clarify deprecation guidance and review terminology --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0d345a7 commit 75e56ab

24 files changed

Lines changed: 391 additions & 99 deletions

‎.github/workflows/python-package.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ jobs:
2020
python-version: "3.14"
2121
cache: "pip"
2222
- name: Install ruff
23-
# Keep this version aligned with the ruff-pre-commit revision.
24-
run: pip install ruff==0.16.1
23+
run: pip install -e .[lint]
2524
- name: Lint with ruff
2625
run: |
2726
ruff check . --output-format=github

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ repos:
3030
exclude: ^tests/data/
3131

3232
- repo: https://github.com/astral-sh/ruff-pre-commit
33-
rev: v0.16.1
33+
rev: v0.16.5
3434
hooks:
3535
- id: ruff-check
3636
args: [--fix]

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ can predict where a thing is defined.
7070
- Python >= 3.10; the CI test matrix is 3.10, 3.13, 3.14.
7171

7272
## Commands
73-
- Lint: `ruff check .` and `ruff format --check .` (pinned to the version in
74-
`.pre-commit-config.yaml` and the CI lint job — keep them aligned).
73+
- Lint: `ruff check .` and `ruff format --check .`
74+
(`pip install -e '.[lint]'` installs the pinned version;
75+
keep the `.pre-commit-config.yaml` revision aligned with it).
7576
- Tests: `coverage run -m pytest tests/ && coverage report`, or focused like
7677
`pytest tests/waterdata_test.py::test_mock_get_samples`. `coverage report` is
7778
a merge gate: branch coverage with a `fail_under` ratchet in

‎NEWS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
**09/01/2026:** **Announcement:** We at USGS Water Data for the Nation want your feedback! Tell us how we're doing by taking our quick [survey](https://usgswaterresources.gov1.qualtrics.com/jfe/form/SV_07gX8G1DeOtVrH8), available through September 2026.
44

5+
**08/30/2026:** **Bug fix:** `waterdata.get_ratings(..., file_path=...)` wrote each rating with no explicit encoding, so on a non-UTF-8 locale an unrepresentable character raised `UnicodeEncodeError` and the rating was silently dropped from the returned dict; ratings are now written as UTF-8, byte for byte with the response. **Bug fix:** that same write blocked the event loop, so on a multi-location request every other in-flight download stalled for its duration; it now runs in a worker thread.
6+
57
**08/27/2026:** **Bug fix:** `nwis.get_discharge_peaks` and `nwis.get_record(service='peaks')` discarded every peak whose date is only partly known. NWIS zero-fills the unknown part of a historical peak's date -- `YYYY-MM-00` when the day is not known, `YYYY-00-00` when the month is not either (the `Bd` and `Bm` `peak_cd` qualifiers) -- and neither parses as a date, so `preformat_peaks_response` coerced both to `NaT` and then dropped the row along with its discharge value. These are real peaks, and disproportionately a site's largest: site 14105700 lost 20 of its 167 peaks, among them an 1859 flood of 847,000 ft3/s, and 06934500 lost its 1844 peak of 700,000 ft3/s. Such a peak is now kept, with `datetime` left as `NaT`. The date is not completed into one NWIS does not have: a `datetime64` column cannot hold a partial date, so any value there would assert a day the record does not contain. **Behavior change:** peaks queries return more rows than before, and `datetime` may now be `NaT` -- a caller selecting on the datetime index will not see those peaks and should filter on `peak_dt` instead. **Behavior change:** `peak_dt` is no longer removed from the returned frame. It is the only column that holds a censored peak's year, since the peaks response has no `water_yr`, and the only dependable way to tell an unknown day from a known one -- `peak_cd` does not always include the qualifier (22 of 24 censored dates across six sites tested). For peaks with a resolved timestamp alongside explicit `year`/`month`/`day` and a `qualifier` field, use `waterdata.get_peaks()`.
68

79
**08/26/2026:** **Bug fix:** `nwis.format_response(df, service='peaks')` and `nwis.preformat_peaks_response` raised `KeyError('peak_dt')` on an empty peaks response instead of returning an empty frame. Both are public, and every other service already treated an empty result as a legitimate empty frame rather than an error (issue #171); the peaks branch was missed because it reformats the datetime column before the empty-frame check. Callers can now check `df.empty` rather than catching an exception. A *non-empty* frame with no `peak_dt` column is malformed rather than empty, and still raises.

‎dataretrieval/codes/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
"""Facade over the ``states`` and ``timezones`` lookup tables.
2+
3+
Re-exports the state code maps, their normalizers (``to_state``,
4+
``apply_state``), and the ``tz`` UTC-offset map.
5+
"""
6+
17
from .states import *
28
from .timezones import *

‎dataretrieval/configuration.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ def _accepts(adapter: str, name: str) -> bool:
804804
return name in _ALL_SETTINGS if accepted is None else name in accepted
805805

806806

807-
def _display_api_key(adapter: str | None = None) -> str:
807+
def _display_api_key(_adapter: str | None = None) -> str:
808808
"""Render the key's presence, never its value."""
809809
return "<set>" if api_key() else "<not set>"
810810

@@ -814,7 +814,7 @@ def _display_concurrency(adapter: str | None = None) -> str:
814814
return CONCURRENCY_UNBOUNDED if value is None else str(value)
815815

816816

817-
def _display_progress(adapter: str | None = None) -> str:
817+
def _display_progress(_adapter: str | None = None) -> str:
818818
setting = progress()
819819
return "auto" if setting is None else ("on" if setting else "off")
820820

‎dataretrieval/nwis.py‎

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,15 @@ def _parse_json_or_raise(response: httpx.Response) -> pd.DataFrame:
148148
raise
149149

150150

151-
def _localize_datetime_index(df: pd.DataFrame) -> pd.DataFrame:
151+
def _localized_datetime_index(index: pd.Index) -> pd.Index:
152152
"""Localize a naive datetime index (or multi-index level) to UTC."""
153-
if hasattr(df.index, "levels"):
153+
if hasattr(index, "levels"):
154154
# Multi-index: localize the datetime level (level 1)
155-
if hasattr(df.index.levels[1], "tzinfo") and df.index.levels[1].tzinfo is None:
156-
df = df.tz_localize("UTC", level=1)
157-
elif hasattr(df.index, "tzinfo") and df.index.tzinfo is None:
158-
df = df.tz_localize("UTC")
159-
return df
155+
if hasattr(index.levels[1], "tzinfo") and index.levels[1].tzinfo is None:
156+
return index.set_levels(index.levels[1].tz_localize("UTC"), level=1)
157+
elif hasattr(index, "tzinfo") and index.tzinfo is None:
158+
return index.tz_localize("UTC")
159+
return index
160160

161161

162162
def format_response(
@@ -198,11 +198,19 @@ def format_response(
198198
return df
199199

200200
if len(df["site_no"].unique()) > 1 and mi:
201-
df.set_index(["site_no", "datetime"], inplace=True)
201+
keys = ["site_no", "datetime"]
202202
else:
203-
df.set_index(["datetime"], inplace=True)
203+
keys = ["datetime"]
204+
205+
# Index our own frame, never the caller's. The shallow copy shares the
206+
# columns; ``set_index`` without ``inplace`` duplicates them, because
207+
# pandas deep-copies the frame whenever copy-on-write is off.
208+
df = df.copy(deep=False)
209+
df.set_index(keys, inplace=True) # noqa: PD002 # our copy, not the caller's
204210

205-
df = _localize_datetime_index(df)
211+
# Retag the index alone; ``DataFrame.tz_localize`` relabels the axis by
212+
# duplicating every column whenever copy-on-write is off.
213+
df.index = _localized_datetime_index(df.index)
206214
return df.sort_index()
207215

208216

@@ -238,6 +246,8 @@ def preformat_peaks_response(df: pd.DataFrame) -> pd.DataFrame:
238246
# still raise.
239247
return df
240248

249+
# Derive the column on our own frame, never the caller's.
250+
df = df.copy(deep=False)
241251
df["datetime"] = pd.to_datetime(df["peak_dt"], errors="coerce")
242252
return df
243253

@@ -897,6 +907,44 @@ def what_sites(
897907
return df, NWIS_Metadata(response, **kwargs)
898908

899909

910+
# Each ignored ``get_record`` parameter maps to its declared default and
911+
# replacement getter guidance, not restrictions on the replacement getter.
912+
_DEFUNCT_RECORD_OPTIONS: dict[str, tuple[object, str]] = {
913+
"wide_format": (True, "`waterdata.get_samples()`"),
914+
"datetime_index": (
915+
True,
916+
"`waterdata.get_continuous()` or `waterdata.get_daily()`",
917+
),
918+
"state": (None, "`nwdc.get_wateruse(state=...)`"),
919+
}
920+
921+
922+
def _warn_defunct_record_options(**given: object) -> None:
923+
"""Warn when an ignored ``get_record`` parameter differs from its default.
924+
925+
``given`` must contain only names from ``_DEFUNCT_RECORD_OPTIONS``.
926+
Values equal to the declared defaults do not produce parameter warnings,
927+
whether passed explicitly or omitted by the caller of ``get_record``.
928+
Replacement names in the table identify getters for retrieving the data;
929+
they do not mean those getters accept the ignored parameters.
930+
"""
931+
for name, value in given.items():
932+
unset, replacement = _DEFUNCT_RECORD_OPTIONS[name]
933+
if value != unset:
934+
warn_deprecated(
935+
f"`nwis.get_record`'s `{name}` argument",
936+
replacement=replacement,
937+
removal=_NWIS_REMOVAL_DATE,
938+
detail=(
939+
"It is ignored, and has been since the service that read "
940+
"it was retired."
941+
),
942+
# _warn_defunct_record_options -> get_record -> @_deprecated
943+
# wrapper -> the caller's own line.
944+
stacklevel=4,
945+
)
946+
947+
900948
@_deprecated
901949
def get_record(
902950
sites: list[str] | str | None = None,
@@ -929,12 +977,25 @@ def get_record(
929977
If False, return a dataframe with a single-level index (datetime).
930978
Default is True.
931979
wide_format : bool, optional
932-
If True, return data in wide format, with multiple samples per row and
933-
one row per time. Default is True.
980+
(defunct) Previously shaped output from the retired 'qwdata' service.
981+
``get_record`` ignores this parameter; passing ``False`` emits a
982+
``DeprecationWarning``.
983+
To retrieve sample data, use ``waterdata.get_samples``,
984+
which returns one row per result and has no ``wide_format`` parameter.
934985
datetime_index : bool, optional
935-
If True, create a datetime index. Default is True.
986+
(defunct) Previously shaped output from the retired 'qwdata' and
987+
'gwlevels' services.
988+
``get_record`` ignores this parameter; passing ``False`` emits a
989+
``DeprecationWarning``.
990+
To retrieve time-series data, use ``waterdata.get_continuous`` or
991+
``waterdata.get_daily``, which return ``time`` as a column and have
992+
no ``datetime_index`` parameter.
936993
state: string, optional, default is None
937-
State full name, abbreviation, or id.
994+
(defunct) Previously selected sites for the retired 'water_use' service.
995+
``get_record`` ignores this parameter; passing a value other than
996+
``None`` emits a ``DeprecationWarning``.
997+
To retrieve water-use data by state, use ``nwdc.get_wateruse(state=...)``.
998+
Its ``state`` parameter is supported and filters the query.
938999
service: string, default is 'iv'
9391000
- 'iv' : instantaneous data
9401001
- 'dv' : daily mean data
@@ -1013,6 +1074,10 @@ def get_record(
10131074
),
10141075
)
10151076

1077+
_warn_defunct_record_options(
1078+
wide_format=wide_format, datetime_index=datetime_index, state=state
1079+
)
1080+
10161081
if service == "iv":
10171082
df, _ = get_iv(
10181083
sites=sites,
@@ -1099,15 +1164,13 @@ def _parse_parameter_record(
10991164
record_df["qualifiers"] = (
11001165
record_df["qualifiers"].astype(str).str.strip("[]").str.replace("'", "")
11011166
)
1102-
record_df.rename(
1167+
return record_df.rename(
11031168
columns={
11041169
"value": col_name,
11051170
"dateTime": "datetime",
11061171
"qualifiers": col_name + "_cd",
1107-
},
1108-
inplace=True,
1172+
}
11091173
)
1110-
return record_df
11111174

11121175

11131176
def _parse_site_block(site_block: list[dict[str, Any]]) -> pd.DataFrame:

‎dataretrieval/ogc/dates.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ def _parse_datetime(value: str) -> datetime | None:
5353
candidate = value[:-1] + "+00:00" if value.endswith("Z") else value
5454
for fmt in _DATETIME_FORMATS:
5555
try:
56-
return datetime.strptime(candidate, fmt)
56+
# DTZ007: naive is the documented outcome for a naive input --
57+
# ``_DATETIME_FORMATS`` carries both the ``%z`` and the bare forms.
58+
return datetime.strptime(candidate, fmt) # noqa: DTZ007
5759
except ValueError:
5860
continue
5961
return None

‎dataretrieval/transport/pagination.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def report_page(page: httpx.Response, frame: pd.DataFrame) -> None:
115115

116116
try:
117117
frame, cursor = parse_response(response)
118-
except Exception as exc: # noqa: BLE001
118+
except Exception as exc:
119119
logger.warning("Initial response parse failed.")
120120
raise DataRetrievalError(
121121
paginated_failure_message(0, exc, response.url)
@@ -140,7 +140,7 @@ def report_page(page: httpx.Response, frame: pd.DataFrame) -> None:
140140
nrows += len(frame)
141141
total_elapsed += _safe_elapsed(response)
142142
report_page(response, frame)
143-
except Exception as exc: # noqa: BLE001
143+
except Exception as exc:
144144
logger.warning(
145145
"Request failed at cursor %r. Data download interrupted.", cursor
146146
)

‎dataretrieval/transport/retry.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ async def attempt_once() -> _T:
289289
while True:
290290
try:
291291
return await attempt_once()
292-
except Exception as exc: # noqa: BLE001 - re-raised unless retryable
292+
except Exception as exc:
293293
attempt += 1
294294
wait = _retry_delay(exc, attempt, policy)
295295
if wait is None:
@@ -311,7 +311,7 @@ def retry_sync(fn: Callable[[], _T], policy: RetryPolicy | None = None) -> _T:
311311
while True:
312312
try:
313313
return fn()
314-
except Exception as exc: # noqa: BLE001 - re-raised unless retryable
314+
except Exception as exc:
315315
attempt += 1
316316
wait = _retry_delay(exc, attempt, policy)
317317
if wait is None:

0 commit comments

Comments
 (0)