Skip to content

Commit 5fb747a

Browse files
author
Julian Kath
committed
fix: swallow "no data today" 404/409 in changed-* endpoints
1 parent d30ed16 commit 5fb747a

3 files changed

Lines changed: 132 additions & 15 deletions

File tree

src/stats_can/scwds.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,24 @@ def get_changed_cube_list(date: dt.date | None = None) -> list[ChangedCube]:
139139
Returns
140140
-------
141141
:
142-
list of changed cubes, one for each table and when it was updated
142+
list of changed cubes, one for each table and when it was updated.
143+
Returns an empty list when ``date`` is the current day but the daily
144+
release window has not opened yet (the API returns HTTP 409 in that
145+
case). HTTP 404 (e.g. for a future ``date``) is propagated as it
146+
indicates a caller error.
143147
"""
144148
if date is None:
145149
date = dt.date.today()
146-
return _fetch_and_validate(
147-
url=f"{SC_URL}getChangedCubeList/{date}", schema=list[ChangedCube]
148-
)
150+
try:
151+
return _fetch_and_validate(
152+
url=f"{SC_URL}getChangedCubeList/{date}", schema=list[ChangedCube]
153+
)
154+
except requests.HTTPError as exc:
155+
# 409 means "today's release window hasn't opened yet" -- a normal
156+
# condition, not an error. 404 (future date) is still a real error.
157+
if exc.response is not None and exc.response.status_code == 409:
158+
return []
159+
raise
149160

150161

151162
def get_cube_metadata(tables: str | list[str]) -> list[CubeMetadata]:
@@ -245,7 +256,11 @@ def get_changed_series_data_from_cube_pid_coord(
245256
-------
246257
:
247258
One :class:`VectorData` per requested pair, in the order returned
248-
by the API.
259+
by the API. Returns an empty list when none of the requested pairs
260+
have changes today (the API returns HTTP 404 in that case). Note
261+
that the API does not distinguish "valid pair but unchanged" from
262+
"invalid pair" -- both produce a 404. Use
263+
:func:`get_series_info_from_cube_pid_coord` to validate a pair.
249264
"""
250265
if isinstance(pairs, tuple):
251266
pairs = [pairs]
@@ -257,9 +272,14 @@ def get_changed_series_data_from_cube_pid_coord(
257272
}
258273
for product_id, coord in pairs
259274
]
260-
return _post_in_chunks(
261-
f"{SC_URL}getChangedSeriesDataFromCubePidCoord", body, VectorData
262-
)
275+
try:
276+
return _post_in_chunks(
277+
f"{SC_URL}getChangedSeriesDataFromCubePidCoord", body, VectorData
278+
)
279+
except requests.HTTPError as exc:
280+
if exc.response is not None and exc.response.status_code == 404:
281+
return []
282+
raise
263283

264284

