Skip to content

Commit f559657

Browse files
thodson-usgsclaude
andcommitted
fix(waterdata): resolve naive datetime filters per-date, not at today's offset
_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 ee653e5 commit f559657

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
@@ -223,7 +223,7 @@ def _parse_datetime(value: str) -> datetime | None:
223223
return None
224224

225225

226-
def _format_one(dt, *, date: bool, local_tz) -> str | None:
226+
def _format_one(dt, *, date: bool) -> str | None:
227227
"""Format a single datetime element for inclusion in the API time arg."""
228228
if pd.isna(dt) or dt == "" or dt is None:
229229
return ".."
@@ -232,7 +232,11 @@ def _format_one(dt, *, date: bool, local_tz) -> str | None:
232232
return None
233233
if date:
234234
return parsed.strftime("%Y-%m-%d")
235-
aware = parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=local_tz)
235+
# Naive inputs are interpreted in the system local zone (for backwards
236+
# compatibility). Use ``.astimezone()`` rather than a fixed offset so each
237+
# value is resolved against the DST rules for ITS OWN date — a frozen
238+
# ``datetime.now()`` offset shifted off-season inputs by an hour.
239+
aware = parsed if parsed.tzinfo is not None else parsed.astimezone()
236240
return aware.astimezone(ZoneInfo("UTC")).strftime("%Y-%m-%dT%H:%M:%SZ")
237241

238242

@@ -317,12 +321,8 @@ def _format_api_dates(
317321
return single
318322

319323
# Half-bounded ranges: NA endpoints render as ".."; any unparseable non-NA
320-
# element invalidates the range. Resolve the local tz only now — after the
321-
# all-NA / duration / interval guards above have had their chance to return.
322-
local_timezone = datetime.now().astimezone().tzinfo
323-
formatted = [
324-
_format_one(dt, date=date, local_tz=local_timezone) for dt in datetime_input
325-
]
324+
# element invalidates the range.
325+
formatted = [_format_one(dt, date=date) for dt in datetime_input]
326326
if any(f is None for f in formatted):
327327
return None
328328
return "/".join(formatted)

0 commit comments

Comments
 (0)