Gemini 2.5 Flash × api-client
1.7Class 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.7correctness 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
```