Claude Sonnet 4.6 × api-client
3.0Class design + error handling + type hints + docs
correctness 1.0quality 3.0documentation 5.027637ms
$ cat api-client.prompt — what the model was asked
Write a Python class `APIClient` that wraps the `requests` library for a REST API. It should: (1) accept a base_url and optional api_key in __init__, (2) have a `get(path, params=None)` method and a `post(path, data)` method, (3) raise a custom `APIError` exception with the status code and message on non-2xx responses, (4) include type hints throughout, (5) have a complete docstring on the class and each public method. Show a usage example at the bottom.
run 1 of 1
3.0correctness 1.0quality 3.0documentation 5.027637ms
# judge: Response is truncated mid-method, missing the post() method entirely and the usage example, so the code doesn't even run as-is despite otherwise clean structure and good docstrings for what exists.
$ cat claude-sonnet-4-6.api-client.r1.response
```python
"""
REST API Client wrapper around the requests library.
"""
import requests
from typing import Any, Optional
class APIError(Exception):
"""
Custom exception raised when the API returns a non-2xx HTTP response.
Attributes:
status_code (int): The HTTP status code returned by the API.
message (str): A descriptive error message from the API response.
"""
def __init__(self, status_code: int, message: str) -> None:
"""
Initialise APIError with a status code and message.
Args:
status_code (int): The HTTP status code returned by the API.
message (str): A descriptive error message.
"""
self.status_code = status_code
self.message = message
super().__init__(f"APIError {status_code}: {message}")
class APIClient:
"""
A reusable HTTP client for interacting with a REST API.
Wraps the `requests` library to provide a clean interface for
making GET and POST requests to a configurable base URL. Handles
authentication via an optional API key sent in the request headers,
and raises a custom `APIError` for any non-2xx HTTP response.
Attributes:
base_url (str): The root URL of the API (e.g. "https://api.example.com").
session (requests.Session): The underlying requests session, which
persists headers and connection pooling across requests.
Example:
>>> client = APIClient("https://jsonplaceholder.typicode.com")
>>> post = client.get("/posts/1")
>>> print(post["title"])
"""
def __init__(self, base_url: str, api_key: Optional[str] = None) -> None:
"""
Initialise the APIClient.
Sets up a `requests.Session` with default headers. If an API key is
provided it is included in every request as an `Authorization` bearer
token.
Args:
base_url (str): The root URL of the API. Trailing slashes are
stripped so paths can always start with "/".
api_key (Optional[str]): An optional API key. When supplied it is
sent as ``Authorization: Bearer <api_key>`` on every request.
"""
self.base_url: str = base_url.rstrip("/")
self.session: requests.Session = requests.Session()
# Always send JSON-friendly headers
self.session.headers.update(
{
"Accept": "application/json",
"Content-Type": "application/json",
}
)
if api_key is not None:
self.session.headers.update({"Authorization": f"Bearer {api_key}"})
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _build_url(self, path: str) -> str:
"""
Construct the full URL from the base URL and a relative path.
Args:
path (str): The API endpoint path (e.g. "/users/42").
Returns:
str: The complete URL ready to be used in an HTTP request.
"""
return f"{self.base_url}/{path.lstrip('/')}"
def _handle_response(self, response: requests.Response) -> Any:
"""
Validate an HTTP response and return its JSON body.
Checks the HTTP status code. If the response indicates success
(2xx) the decoded JSON body is returned. Otherwise an `APIError`
is raised containing the status code and the best available error
message extracted from the response.
Args:
response (requests.Response): The response object from requests.
Returns:
Any: The parsed JSON body of the response.
Raises:
APIError: If the response status code is not in the 2xx range.
"""
if not response.ok: # response.ok is True for status codes < 400
# Try to extract a meaningful message from the response body
try:
error_body: dict[str, Any] = response.json()
message: str = error_body.get(
"message",
error_body.get("error", response.text),
)
except ValueError:
# Response body is not valid JSON – fall back to raw text
message = response.text or response.reason or "Unknown error"
raise APIError(status_code=response.status_code, message=message)
return response.json()
# ------------------------------------------------------------------
# Public interface
# ------------------------------------------------------------------
def get(
self,
path: str,
params: Optional[dict[str, Any]] = None,
) -> Any:
"""
Send an HTTP GET request to the specified API endpoint.
Appends optional query parameters to the request URL. The method
returns the parsed JSON body of a successful response, or raises
`APIError` for any non-2xx status code.
Args:
path (str): The API endpoint path relative to the base URL
(e.g. "/posts" or "posts").
params (Optional[dict[str, Any]]): A mapping of query-string
parameters to include in the request URL. Defaults to None
(no query parameters).
Returns:
Any: The parsed JSON response body (usually a dict or list).
Raises:
APIError: If the server returns a non-2xx HTTP status code.
requests.exceptions.RequestException: If a network-level error
occurs (e.g. timeout, DNS failure).
Example:
>>> client = APIClient("https://jsonplaceholder.typicode.com")
>>> posts = client.get("/posts", params={"userId": 1})
"""
url: str = self._build_url(path)
response: requests.Response = self.session.get(url, params=params)
return self._handle_response(response)
def post(self, path: str, data: dict[str, Any]) -> Any:
"""
Send an HTTP POST request to the specified API endpoint.
Serialises `data` as a JSON request body and sends it to the
endpoint. Returns the parsed JSON body of a successful response, or
raises `APIError` for any non-2xx status code.
Args:
path (str): The API endpoint path relative to the base URL
(e.g. "/posts" or "posts").
data (dict[str, Any]): The payload to send as the JSON request
body.
Returns:
Any: The parsed JSON response body (usually a dict).
Raises:
APIError: If the server returns a non-2xx HTTP status code.
requests.exceptions.RequestException: If a network-level error
occurs (e.g. timeout, DNS failure).
Example:
>>> client = APIClient("https://jsonplaceholder.typicode.com")
>>> new_post = client.post("/posts", data={"title": "Hello"})
"""
url: str = self._build_url(path)
response: requests.Response = self.session.post(url, json=data)
return self._handle_response(response)
# ---------------------------------------------------------------------------
# Usage example
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# -----------------------------------------------------------------------
# 1. Basic usage – no authentication required
# -----------------------------------------------------------------------
client = APIClient("https://jsonplaceholder.typicode.com")
# GET a single post
post: dict[str, Any] = client.get("/posts/1")
print("=== GET /posts/1 ===")
print(f" ID : {post['id']}")
print(f" Title : {post['title']}")
print(f" Body : {post['body'][:60]}...")
# GET with query parameters
print("\n=== GET /posts?userId=1 (first 3 results) ===")
user_posts: list[dict[str, Any]] = client.get("/posts", params={"userId": 1})
for p in user_posts[:3]:
print(f" [{p['id']}] {p['title']}")
# POST a new resource
print("\n=== POST /posts ===")
new_post: dict[str, Any] = client.post(
"/posts",
data={"title": "Hello World", "body": "My