Spaces:
Sleeping
Sleeping
File size: 6,569 Bytes
637f42c | 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """
BONUS TASK: Write a working REST API client with retry logic.
Simulates: "We need a script that fetches paginated data from an API,
handles rate limits, and writes results to a CSV."
This task is bonus/unscored in the main benchmark but available for
extended evaluation. It requires the agent to:
1. Write a class with exponential backoff retry
2. Handle pagination (next_cursor pattern)
3. Write results to CSV with proper headers
4. Unit tests must mock the HTTP calls (no real network)
"""
from __future__ import annotations
TASK_ID = "bonus_api_client_retry"
DIFFICULTY = "bonus"
MAX_STEPS = 35
DESCRIPTION = """
## Bonus Task: Paginated API Client with Retry Logic
Write `client/fetcher.py` — a `DataFetcher` class that:
1. `fetch_all(endpoint: str) -> list[dict]`
- Calls `GET endpoint?cursor=<token>` in a loop
- Stops when response has no `next_cursor`
- Returns all items combined
2. Retry logic (exponential backoff):
- On HTTP 429 or 5xx: wait `2^attempt` seconds, retry up to 3 times
- On 4xx (except 429): raise immediately
3. Write results to CSV:
`write_csv(data: list[dict], path: str) -> None`
Tests mock `requests.get` — no real network calls.
"""
INITIAL_FILES: dict[str, str] = {
"client/__init__.py": "from .fetcher import DataFetcher\n__all__ = ['DataFetcher']\n",
"client/fetcher.py": """\
\"\"\"Paginated API client with retry logic — implement the TODOs.\"\"\"
import csv
import time
from typing import Any
import requests
class DataFetcher:
\"\"\"Fetches paginated data from a REST API with exponential backoff.\"\"\"
MAX_RETRIES = 3
def __init__(self, base_url: str, api_key: str = "") -> None:
self.base_url = base_url.rstrip("/")
self.headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
def fetch_all(self, endpoint: str) -> list[dict[str, Any]]:
\"\"\"Fetch all pages and return combined item list.\"\"\"
# TODO: implement pagination + retry
raise NotImplementedError
def write_csv(self, data: list[dict[str, Any]], path: str) -> None:
\"\"\"Write list of dicts to CSV file.\"\"\"
# TODO: implement
raise NotImplementedError
def _get(self, url: str, params: dict | None = None) -> dict[str, Any]:
\"\"\"Single HTTP GET with exponential backoff retry.\"\"\"
# TODO: implement retry logic
raise NotImplementedError
""",
"tests/__init__.py": "",
"tests/test_fetcher.py": """\
\"\"\"Tests for DataFetcher — all HTTP calls are mocked.\"\"\"
import csv, io, pytest
from unittest.mock import MagicMock, patch
from client import DataFetcher
def mock_response(data, status=200):
r = MagicMock()
r.status_code = status
r.json.return_value = data
r.raise_for_status = MagicMock(
side_effect=None if status < 400 else Exception(f"HTTP {status}")
)
return r
class TestFetchAll:
def test_single_page(self):
fetcher = DataFetcher("http://api.test")
page1 = {"items": [{"id": 1}, {"id": 2}], "next_cursor": None}
with patch("requests.get", return_value=mock_response(page1)):
result = fetcher.fetch_all("/data")
assert result == [{"id": 1}, {"id": 2}]
def test_two_pages(self):
fetcher = DataFetcher("http://api.test")
p1 = {"items": [{"id": 1}], "next_cursor": "tok1"}
p2 = {"items": [{"id": 2}], "next_cursor": None}
responses = iter([mock_response(p1), mock_response(p2)])
with patch("requests.get", side_effect=lambda *a, **kw: next(responses)):
result = fetcher.fetch_all("/data")
assert result == [{"id": 1}, {"id": 2}]
def test_empty_response(self):
fetcher = DataFetcher("http://api.test")
with patch("requests.get", return_value=mock_response({"items": [], "next_cursor": None})):
assert fetcher.fetch_all("/data") == []
def test_auth_header_sent(self):
fetcher = DataFetcher("http://api.test", api_key="secret")
page = {"items": [], "next_cursor": None}
with patch("requests.get", return_value=mock_response(page)) as mock_get:
fetcher.fetch_all("/data")
_, kwargs = mock_get.call_args
assert "Authorization" in kwargs.get("headers", {})
class TestRetry:
def test_retries_on_429(self):
fetcher = DataFetcher("http://api.test")
rate_limited = mock_response({}, 429)
success = mock_response({"items": [{"id": 1}], "next_cursor": None}, 200)
calls = iter([rate_limited, rate_limited, success])
with patch("requests.get", side_effect=lambda *a, **kw: next(calls)), \
patch("time.sleep"):
result = fetcher.fetch_all("/data")
assert result == [{"id": 1}]
def test_raises_on_404(self):
fetcher = DataFetcher("http://api.test")
with patch("requests.get", return_value=mock_response({}, 404)):
with pytest.raises(Exception):
fetcher.fetch_all("/data")
def test_max_retries_exceeded_raises(self):
fetcher = DataFetcher("http://api.test")
always_429 = mock_response({}, 429)
with patch("requests.get", return_value=always_429), \
patch("time.sleep"):
with pytest.raises(Exception):
fetcher.fetch_all("/data")
class TestWriteCsv:
def test_writes_headers_and_rows(self, tmp_path):
fetcher = DataFetcher("http://api.test")
data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
path = str(tmp_path / "out.csv")
fetcher.write_csv(data, path)
with open(path) as f:
rows = list(csv.DictReader(f))
assert rows[0]["name"] == "Alice"
assert rows[1]["age"] == "25"
def test_empty_data_writes_only_header(self, tmp_path):
fetcher = DataFetcher("http://api.test")
path = str(tmp_path / "empty.csv")
fetcher.write_csv([{"id": 0}], path) # write once to get header
fetcher.write_csv([], path) # overwrite with empty
with open(path) as f:
content = f.read()
assert content.strip() == "" or "id" in content # either empty or header
""",
"pyproject.toml": "[tool.ruff]\nline-length = 88\nselect = [\"E\", \"F\", \"W\"]\nignore = []\n",
"README.md": "# Bonus: API Client with Retry\n\nImplement paginated fetch + exponential backoff retry.\n",
}
REQUIRED_KEYWORDS_IN_REVIEW = [
"exponential", "backoff", "pagination", "cursor", "retry"
]
PASSING_TESTS = 9
|