mager-bench

Claude Sonnet 4.6 × api-client

3.7

Class design + error handling + type hints + docs

correctness 2.0quality 5.0documentation 4.037832ms
$ 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.7
correctness 2.0quality 5.0documentation 4.037832ms

# judge: Response is cut off mid-docstring for post(), missing the actual implementation and the required usage example, making the class non-functional and the task incomplete despite good style in the parts that exist.

$ 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 an API response has a non-2xx status code.

    Attributes:
        status_code (int): The HTTP status code returned by the API.
        message (str): A descriptive error message.
    """

    def __init__(self, status_code: int, message: str) -> None:
        """
        Initialize 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 that wraps the `requests` library for interacting
    with a REST API.

    This client handles:
    - Base URL management so callers only supply relative paths.
    - Optional API key authentication via the Authorization header.
    - Automatic JSON serialization/deserialization for requests and responses.
    - Consistent error handling: non-2xx responses raise `APIError`.

    Attributes:
        base_url (str): The root URL of the API (e.g., "https://api.example.com").
        session (requests.Session): The underlying requests session, which
            enables connection reuse and shared headers across all calls.

    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:
        """
        Initialize the API client.

        Args:
            base_url (str): The root URL of the API. Trailing slashes are
                stripped automatically so path joining is consistent.
            api_key (Optional[str]): If provided, this value is sent as a
                Bearer token in the `Authorization` header of every request.

        Example:
            >>> client = APIClient("https://api.example.com", api_key="secret")
        """
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()

        # Set shared headers for every request made by this session.
        self.session.headers.update({"Content-Type": "application/json",
                                     "Accept": "application/json"})

        if api_key:
            self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _build_url(self, path: str) -> str:
        """
        Combine the base URL with a relative path.

        Args:
            path (str): The API endpoint path (e.g., "/users/42").

        Returns:
            str: The fully-qualified URL.
        """
        return f"{self.base_url}/{path.lstrip('/')}"

    def _handle_response(self, response: requests.Response) -> Any:
        """
        Inspect an HTTP response and either return its parsed JSON body or
        raise `APIError` for non-2xx status codes.

        Args:
            response (requests.Response): The response object from requests.

        Returns:
            Any: The parsed JSON payload (dict, list, etc.).

        Raises:
            APIError: If the response status code is not in the 2xx range.
        """
        if not response.ok:  # ok is True for 2xx status codes
            # Attempt to extract a descriptive message from the response body.
            try:
                detail = response.json()
                # Many APIs nest the error message under "message" or "error".
                message = (
                    detail.get("message")
                    or detail.get("error")
                    or str(detail)
                )
            except ValueError:
                # Fall back to raw text if the body is not valid JSON.
                message = response.text or response.reason

            raise APIError(status_code=response.status_code, message=message)

        # Return parsed JSON when the response body is not empty.
        if response.content:
            return response.json()
        return None

    # ------------------------------------------------------------------
    # Public interface
    # ------------------------------------------------------------------

    def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
        """
        Send an HTTP GET request to the specified endpoint.

        Query parameters are URL-encoded and appended to the request URL
        automatically by the underlying requests library.

        Args:
            path (str): The API endpoint path relative to the base URL
                (e.g., "/users" or "users/42").
            params (Optional[dict[str, Any]]): A dictionary of query string
                parameters. Defaults to ``None`` (no query parameters).

        Returns:
            Any: The parsed JSON response body.

        Raises:
            APIError: If the server returns a non-2xx status code.
            requests.exceptions.RequestException: For low-level network errors
                (connection refused, timeout, etc.).

        Example:
            >>> client = APIClient("https://jsonplaceholder.typicode.com")
            >>> users = client.get("/users", params={"_limit": 3})
            >>> len(users)
            3
        """
        url = self._build_url(path)
        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 with a JSON body to the specified endpoint.

        The ``data`` dictionary is serialized to JSON automatically. The
        ``Content-Type: application/json`` header is set for every request via
        the shared session, so no extra configuration is required.

        Args:
            path (str): The API endpoint path relative to the base URL
                (e.g., "/posts" or "auth/login").
            data (dict[str, Any]): The request payload. Must be a dictionary
                that is JSON-serializable.

        Returns:
            Any: The parsed JSON response body (often the newly created
                resource, depending on the API).

        Raises:
            APIError: If the server returns a non-2xx status code.
            requests.exceptions.RequestException: For low-level network errors
                (connection refused, timeout, etc.).

        Example:
            >>> client = APIClient("https://jsonplaceholder.typicode.com")
            >>> new_post = client.post("/posts", data={"title": "Hello", "body": "World", "userId": 1})
            >>> print(new_post["id"])
            101
        """
        url = self._build_url(path)
        response = self.session.post(url, json=data)
        return self._handle_response(response)


# ---------------------------------------------------------------------------
# Usage example
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    # We use JSONPlaceholder, a free public REST API, so this example requires
    # no credentials and can be run as-is.

    BASE_URL = "https://jsonplaceholder.typicode.com"

    # ── 1. Create the client (no API key needed for JSONPlaceholder) ─────────
    client = APIClient(BASE_URL)

    # ── 2. GET a single resource ─────────────────────────────────────────────
    print("── GET /posts/1 ─────────────────────────────────────────────────")
    try:
        post = client.get("/posts/1")
        print(f"  id    : {post['id']}")
        print(f"  title : {post['title']}")
    except APIError as exc:
        print(f"  Request failed → {exc}")

    # ── 3. GET with query parameters ─────────────────────────────────────────
    print("\n── GET /posts?_limit=3 ──────────────────────────────────────────")
    try:
        posts = client.get("/posts", params={"_limit": 3})
        for p in posts:
            print(f"  [{p['id']}] {p['title'][:50]}")
    except APIError as exc:
        print(f"  Request failed → {exc}")

    # ── 4. POST a new resource ───────────────────────────────────────────────
    print("\n── POST /posts ──────────────────────────────────────────────────")
    try:
        new_post = client.post(
            "/posts",
            data={"title": "My New Post", "body": "Hello, world!", "userId": 1},
        )
        print(f"  Created post with id: {new_post['id']}")
        print(f"  Title               : {new_post['title']}")
    except APIError as exc:
        print(f"  Request failed → {exc}")

    # ── 5. Demonstrate error handling ────────────────────────────────────────
    print("\n── GET /posts/99999 (non-existent → 404) ────────────────────────")
    try:
        client.get("/posts/99999")
    except APIError as exc:
        # APIError carries the status code separately so callers can branch on it.
        print(f"  Caught APIError!")
        print(f"  Status code : {exc.status_code}")
        print(f"  Message     : {exc.message}")
```

---

## How it works — key design decisions

| Decision | Rationale |
|---|---|
| **`requests.Session`** | Reuses the TCP connection across multiple calls and centralises shared headers (auth, content-type), avoiding repetition in every method. |
| **`_build_url`** strips slashes | Prevents double-slash URLs regardless of whether the caller writes `"/users"` or `"users"`. |
| **`_handle_response`** is private | Keeps error-handling logic in one place; both `get` and `post` delegate to it without duplication. |
| **`response.ok`** | A clean `requests` shortcut that is `True` for any 2xx status code. |
| **`APIError` carries `status_code`** | Callers can catch the exception and inspect the code to implement retry logic or branch on specific errors (401 → re-auth, 429 → back-off, etc.). |
| **`json=data` in POST** | Lets `requests` handle serialization *and* automatically sets the correct `Content-Type`, removing a common source of bugs. |