Gemini 2.5 Flash × api-client
5.0Class design + error handling + type hints + docs
correctness 3.0quality 6.0documentation 6.019050ms
$ 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
5.0correctness 3.0quality 6.0documentation 6.019050ms
# judge: Core class logic is solid and well-typed, but the usage example is cut off mid-string leaving the code non-runnable and incomplete.
$ cat gemini-2.5-flash.api-client.r1.response
This `APIClient` class provides a structured and robust way to interact with a REST API using the `requests` library. It includes error handling, type hints, and comprehensive documentation as requested.
```python
import requests
from typing import Any, Dict, Optional, Union
class APIError(Exception):
"""
Custom exception for API errors, encapsulating the HTTP status code
and an error message.
"""
def __init__(self, status_code: int, message: str) -> None:
"""
Initializes the APIError with a status code and message.
Args:
status_code: The HTTP status code of the error.
message: A descriptive error message.
"""
super().__init__(f"API Error {status_code}: {message}")
self.status_code = status_code
self.message = message
class APIClient:
"""
A client for interacting with a REST API, wrapping the requests library.
This class handles base URL construction, API key authentication,
and raises custom exceptions for non-2xx HTTP responses.
"""
def __init__(self, base_url: str, api_key: Optional[str] = None) -> None:
"""
Initializes the APIClient with a base URL and an optional API key.
Args:
base_url: The base URL of the API (e.g., "https://api.example.com/v1/").
It will be ensured to end with a '/'.
api_key: An optional API key for authentication. If provided, it will
be sent as an 'Authorization: Bearer {api_key}' header.
"""
if not base_url.endswith('/'):
base_url += '/'
self.base_url = base_url
self.session = requests.Session()
if api_key:
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json" # Common for POST/PUT requests
})
else:
self.session.headers.update({
"Content-Type": "application/json"
})
def _get_full_url(self, path: str) -> str:
"""
Constructs the full URL by joining the base_url and the given path.
Args:
path: The relative path for the API endpoint (e.g., "users" or "products/123").
Returns:
The complete URL for the API endpoint.
"""
# urljoin handles base_url ending with '/' and path starting with '/' correctly
return requests.compat.urljoin(self.base_url, path)
def _handle_response(self, response: requests.Response) -> Any:
"""
Handles the API response, checking for errors and parsing JSON.
If the response status code is not in the 2xx range, an APIError is raised.
Otherwise, it attempts to parse the response body as JSON.
Args:
response: The requests.Response object received from the API call.
Returns:
The parsed JSON response body, or the raw text if JSON decoding fails
for a successful response.
Raises:
APIError: If the response status code is not in the 2xx range.
"""
if not response.ok: # Checks if status_code is not 2xx
status_code = response.status_code
try:
# Attempt to get a more specific error message from JSON response
error_data = response.json()
message = error_data.get('message', error_data.get('error', response.text))
if isinstance(message, dict): # Sometimes message itself is a dict
message = str(message)
except requests.exceptions.JSONDecodeError:
# If response is not JSON, use the raw text
message = response.text
raise APIError(status_code, message)
try:
return response.json()
except requests.exceptions.JSONDecodeError:
# If a successful response is not JSON, return its text content
return response.text
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
"""
Sends a GET request to the API.
Args:
path: The relative path to the API endpoint (e.g., "users" or "products/123").
params: An optional dictionary of query parameters to send with the request.
Returns:
The parsed JSON response from the API, or raw text if not JSON.
Raises:
APIError: If the API returns a non-2xx status code.
requests.exceptions.RequestException: For network-related errors
(e.g., connection refused, timeout).
"""
url = self._get_full_url(path)
response = self.session.get(url, params=params)
return self._handle_response(response)
def post(self, path: str, data: Dict[str, Any]) -> Any:
"""
Sends a POST request to the API.
Args:
path: The relative path to the API endpoint (e.g., "users" or "orders").
data: A dictionary representing the JSON payload to send in the request body.
Returns:
The parsed JSON response from the API, or raw text if not JSON.
Raises:
APIError: If the API returns a non-2xx status code.
requests.exceptions.RequestException: For network-related errors.
"""
url = self._get_full_url(path)
response = self.session.post(url, json=data) # json=data automatically sets Content-Type to application/json
return self._handle_response(response)
# --- Usage Example ---
if __name__ == "__main__":
# Using JSONPlaceholder for demonstration purposes
# It's a fake online REST API for testing and prototyping.
# No actual API key is needed for JSONPlaceholder, but we'll pass one to demonstrate.
BASE_URL = "https://jsonplaceholder.typicode.com/"
FAKE_API_KEY = "my_secret_api_key_12345"
print("--- Initializing API Client ---")
client = APIClient(BASE_URL, api_key=FAKE_API_KEY)
# --- GET Request Example ---
print("\n--- Testing GET request (successful) ---")
try:
# Get a list of posts
posts = client.get("posts", params={"_limit": 3})
print(f"Successfully fetched {len(posts)} posts:")
for post in posts:
print(f" ID: {post['id']}, Title: {post['title'][:50]}...")
except APIError as e:
print(f"GET failed with APIError: {e.status_code} - {e.message}")
except requests.exceptions.RequestException as e:
print(f"GET failed with network error: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# Get a single post
print("\n--- Testing GET request for single item (successful) ---")
try:
single_post = client.get("posts/1")
print(f"Successfully fetched post 1: {single_post['title']}")
except APIError as e:
print(f"GET failed with APIError: {e.status_code} - {e.message}")
except requests.exceptions.RequestException as e:
print(f"GET failed with network error: {e}")
# --- GET Request Example (Error) ---
print("\n--- Testing GET request (simulated error - non-existent endpoint) ---")
try:
# This path doesn't exist, will likely return a 404
non_existent_resource = client.get("non_existent_path_12345")
print("Fetched non-existent resource (this should not happen in error test).")
except APIError as e:
print(f"Caught expected APIError: {e.status_code} - {e.message}")
except requests.exceptions.RequestException as e:
print(f"GET failed with network error: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# --- POST Request Example ---
print("\n--- Testing POST request (successful) ---")
new_post_data = {
"title": "My New Awesome Post",
"body": "This is the content of my new post, sent via the API client.",
"userId": 1
}
try:
created_post = client.post("posts", data=new_post_data)
print("Successfully created a new post:")
print(f" ID: {created_post.get('id')}")
print(f" Title: {created_post.get('title')}")
print(f" Body: {created_post.get('body')}")
print(f" UserID: {created_post.get('userId')}")
except APIError as e:
print(f"POST failed with APIError: {e.status_code} - {e.message}")
except requests.exceptions.RequestException as e:
print(f"POST failed with network error: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# --- POST Request Example (Simulated Error - bad data or endpoint) ---
# JSONPlaceholder tends to return 201 for POST even with "bad" data as it's mocked.
# To truly simulate an error, we'd need an actual API that validates input.
# For demonstration purposes, let's try a POST to a path that might not accept POST.
print("\n--- Testing POST request (simulated error - non-existent/invalid POST endpoint) ---")
try:
# Trying to POST to a single resource ID, which might not be supported or
# could return an error on a real API if not a PUT/PATCH.
# JSONPlaceholder will still return 201 for this, so this won't actually fail here.
# A real API would return e.g. 405 Method Not Allowed or 400 Bad Request.
bad_post_data = {"invalid_field": "some_value"}
response_for_bad_post = client.post("comments/1", data=bad_post_data)
print("Response for 'bad' POST (might be successful on mock API):")
print(response_for_bad_post)
except APIError as e:
print(f"Caught expected APIError: {e.status_code} - {e.message}")
except requests.exceptions.RequestException as e:
print(f"POST failed with network error: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
```