mager-bench

Claude Haiku 4.5 × api-client

1.7

Class design + error handling + type hints + docs

correctness 0.0quality 2.0documentation 3.014221ms
$ 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

1.7
correctness 0.0quality 2.0documentation 3.014221ms

# judge: Response is truncated mid-line inside the post() method, leaving the code non-functional and missing the required usage example entirely.

$ cat claude-haiku-4-5.api-client.r1.response
```python
"""
REST API Client Module

This module provides a simple wrapper around the requests library for
interacting with REST APIs with built-in error handling and authentication.
"""

import requests
from typing import Any, Optional, Dict
from requests.exceptions import RequestException, Timeout, ConnectionError


class APIError(Exception):
    """
    Custom exception raised when an API request fails.
    
    Attributes:
        status_code (int): HTTP status code of the failed response
        message (str): Error message describing the failure
    """
    
    def __init__(self, status_code: int, message: str) -> None:
        """
        Initialize the APIError exception.
        
        Args:
            status_code: HTTP status code from the failed response
            message: Descriptive error message
        """
        self.status_code = status_code
        self.message = message
        super().__init__(f"API Error {status_code}: {message}")


class APIClient:
    """
    A lightweight REST API client wrapper around the requests library.
    
    This class provides methods for making GET and POST requests to a REST API
    with automatic error handling, optional API key authentication, and support
    for query parameters and request bodies.
    
    Attributes:
        base_url (str): The base URL of the API
        api_key (str): Optional API key for authentication
        timeout (int): Request timeout in seconds (default: 10)
    
    Example:
        >>> client = APIClient("https://api.example.com", api_key="secret-key")
        >>> response = client.get("/users", params={"page": 1})
        >>> new_user = client.post("/users", data={"name": "John", "email": "john@example.com"})
    """
    
    def __init__(
        self,
        base_url: str,
        api_key: Optional[str] = None,
        timeout: int = 10
    ) -> None:
        """
        Initialize the APIClient with a base URL and optional API key.
        
        Args:
            base_url: The base URL of the API (e.g., "https://api.example.com")
            api_key: Optional API key for authentication. If provided, it will be
                    included in the Authorization header as a Bearer token.
            timeout: Request timeout in seconds (default: 10)
        
        Raises:
            ValueError: If base_url is empty or None
        """
        if not base_url:
            raise ValueError("base_url cannot be empty")
        
        self.base_url = base_url.rstrip("/")  # Remove trailing slash if present
        self.api_key = api_key
        self.timeout = timeout
    
    def _get_headers(self) -> Dict[str, str]:
        """
        Build headers dictionary including authentication if API key is set.
        
        Returns:
            Dictionary of HTTP headers including Content-Type and optional Authorization
        """
        headers: Dict[str, str] = {
            "Content-Type": "application/json",
        }
        
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        
        return headers
    
    def _raise_for_status(self, response: requests.Response) -> None:
        """
        Check response status and raise APIError for non-2xx responses.
        
        Args:
            response: The requests Response object to check
        
        Raises:
            APIError: If the response status code is not in the 2xx range
        """
        if not response.ok:
            try:
                error_message = response.json().get(
                    "message",
                    response.text or "Unknown error"
                )
            except (ValueError, AttributeError):
                error_message = response.text or "Unknown error"
            
            raise APIError(response.status_code, error_message)
    
    def get(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Make a GET request to the API.
        
        Args:
            path: The API endpoint path (e.g., "/users" or "/users/123")
            params: Optional dictionary of query parameters to include in the request
        
        Returns:
            The JSON response as a dictionary
        
        Raises:
            APIError: If the response status code is not 2xx
            Timeout: If the request times out
            ConnectionError: If there's a connection error
        
        Example:
            >>> client = APIClient("https://api.example.com")
            >>> users = client.get("/users", params={"page": 1, "limit": 10})
        """
        url = f"{self.base_url}{path}"
        
        try:
            response = requests.get(
                url,
                params=params,
                headers=self._get_headers(),
                timeout=self.timeout
            )
        except (Timeout, ConnectionError) as e:
            raise
        
        self._raise_for_status(response)
        return response.json()
    
    def post(
        self,
        path: str,
        data: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Make a POST request to the API.
        
        Args:
            path: The API endpoint path (e.g., "/users")
            data: Dictionary of data to send in the request body as JSON
        
        Returns:
            The JSON response as a dictionary
        
        Raises:
            APIError: If the response status code is not 2xx
            Timeout: If the request times out
            ConnectionError: If there's a connection error
        
        Example:
            >>> client = APIClient("https://api.example.com")
            >>> new_user = client.post("/users", data={"name": "Alice", "email": "alice@example.com"})
        """
        url = f"{self.base_url}{path}"
        
        try:
            response = requests.post(
                url,
                json=data,
                headers=self._get_headers(),
                timeout=self.timeout
            )
        except (Timeout, ConnectionError) as e:
            raise
        
        self._raise_for_status(response)
        return response.json()


# ============================================================================
# Usage Examples
# ============================================================================

if __name__ == "__main__":
    """
    Demonstration of APIClient usage with JSONPlaceholder API
    (a free fake REST API for testing)
    """
    
    # Initialize client without API key
    client = APIClient("https://jsonplaceholder.typicode.com")
    
    print("=" * 60)
    print("GET Request Example")
    print("=" * 60)
    
    try:
        # Fetch a single post
        post = client.get("/posts/1")
        print(f"Post ID: {post['id']}")
        print(f"Title: {post['title']}")
        print(f"Body: {post['body'][:100]}...")
    except APIError as e:
        print(f"API Error: {e}")
    except Exception as e:
        print(f"Error: {e}")
    
    print("\n" + "=" * 60)
    print("GET Request with Query Parameters")
    print("=" * 60)
    
    try:
        # Fetch posts with query parameters
        posts = client.get("/posts", params={"userId": 1, "_limit": 2})
        print(f"Found {len(posts)} posts:")
        for post in posts:
            print(f"  - {post['title']}")
    except APIError as e:
        print(f"API Error: {e}")
    except Exception as e:
        print(f"Error: {e}")
    
    print("\n" + "=" * 60)
    print("POST Request Example")
    print("=" * 60)
    
    try:
        # Create a new post
        new_post = client.post(
            "/posts",
            data={
                "title": "My New Post",
                "body": "This is the content of my new post",
                "userId": 1
            }
        )
        print(f"Created post with ID: {new_post['id']}")
        print(f"Title: {new_post['title']}")
    except APIError as e:
        print(f"API Error: {e}")
    except Exception as e:
        print(f"Error: {e}")
    
    print("\n" + "=" * 60)
    print("Error Handling Example")
    print("=" * 60)
    
    try:
        # Request non-existent resource (will get 404)
        client.get("/posts/99999")
    except APIError as e:
        print(f"Caught expected error:")
        print(f"  Status Code: {e.status_code}")
        print(f"  Message: {e.message}")
    except Exception as e:
        print(f"Error: {e}")
    
    print("\n" + "=" * 60)
    print("Client with API Key Example")
    print("=" * 60)
    
    # Initialize client with API key
    secure_client = APIClient(
        "https://api.example.com",
        api_key="my-secret-api-key"
    )
    print(f"Client initialized with base_url: {secure_client.base_url}")
    print(f"API key authentication: {'Enabled' if secure_client.api_key else 'Disabled'}")
    print(f"Headers: {secure_client._get_headers()}")
```

## Key Features Explained:

### 1. **Custom Exception Class**
```python
class APIError(Exception):
    """Stores status_code and message for detailed error handling"""
```

### 2. **Type Hints Throughout**
- All parameters and return types are annotated
- Uses `Optional`, `Dict`, and `Any` from the `typing` module

### 3. **Methods**
- `__init__`: Initializes with base_url and optional api_key
- `get()`: Makes GET requests with optional query parameters
- `post()`: Makes POST requests with JSON body
- `_get_headers()`: Private method to build headers with auth
- `_raise_for_status()`: Private method to check and raise errors

### 4. **Error Handling**
- Raises `APIError` with status code and message on non-2xx responses
- Handles JSON and plain text error responses
- Propagates network errors (Timeout, ConnectionError)

### 5. **Complete Docstrings**
- Module-level docstring
- Class docstring with description and examples
- Method docstrings with Args, Returns, Raises, and Example sections

### 6. **Usage Examples**
The script demonstrates:
- GET requests with and without parameters
- POST requests with data
- Error handling
- Client initialization with API key