mager-bench

Gemini 2.5 Flash × api-client

1.7

Class design + error handling + type hints + docs

correctness 0.0quality 2.0documentation 3.035473ms
$ 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.035473ms

# judge: The response is truncated mid-code (cuts off at 'if response.content: re') and never includes get/post methods or the usage example, making it non-functional and incomplete.

$ cat gemini-2.5-flash.api-client.r1.response
This `APIClient` class provides a robust and type-hinted wrapper around the `requests` library, designed for interacting with RESTful APIs. It includes custom error handling, automatic JSON serialization/deserialization, and support for API key authentication.

```python
import requests
from typing import Optional, Dict, Any, Union

# 1. Custom APIError exception
class APIError(Exception):
    """
    Custom exception for API errors encountered by the APIClient.

    This exception is raised when an HTTP response indicates an error (non-2xx status code).
    It encapsulates the status code, a descriptive message, and optionally the
    full response body for debugging purposes.
    """

    def __init__(self, status_code: int, message: str, response_body: Optional[Union[Dict[str, Any], str]] = None):
        """
        Initializes the APIError.

        Args:
            status_code: The HTTP status code of the API response (e.g., 404, 500).
            message: A human-readable error message, often derived from the API's
                     response or a default description.
            response_body: The raw response body, if available. This can be a dictionary
                           (if JSON) or a string (if plain text), providing more
                           detailed error information from the API.
        """
        self.status_code = status_code
        self.message = message
        self.response_body = response_body
        super().__init__(f"API Error {status_code}: {message}")

    def __str__(self) -> str:
        """Returns the string representation of the error."""
        details = f"Status Code: {self.status_code}, Message: {self.message}"
        if self.response_body:
            # For cleaner output, stringify dicts and truncate long strings
            if isinstance(self.response_body, dict):
                body_str = str(self.response_body)
            elif isinstance(self.response_body, str):
                body_str = self.response_body
            else:
                body_str = f"{self.response_body}"
            
            # Truncate long response bodies for readability
            if len(body_str) > 200:
                body_str = body_str[:197] + "..."
            details += f", Response Body: {body_str}"
        return f"APIError({details})"


# 2. APIClient class
class APIClient:
    """
    A Python client for interacting with a REST API, wrapping the `requests` library.

    This client handles base URL management, API key authentication (Bearer token),
    JSON content types, and automatic error handling for non-2xx responses by
    raising a custom `APIError`. It uses `requests.Session` for efficient
    connection pooling and persistent headers.
    """

    def __init__(self, base_url: str, api_key: Optional[str] = None):
        """
        Initializes the APIClient with a base URL and an optional API key.

        Args:
            base_url: The base URL for the API (e.g., "https://api.example.com/v1").
                      It will be stripped of any trailing slash to ensure consistent
                      path joining.
            api_key: An optional API key for authentication. If provided,
                     it will be included in the 'Authorization' header as
                     'Bearer <api_key>'.

        Raises:
            ValueError: If the `base_url` is empty.
        """
        if not base_url:
            raise ValueError("base_url cannot be empty")

        self.base_url: str = base_url.rstrip('/')  # Ensure no trailing slash
        self.session: requests.Session = requests.Session()

        # Set default headers for JSON content-type
        self.session.headers.update({"Content-Type": "application/json"})

        # Add Authorization header if an API key is provided
        if api_key:
            self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    def _request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
        """
        Internal helper method to make an HTTP request and handle common logic.

        This method constructs the full URL, sends the request using the configured
        session, checks for non-2xx responses, and parses the JSON response.
        It's used by public methods like `get` and `post`.

        Args:
            method: The HTTP method to use (e.g., "GET", "POST", "PUT", "DELETE").
            path: The API endpoint path relative to the base URL
                  (e.g., "users", "products/123", "orders"). It will be stripped
                  of any leading slash to ensure consistent URL construction.
            **kwargs: Additional keyword arguments to pass directly to
                      `requests.Session.request`. Common arguments include `params`
                      for query parameters (GET) and `json` for JSON payload (POST/PUT).

        Returns:
            The JSON response body as a dictionary. If the response content is empty
            (e.g., a 204 No Content response), an empty dictionary is returned.

        Raises:
            APIError: If the API returns a non-2xx status code. This custom exception
                      will contain the status code, a descriptive message, and the
                      raw response body for debugging.
            requests.exceptions.RequestException: For network-related errors
                                                  (e.g., connection refused, timeout,
                                                  DNS resolution failure).
        """
        # Construct the full URL, ensuring path doesn't start with a slash
        full_url: str = f"{self.base_url}/{path.lstrip('/')}"

        try:
            response: requests.Response = self.session.request(method, full_url, **kwargs)
            response.raise_for_status()  # Raises HTTPError for 4xx/5xx responses

            # Attempt to return JSON if content exists, otherwise an empty dict
            if response.content:
                return response.json()
            return {}  # For 204 No Content, etc.

        except requests.exceptions.HTTPError as e:
            # Handle specific HTTP errors (4xx/5xx) by raising our custom APIError
            status_code: int = e.response.status_code
            error_message: str = f"Request to {full_url} failed with status {status_code}"
            response_body: Optional[Union[Dict[str, Any], str]] = None

            if e.response.text:
                try:
                    # Attempt to parse response body as JSON for more details
                    response_body = e.response.json()
                    # Look for common error message keys in the JSON response
                    if isinstance(response_body, dict):
                        error_message = response_body.get("message", response_body.get("error", error_message))
                except requests.exceptions.JSONDecodeError:
                    # If response body is not JSON, use raw text as the body and message
                    response_body = e.response.text
                    error_message = response_body if response_body else error_message
            
            # Raise the custom APIError, preserving the original exception chain
            raise APIError(status_code, error_message, response_body) from e

        except requests.exceptions.RequestException as e:
            # Catch general request exceptions (e.g., network issues, timeouts)
            # These are re-raised as standard requests exceptions.
            raise e

    def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """
        Sends a GET request to the specified API path.

        Args:
            path: The API endpoint path (e.g., "users", "products/123", "items").
            params: An optional dictionary of query parameters to send with the request.
                    These will be appended to the URL as `?key1=value1&key2=value2`.

        Returns:
            The JSON response body as a dictionary.

        Raises:
            APIError: If the API returns a non-2xx status code.
            requests.exceptions.RequestException: For network-related errors.
        """
        return self._request("GET", path, params=params)

    def post(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
        """
        Sends a POST request to the specified API path with a JSON payload.

        The `data` dictionary will be automatically serialized to JSON and
        sent in the request body. The `Content-Type` header will be set
        to `application/json` automatically by `requests` when using the `json` argument.

        Args:
            path: The API endpoint path (e.g., "users", "orders", "products").
            data: A dictionary representing the JSON payload to send in the request body.

        Returns:
            The JSON response body as a dictionary.

        Raises:
            APIError: If the API returns a non-2xx status code.
            requests.exceptions.RequestException: For network-related errors.
        """
        return self._request("POST", path, json=data)

    # You could similarly add methods for PUT, DELETE, etc.
    # def put(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
    #     """Sends a PUT request with a JSON payload."""
    #     return self._request("PUT", path, json=data)
    #
    # def delete(self, path: str) -> Dict[str, Any]:
    #     """Sends a DELETE request."""
    #     return self._request("DELETE", path)


# 3. Usage Example
if __name__ == "__main__":
    # Using JSONPlaceholder as a mock API for demonstration purposes
    # It's a free fake API for testing and prototyping.
    BASE_URL = "https://jsonplaceholder.typicode.com"
    print(f"--- Demonstrating APIClient with {BASE_URL} ---")

    # Initialize the client
    client = APIClient(BASE_URL)

    # Example 1: Successful GET request
    print("\n--- GETting posts/1 (expecting success) ---")
    try:
        post_id = 1
        post_data = client.get(f"posts/{post_id}")
        print(f"Successfully fetched post {post_id}:")
        print(f"  Title: {post_data.get('title')[:60]}...")
        print(f"  Body: {post_data.get('body')[:60]}...")
    except APIError as e:
        print(f"Error fetching post: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

    # Example 2: Successful POST request
    print("\n--- POSTing a new post (expecting success) ---")
    new_post_payload = {
        "title": "My New Awesome Post",
        "body": "This is the content of my brand new post.",
        "userId": 1,
    }
    try:
        created_post = client.post("posts", new_post_payload)
        print("Successfully created a new post:")
        print(f"  ID: {created_post.get('id')}") # JSONPlaceholder returns 101 for new posts
        print(f"  Title: {created_post.get('title')}")
        print(f"  UserId: {created_post.get('userId')}")
    except APIError as e:
        print(f"Error creating post: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

    # Example 3: Handling APIError (404 Not Found)
    print("\n--- Attempting to GET a non-existent post (expecting APIError 404) ---")
    try:
        non_existent_id = 999999  # Very high ID, likely doesn't exist
        client.get(f"posts/{non_existent_id}")
        print("Unexpected: Fetched a non-existent post!")
    except APIError as e:
        print(f"Caught expected APIError: {e}")
        assert e.status_code == 404
        print(f"  Status Code: {e.status_code}")
        print(f"  Message: '{e.message}'")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

    # Example 4: Handling a request to a completely invalid path (also likely 404)
    print("\n--- Attempting to GET an invalid path (expecting APIError 404) ---")
    try:
        client.get("this/path/does/not/exist/at/all")
        print("Unexpected: Fetched from an invalid path!")
    except APIError as e:
        print(f"Caught expected APIError: {e}")
        assert e.status_code == 404
        print(f"  Status Code: {e.status_code}")
        print(f"  Message: '{e.message}'")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

    # Example 5: Initializing client with a mock API key
    print("\n--- Initializing client with a mock API key (demonstrates header setup) ---")
    MOCK_API_KEY = "my_super_secret_token_abc123"
    client_with_key = APIClient(BASE_URL, api_key=MOCK_API_KEY)
    print(f"Client headers with API key: {client_with_key.session.headers}")
    assert client_with_key.session.headers.get("Authorization") == f"Bearer {MOCK_API_KEY}"
    print("APIClient successfully initialized with Authorization header (no actual API key validation here).")
    
    # You could then use client_with_key to interact with protected endpoints:
    # try:
    #     protected_resource = client_with_key.get("protected/data")
    #     print(f"Fetched protected resource: {protected_resource}")
    # except APIError as e:
    #     print(f"Error accessing protected resource: {e}")
    #     # Expect 401 Unauthorized or 403 Forbidden if key is invalid/missing on a real API
```