Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cwms/timeseries/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ def _call_with_retry(fn: Any, *args: Any, attempts: int = _CHUNK_ATTEMPTS) -> An
try:
return fn(*args)
except Exception as e:
status_code = getattr(getattr(e, "response", None), "status_code", None)
if status_code == 404:
raise
if i == attempts - 1:
raise
logging.warning(f"chunk attempt {i + 1}/{attempts} failed: {e}")
Expand Down
29 changes: 29 additions & 0 deletions tests/cda/timeseries/timeseries_CDA_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TEST_TSID_COPY_NULLS = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Copy-Nulls"
TS_ID_REV_TEST = TEST_TSID_MULTI.replace("Raw-Multi", "Raw-Rev-Test")
TEST_TSID_CHUNK_PARTIAL = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Multi-Partial"
TEST_TSID_MISSING = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Missing-404"
# Generate 15-minute interval timestamps
START_DATE_CHUNK_MULTI = datetime(2025, 7, 31, 0, 0, tzinfo=timezone.utc)
END_DATE_CHUNK_MULTI = datetime(2025, 9, 30, 23, 45, tzinfo=timezone.utc)
Expand All @@ -37,6 +38,7 @@
TEST_TSID_CHUNK_NULLS,
TEST_TSID_COPY_NULLS,
TEST_TSID_CHUNK_PARTIAL,
TEST_TSID_MISSING,
TEST_TSID_DELETE,
]

Expand Down Expand Up @@ -342,7 +344,7 @@
ts.store_timeseries(ts_json, multithread=True, chunk_size=chunk_size)

error_msg = str(exc_info.value)
assert "1 of " in error_msg and "chunk(s) failed to store" in error_msg

Check warning on line 347 in tests/cda/timeseries/timeseries_CDA_test.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Split this composite assertion into separate assertions.

See more on https://sonarcloud.io/project/issues?id=HydrologicEngineeringCenter_cwms-python&issues=AZ-z0m-2X1qv0iLRYPR7&open=AZ-z0m-2X1qv0iLRYPR7&pullRequest=303
assert "Error storing chunk from" in error_msg


Expand Down Expand Up @@ -385,11 +387,38 @@
)

error_msg = str(exc_info.value)
assert "1 of " in error_msg and "chunk(s) failed to fetch" in error_msg

Check warning on line 390 in tests/cda/timeseries/timeseries_CDA_test.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Split this composite assertion into separate assertions.

See more on https://sonarcloud.io/project/issues?id=HydrologicEngineeringCenter_cwms-python&issues=AZ-z0m-2X1qv0iLRYPR8&open=AZ-z0m-2X1qv0iLRYPR8&pullRequest=303
assert "Failed to fetch data from" in error_msg
assert "simulated CDA failure" in error_msg


def test_get_timeseries_missing_chunk_404_real_api():
"""A missing time series should fail once with a 404, not retry."""

max_days = 14
chunks = ts.chunk_timeseries_time_range(
START_DATE_CHUNK_MULTI,
END_DATE_CHUNK_MULTI.replace(tzinfo=timezone.utc),
timedelta(days=max_days),
)
assert len(chunks) > 1, "Test requires multiple chunks to exercise retry scope"

with pytest.raises(RuntimeError) as exc_info:
ts.get_timeseries(
ts_id=TEST_TSID_MISSING,
office_id=TEST_OFFICE,
begin=START_DATE_CHUNK_MULTI,
end=END_DATE_CHUNK_MULTI,
max_days_per_chunk=max_days,
unit="SI",
)

error_msg = str(exc_info.value)
assert "chunk(s) failed to fetch" in error_msg
assert "Failed to fetch data from" in error_msg
assert "Not Found" in error_msg or "404" in error_msg


def test_store_timesereis_chunk_to_with_null_values():
# Define parameters
ts_id = TEST_TSID_CHUNK_NULLS
Expand Down
21 changes: 21 additions & 0 deletions tests/mock/timeseries/timeseries_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,27 @@ def test_get_timeseries_paging(requests_mock):
assert data.df.shape == (30, 3)


def test_call_with_retry_does_not_retry_404():
class ResponseStub:
url = "https://mockwebserver.cwms.gov/timeseries"
status_code = 404
reason = "Not Found"
content = b""

call_count = 0

def failing_call():
nonlocal call_count
call_count += 1
raise cwms.api.ApiError(ResponseStub())

with pytest.raises(cwms.api.ApiError) as exc_info:
timeseries._call_with_retry(failing_call)

assert exc_info.value.response.status_code == 404
assert call_count == 1


def test_get_timeseries_group_default(requests_mock):
group_id = "USGS TS Data Acquisition"
category_id = "Data Acquisition"
Expand Down
Loading