forked from ChelseaKR/disclosed
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_rate_limits.py
More file actions
142 lines (108 loc) · 5.12 KB
/
Copy pathtest_rate_limits.py
File metadata and controls
142 lines (108 loc) · 5.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Retry behaviour: which failures are worth waiting out, and which are ours to fix."""
from __future__ import annotations
import json
import urllib.error
from typing import Any
import pytest
from disclosed.sources import college_scorecard
class _FakeResponse:
"""The shape ``urlopen`` returns: a byte body, a status, and headers with ``.get``."""
def __init__(self, body: str) -> None:
self._body = body.encode("utf-8")
self.status = 200
self.headers: dict[str, str] = {}
def read(self) -> bytes:
return self._body
def __enter__(self) -> _FakeResponse:
return self
def __exit__(self, *exc: object) -> None:
return None
def _ok() -> _FakeResponse:
return _FakeResponse(json.dumps({"metadata": {"total": 1}, "results": [{"id": 1}]}))
def _http_error(code: int) -> urllib.error.HTTPError:
return urllib.error.HTTPError("url", code, "boom", {}, None) # type: ignore[arg-type]
@pytest.fixture(autouse=True)
def no_real_sleeping(monkeypatch: pytest.MonkeyPatch) -> list[float]:
"""Record backoff durations instead of serving them, so the suite stays fast."""
slept: list[float] = []
monkeypatch.setattr(college_scorecard, "_sleep", slept.append)
return slept
class TestRetries:
def test_recovers_when_a_429_clears(self, monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[int] = []
def flaky(url: str, timeout: float = 0) -> _FakeResponse:
calls.append(1)
if len(calls) < 3:
raise _http_error(429)
return _ok()
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", flaky)
assert college_scorecard.fetch_page(0)["results"] == [{"id": 1}]
assert len(calls) == 3
def test_backoff_grows(
self, monkeypatch: pytest.MonkeyPatch, no_real_sleeping: list[float]
) -> None:
calls: list[int] = []
def flaky(url: str, timeout: float = 0) -> _FakeResponse:
calls.append(1)
if len(calls) < 3:
raise _http_error(429)
return _ok()
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", flaky)
college_scorecard.fetch_page(0)
assert no_real_sleeping == [2.0, 4.0]
def test_server_errors_are_retried_too(self, monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[int] = []
def flaky(url: str, timeout: float = 0) -> _FakeResponse:
calls.append(1)
if len(calls) < 2:
raise _http_error(503)
return _ok()
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", flaky)
college_scorecard.fetch_page(0)
assert len(calls) == 2
def test_persistent_429_raises_rate_limited(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: (_ for _ in ()).throw(_http_error(429)),
)
with pytest.raises(college_scorecard.RateLimited, match="after 4 attempts"):
college_scorecard.fetch_page(0)
def test_demo_key_gets_the_specific_remedy(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""The error must name the fix, because 'rate limited' alone leaves the user stuck."""
monkeypatch.delenv("DATA_GOV_API_KEY", raising=False)
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: (_ for _ in ()).throw(_http_error(429)),
)
with pytest.raises(college_scorecard.RateLimited, match="DATA_GOV_API_KEY"):
college_scorecard.fetch_page(0)
def test_real_key_omits_the_demo_key_hint(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DATA_GOV_API_KEY", "realkey")
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: (_ for _ in ()).throw(_http_error(429)),
)
with pytest.raises(college_scorecard.RateLimited) as caught:
college_scorecard.fetch_page(0)
assert "DEMO_KEY" not in str(caught.value)
class TestNoPointRetrying:
@pytest.mark.parametrize("code", [400, 401, 403, 404])
def test_client_errors_fail_immediately(
self, code: int, monkeypatch: pytest.MonkeyPatch, no_real_sleeping: list[float]
) -> None:
"""A malformed request will stay malformed. Retrying only wastes the caller's time."""
calls: list[int] = []
def failing(url: str, timeout: float = 0) -> Any:
calls.append(1)
raise _http_error(code)
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", failing)
with pytest.raises(college_scorecard.ScorecardError, match=f"HTTP {code}"):
college_scorecard.fetch_page(0)
assert len(calls) == 1
assert no_real_sleeping == []
def test_rate_limited_is_catchable_as_scorecard_error(self) -> None:
"""Callers that only care that the fetch failed should not need the subclass."""
assert issubclass(college_scorecard.RateLimited, college_scorecard.ScorecardError)