265285
def get_changed_series_data_from_vector(
@@ -275,10 +295,22 @@ def get_changed_series_data_from_vector(
275295
Returns
276296
-------
277297
:
278-
List of dicts containing changed data for each vector
298+
List of dicts containing changed data for each vector. Returns an
299+
empty list when none of the requested vectors have changes today
300+
(the API returns HTTP 404 in that case). Note that the API does not
301+
distinguish "valid vector but unchanged" from "invalid vector" --
302+
both produce a 404. Use :func:`get_series_info_from_vector` to
303+
validate a vector id.
279304
"""
280305
body = [{"vectorId": v} for v in parse_vectors(vectors)]
281-
return _post_in_chunks(f"{SC_URL}getChangedSeriesDataFromVector", body, VectorData)
306+
try:
307+
return _post_in_chunks(
308+
f"{SC_URL}getChangedSeriesDataFromVector", body, VectorData
309+
)
310+
except requests.HTTPError as exc:
311+
if exc.response is not None and exc.response.status_code == 404:
312+
return []
313+
raise
282314

283315

284316
def get_data_from_cube_pid_coord_and_latest_n_periods(

tests/test_error_handling.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,22 @@ def test_chunks_large_input(self):
408408
scwds.get_changed_series_data_from_cube_pid_coord(pairs)
409409
assert mocked.call_count == 2
410410

411+
def test_404_returns_empty_list(self):
412+
"""HTTP 404 ("no changes today" or invalid pair) should yield ``[]``."""
413+
err_resp = MagicMock(spec=requests.Response)
414+
err_resp.status_code = 404
415+
mock_resp = _mock_response(
416+
status_code=404,
417+
raise_for_status=requests.exceptions.HTTPError(
418+
"404 Not Found", response=err_resp
419+
),
420+
)
421+
with patch.object(scwds._session, "request", return_value=mock_resp):
422+
result = scwds.get_changed_series_data_from_cube_pid_coord(
423+
("35100003", "1.12")
424+
)
425+
assert result == []
426+
411427

412428
class TestGetChangedSeriesDataFromVector:
413429
"""Tests for get_changed_series_data_from_vector."""
@@ -452,6 +468,52 @@ def test_chunks_large_input(self):
452468
scwds.get_changed_series_data_from_vector(vectors)
453469
assert mocked.call_count == 2
454470

471+
def test_404_returns_empty_list(self):
472+
"""HTTP 404 ("no changes today" or invalid vector) should yield ``[]``."""
473+
err_resp = MagicMock(spec=requests.Response)
474+
err_resp.status_code = 404
475+
mock_resp = _mock_response(
476+
status_code=404,
477+
raise_for_status=requests.exceptions.HTTPError(
478+
"404 Not Found", response=err_resp
479+
),
480+
)
481+
with patch.object(scwds._session, "request", return_value=mock_resp):
482+
result = scwds.get_changed_series_data_from_vector("v32164132")
483+
assert result == []
484+
485+
486+
class TestGetChangedCubeList:
487+
"""Tests for get_changed_cube_list status-code handling."""
488+
489+
def test_409_returns_empty_list(self):
490+
"""HTTP 409 (release window not open yet) should yield ``[]``."""
491+
err_resp = MagicMock(spec=requests.Response)
492+
err_resp.status_code = 409
493+
mock_resp = _mock_response(
494+
status_code=409,
495+
raise_for_status=requests.exceptions.HTTPError(
496+
"409 Conflict", response=err_resp
497+
),
498+
)
499+
with patch.object(scwds._session, "request", return_value=mock_resp):
500+
result = scwds.get_changed_cube_list()
501+
assert result == []
502+
503+
def test_404_propagates(self):
504+
"""HTTP 404 (e.g. future date) is a caller error and should raise."""
505+
err_resp = MagicMock(spec=requests.Response)
506+
err_resp.status_code = 404
507+
mock_resp = _mock_response(
508+
status_code=404,
509+
raise_for_status=requests.exceptions.HTTPError(
510+
"404 Not Found", response=err_resp
511+
),
512+
)
513+
with patch.object(scwds._session, "request", return_value=mock_resp):
514+
with pytest.raises(requests.exceptions.HTTPError):
515+
scwds.get_changed_cube_list()
516+
455517

456518
class TestGetDataFromCubePidCoordAndLatestNPeriods:
457519
"""Tests for get_data_from_cube_pid_coord_and_latest_n_periods."""

tests/test_scwds.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,43 @@ def test_gsifv():
9090

9191

9292
def test_gcsdfcpc():
93-
"""Test get changed series data from cube pid coord."""
93+
"""Test get changed series data from cube pid coord.
94+
95+
The API returns data only for series that changed today, so this may
96+
legitimately come back empty.
97+
"""
9498
r = stats_can.scwds.get_changed_series_data_from_cube_pid_coord(
9599
("35100003", "1.12")
96100
)
97101
assert isinstance(r, list)
98-
assert len(r) == 1
99-
assert r[0]["productId"] == 35100003
102+
if r:
103+
assert r[0]["productId"] == 35100003
104+
105+
106+
def test_gcsdfcpc_invalid_pair():
107+
"""Invalid (productId, coordinate) pairs return [] (the API does not
108+
distinguish them from valid-but-unchanged pairs -- both come back as 404).
109+
"""
110+
r = stats_can.scwds.get_changed_series_data_from_cube_pid_coord(("99999999", "1"))
111+
assert r == []
100112

101113

102114
def test_gcsdfv():
103-
"""Test get get changed series data from vector."""
115+
"""Test get get changed series data from vector.
116+
117+
The API returns data only for vectors that changed today, so this may
118+
legitimately come back empty.
119+
"""
104120
r = stats_can.scwds.get_changed_series_data_from_vector(v)
105121
assert isinstance(r, list)
106-
assert len(r) == 1
122+
123+
124+
def test_gcsdfv_invalid_vector():
125+
"""Invalid vector ids return [] (the API does not distinguish them
126+
from valid-but-unchanged vectors -- both come back as 404).
127+
"""
128+
r = stats_can.scwds.get_changed_series_data_from_vector("9")
129+
assert r == []
107130

108131

109132
def test_gdfcpcalnp():

0 commit comments

Comments
 (0)