teamforge / tasks /bonus_task.py
Your Name
fix: add FastAPI REST endpoints for OpenEnv validator
637f42c
"""
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