Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |