Skip to content

Commit 1adf174

Browse files
thodson-usgsclaude
andauthored
fix(waterdata): resolve naive datetime filters per-date, not at today's offset (#307)
_format_api_dates froze `datetime.now().astimezone().tzinfo` (today's UTC offset) and applied it to every naive datetime input. A naive input on a date whose DST status differs from today was shifted by an hour: on an EDT (-0400) machine, time="2020-01-01 12:00:00" produced 2020-01-01T16:00:00Z, but January is EST (-0500) so the correct value is 17:00:00Z. This skewed the query window for get_continuous / get_field_measurements (time/begin/end) across DST boundaries. Use `parsed.astimezone()` to interpret a naive input in the system local zone with the DST rules for its own date, then convert to UTC. Inputs that already carry an explicit offset are unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 665383f commit 1adf174

1 file changed

Lines changed: 8 additions & 8 deletions

File tree

dataretrieval/waterdata/utils.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def _parse_datetime(value: str) -> datetime | None:
233233
return None
234234

235235

236-
def _format_one(dt, *, date: bool, local_tz) -> str | None:
236+
def _format_one(dt, *, date: bool) -> str | None:
237237
"""Format a single datetime element for inclusion in the API time arg."""
238238
if pd.isna(dt) or dt == "" or dt is None:
239239
return ".."
@@ -242,7 +242,11 @@ def _format_one(dt, *, date: bool, local_tz) -> str | None:
242242
return None
243243
if date:
244244
return parsed.strftime("%Y-%m-%d")
245-
aware = parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=local_tz)
245+
# Naive inputs are interpreted in the system local zone (for backwards
246+
# compatibility). Use ``.astimezone()`` rather than a fixed offset so each
247+
# value is resolved against the DST rules for ITS OWN date — a frozen
248+
# ``datetime.now()`` offset shifted off-season inputs by an hour.
249+
aware = parsed if parsed.tzinfo is not None else parsed.astimezone()
246250
return aware.astimezone(ZoneInfo("UTC")).strftime("%Y-%m-%dT%H:%M:%SZ")
247251

248252

@@ -327,12 +331,8 @@ def _format_api_dates(
327331
return single
328332

329333
# Half-bounded ranges: NA endpoints render as ".."; any unparseable non-NA
330-
# element invalidates the range. Resolve the local tz only now — after the
331-
# all-NA / duration / interval guards above have had their chance to return.
332-
local_timezone = datetime.now().astimezone().tzinfo
333-
formatted = [
334-
_format_one(dt, date=date, local_tz=local_timezone) for dt in datetime_input
335-
]
334+
# element invalidates the range.
335+
formatted = [_format_one(dt, date=date) for dt in datetime_input]
336336
if any(f is None for f in formatted):
337337
return None
338338
return "/".join(formatted)

0 commit comments

Comments
 (0)