Skip to content

Commit 20b6eba

Browse files
thodson-usgsclaude
andauthored
Fix get_nearest_continuous: accept scalar targets and missing time column (#251)
The docstring says ``targets`` accepts "anything ``pandas.to_datetime`` consumes", which includes a bare string or ``pd.Timestamp``. But ``pd.to_datetime("2024-01-01T00:00:00Z", utc=True)`` returns a scalar ``Timestamp``, and ``pd.DatetimeIndex(scalar)`` raises ``TypeError`` — so single-value cases crashed despite the documented contract. Wrap a scalar result in a one-element ``DatetimeIndex`` so any ``pandas.to_datetime``-consumable input works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 018d9c2 commit 20b6eba

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

dataretrieval/waterdata/nearest.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def get_nearest_continuous(
139139
... )
140140
"""
141141
_check_nearest_kwargs(kwargs, on_tie)
142-
target_index = pd.DatetimeIndex(pd.to_datetime(targets, utc=True))
142+
target_index = _coerce_targets(targets)
143143
window_td = pd.Timedelta(window)
144144

145145
if len(target_index) == 0:
@@ -153,6 +153,11 @@ def get_nearest_continuous(
153153
filter_lang="cql-text",
154154
**kwargs,
155155
)
156+
if "time" not in df.columns:
157+
raise ValueError(
158+
"get_nearest_continuous requires a 'time' column in the response; "
159+
"if a `properties` kwarg was passed, include 'time' in it"
160+
)
156161
if df.empty:
157162
return _empty_nearest_result(df), md
158163

@@ -174,6 +179,19 @@ def get_nearest_continuous(
174179
return pd.DataFrame(selected).reset_index(drop=True), md
175180

176181

182+
def _coerce_targets(targets: Any) -> pd.DatetimeIndex:
183+
"""Accept anything ``pandas.to_datetime`` consumes, including a single value.
184+
185+
A bare scalar (string, ``Timestamp``, ``datetime``, …) becomes a
186+
one-element ``DatetimeIndex``; an iterable (list, ``Series``, ``ndarray``)
187+
is wrapped directly so its elements are preserved.
188+
"""
189+
parsed = pd.to_datetime(targets, utc=True)
190+
if pd.api.types.is_scalar(parsed):
191+
parsed = [parsed]
192+
return pd.DatetimeIndex(parsed)
193+
194+
177195
def _check_nearest_kwargs(kwargs: dict[str, Any], on_tie: OnTie) -> None:
178196
"""Reject kwargs the helper owns; validate ``on_tie``."""
179197
for forbidden in ("time", "filter", "filter_lang"):

tests/waterdata_nearest_test.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,68 @@ def test_forwards_kwargs_to_get_continuous(patch_get_continuous):
265265
_, kwargs = patch_get_continuous.call_args
266266
assert kwargs["statistic_id"] == "00011"
267267
assert kwargs["approval_status"] == "Approved"
268+
269+
270+
def test_accepts_single_string_target(patch_get_continuous):
271+
"""A bare scalar target must round-trip through pd.to_datetime.
272+
273+
Regression: previously `pd.DatetimeIndex(pd.to_datetime("...", utc=True))`
274+
raised TypeError because pd.to_datetime returns a scalar Timestamp for a
275+
single-string input.
276+
"""
277+
patch_get_continuous.return_value = (
278+
_fake_df([{"time": "2023-06-15T10:30:00Z", "value": 22.4}]),
279+
mock.Mock(),
280+
)
281+
result, _ = get_nearest_continuous(
282+
"2023-06-15T10:30:31Z", monitoring_location_id="USGS-02238500"
283+
)
284+
assert len(result) == 1
285+
assert result["target_time"].iloc[0] == pd.Timestamp("2023-06-15T10:30:31Z")
286+
287+
288+
def test_accepts_single_timestamp_target(patch_get_continuous):
289+
"""A single ``pd.Timestamp`` target also round-trips."""
290+
patch_get_continuous.return_value = (
291+
_fake_df([{"time": "2023-06-15T10:30:00Z", "value": 22.4}]),
292+
mock.Mock(),
293+
)
294+
target = pd.Timestamp("2023-06-15T10:30:31Z")
295+
result, _ = get_nearest_continuous(target, monitoring_location_id="USGS-02238500")
296+
assert len(result) == 1
297+
298+
299+
def test_accepts_pandas_series_targets(patch_get_continuous):
300+
"""A ``pd.Series`` of timestamps preserves all elements (not just the first)."""
301+
patch_get_continuous.return_value = (
302+
_fake_df(
303+
[
304+
{"time": "2023-06-15T10:30:00Z", "value": 22.4},
305+
{"time": "2023-06-16T10:30:00Z", "value": 22.5},
306+
]
307+
),
308+
mock.Mock(),
309+
)
310+
targets = pd.Series(["2023-06-15T10:30:31Z", "2023-06-16T10:30:31Z"])
311+
result, _ = get_nearest_continuous(targets, monitoring_location_id="USGS-02238500")
312+
assert len(result) == 2
313+
314+
315+
def test_missing_time_column_raises_helpful_error(patch_get_continuous):
316+
"""If the response has no 'time' column (e.g. user passed `properties`
317+
that excluded it), raise ValueError instead of crashing with KeyError.
318+
"""
319+
df_no_time = pd.DataFrame(
320+
{
321+
"value": [22.4],
322+
"monitoring_location_id": ["USGS-02238500"],
323+
}
324+
)
325+
patch_get_continuous.return_value = (df_no_time, mock.Mock())
326+
327+
with pytest.raises(ValueError, match="'time' column"):
328+
get_nearest_continuous(
329+
["2023-06-15T10:30:31Z"],
330+
monitoring_location_id="USGS-02238500",
331+
properties=["value", "monitoring_location_id"],
332+
)

0 commit comments

Comments
 (0)