mager-bench

Claude Sonnet 4.6 × api-client

3.0

Class 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.0
correctness 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