Skip to content

Commit ba281d4

Browse files
akarivclaude
andcommitted
downloader: add request timeout and per-year error isolation
requests.get() had no timeout at all - found for real in CI, where a single unresponsive request to CBS's site stalled a run for 7+ minutes with no way to recover. Adds a (connect, read) timeout, and makes download_all() isolate failures per-year (logged and skipped) so one bad/slow year doesn't abort the whole batch - maximizing how much data is actually available afterward, consistent with the CI workflow's "download or fall back to a viable checkpoint" design. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3411b17 commit ba281d4

2 files changed

Lines changed: 47 additions & 8 deletions

File tree

Lamas/lamas/downloader.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
P_LIBUD2 = {2021} # -> p_libud_{4-digit year}.xlsx
2020

2121

22+
# (connect timeout, read timeout) in seconds - without this, a slow/unresponsive connection to
23+
# CBS's site can hang a download indefinitely (found for real: a CI run stalled 7+ minutes on a
24+
# single request with no timeout set at all).
25+
REQUEST_TIMEOUT = (10, 60)
26+
27+
2228
def download_excel(year, downloads_dir=DOWNLOADS_DIR):
2329
out_filename = f'{year}' + ('.xlsx' if year >= XLSX_YEAR else '.xls')
2430
if year in P_LIBUD:
@@ -31,7 +37,7 @@ def download_excel(year, downloads_dir=DOWNLOADS_DIR):
3137
out_filename = f'{downloads_dir}/lamas-muni-{out_filename}'
3238
if not os.path.exists(out_filename):
3339
logger.info('Downloading %s -> %s', url, out_filename)
34-
r = requests.get(url, stream=True)
40+
r = requests.get(url, stream=True, timeout=REQUEST_TIMEOUT)
3541
assert r.status_code == 200, f'Failed to download {url}'
3642
with open(out_filename, 'wb') as f:
3743
shutil.copyfileobj(r.raw, f)
@@ -40,8 +46,14 @@ def download_excel(year, downloads_dir=DOWNLOADS_DIR):
4046

4147

4248
def download_all(downloads_dir=DOWNLOADS_DIR, min_year=MIN_YEAR, max_year=MAX_YEAR):
49+
"""Downloads every configured year, isolated per-year: one year timing out or failing
50+
(network hiccup, a URL pattern change) is logged and skipped rather than aborting the whole
51+
batch, so as many years as possible are still available afterward."""
4352
os.makedirs(downloads_dir, exist_ok=True)
44-
return dict(
45-
(year, download_excel(year, downloads_dir))
46-
for year in range(min_year, max_year + 1)
47-
)
53+
filenames = {}
54+
for year in range(min_year, max_year + 1):
55+
try:
56+
filenames[year] = download_excel(year, downloads_dir)
57+
except Exception as e:
58+
logger.error('Failed to download year %s: %s', year, e)
59+
return filenames

Lamas/tests/test_downloader.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import io
22

3-
from lamas.downloader import download_excel
3+
from lamas.downloader import REQUEST_TIMEOUT, download_all, download_excel
44

55

66
class FakeResponse:
@@ -9,14 +9,26 @@ def __init__(self):
99
self.raw = io.BytesIO(b'fake-content')
1010

1111

12-
def _patch_get(monkeypatch, calls):
13-
def fake_get(url, stream=True):
12+
def _patch_get(monkeypatch, calls, kwargs_seen=None):
13+
def fake_get(url, stream=True, timeout=None):
1414
calls.append(url)
15+
if kwargs_seen is not None:
16+
kwargs_seen.append({'stream': stream, 'timeout': timeout})
1517
return FakeResponse()
1618
monkeypatch.setattr('lamas.downloader.requests.get', fake_get)
1719
monkeypatch.setattr('lamas.downloader.shutil.copyfileobj', lambda *a, **k: None)
1820

1921

22+
def test_download_passes_a_timeout(monkeypatch, tmp_path):
23+
# A hung/unresponsive connection to CBS's site must not block forever - found for real in
24+
# CI, where a request with no timeout stalled a run for 7+ minutes.
25+
calls, kwargs_seen = [], []
26+
_patch_get(monkeypatch, calls, kwargs_seen)
27+
download_excel(2020, str(tmp_path))
28+
assert kwargs_seen[0]['timeout'] == REQUEST_TIMEOUT
29+
assert kwargs_seen[0]['timeout'] is not None
30+
31+
2032
def test_p_libud_years_use_two_digit_pattern(monkeypatch, tmp_path):
2133
calls = []
2234
_patch_get(monkeypatch, calls)
@@ -57,3 +69,18 @@ def test_skips_download_if_file_already_exists(monkeypatch, tmp_path):
5769
result = download_excel(2020, str(tmp_path))
5870
assert calls == []
5971
assert result == str(out)
72+
73+
74+
def test_download_all_isolates_a_failing_year(monkeypatch, tmp_path):
75+
# One year timing out or erroring (network hiccup, a URL pattern change) must not abort the
76+
# whole batch - every other year should still download successfully.
77+
def flaky_get(url, stream=True, timeout=None):
78+
if '2021' in url:
79+
raise TimeoutError('simulated hang')
80+
return FakeResponse()
81+
monkeypatch.setattr('lamas.downloader.requests.get', flaky_get)
82+
monkeypatch.setattr('lamas.downloader.shutil.copyfileobj', lambda *a, **k: None)
83+
84+
filenames = download_all(str(tmp_path), min_year=2020, max_year=2022)
85+
assert set(filenames.keys()) == {2020, 2022}
86+
assert 2021 not in filenames

0 commit comments

Comments
 (0)