This document represents the completion of Step 2 of 2 in the "API Integration Builder" workflow.
Specifically, this output addresses the projectmanager → create_project action, providing the foundational code and structure required to initiate a new project via an external API.
This deliverable provides a comprehensive, professional, and actionable code scaffold and project structure for integrating with an external API to perform a create_project operation. It covers essential aspects such as project setup, configuration management, API client implementation, error handling, and basic logging, enabling your team to quickly establish and customize this integration.
The goal is to provide a robust starting point that can be extended and adapted to your specific external API and project management system requirements.
Below, you will find the recommended project structure and initial code scaffolding designed for a typical API integration focusing on project creation.
A well-organized project structure is crucial for maintainability and scalability. We recommend the following layout:
project_api_integration/ ├── config/ │ └── settings.py # Configuration loading (API keys, endpoints) ├── src/ │ ├── api_client/ │ │ ├── __init__.py │ │ └── project_manager_api.py # Main API client for project operations │ ├── models/ │ │ ├── __init__.py │ │ └── project.py # Data models for project entities │ └── utils/ │ ├── __init__.py │ └── logger.py # Centralized logging utility ├── tests/ │ ├── __init__.py │ └── test_project_creation.py # Unit/integration tests ├── main.py # Entry point for executing the integration ├── .env.example # Example for environment variables ├── requirements.txt # Python dependencies └── README.md # Project documentation
This document provides a comprehensive, detailed, and professional output for the "generate_code" step of the "API Integration Builder" workflow. The goal is to deliver a production-ready, well-commented, and highly customizable Python template for integrating with external RESTful APIs.
Given the generic nature of the request ("API Integration Builder") without specific API details (e.g., API name, endpoints, authentication type, data models), this output focuses on providing a robust, extensible, and best-practice-driven framework. This framework will serve as a strong foundation that can be easily adapted to any specific API.
This output provides a Python-based API client template designed for reliability, maintainability, and security. It incorporates essential features required for production environments, such as:
This template is designed to be a starting point. You will need to customize it with specific API endpoints, request/response data models, and detailed authentication specifics for your target API.
Before diving into the code, understanding the underlying principles is crucial for building reliable integrations:
* Network Errors: Connection issues, timeouts.
* Client Errors (4xx): Bad requests (400), unauthorized (401), forbidden (403), not found (404), rate limited (429).
* Server Errors (5xx): Internal server errors (500), service unavailable (503).
The following Python code provides a flexible and production-ready GenericAPIClient class. It uses the popular requests library for HTTP communication.
import os
import requests
import time
import logging
from typing import Dict, Any, Optional, Union
# --- 1. Configure Logging ---
# It's recommended to configure logging at the application level.
# This setup provides a basic console logger.
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper(),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- 2. Custom Exception Classes ---
# Define custom exceptions for clearer error handling
class APIError(Exception):
"""Base exception for API-related errors."""
def __init__(self, message: str, status_code: Optional[int] = None, response_data: Optional[Any] = None):
super().__init__(message)
self.status_code = status_code
self.response_data = response_data
logger.error(f"APIError: {message} | Status: {status_code} | Response: {response_data}")
class AuthenticationError(APIError):
"""Raised for 401 Unauthorized errors."""
pass
class ForbiddenError(APIError):
"""Raised for 403 Forbidden errors."""
pass
class NotFoundError(APIError):
"""Raised for 404 Not Found errors."""
pass
class RateLimitError(APIError):
"""Raised for 429 Too Many Requests errors."""
pass
class ClientError(APIError):
"""Raised for other 4xx client errors."""
pass
class ServerError(APIError):
"""Raised for 5xx server errors."""
pass
class NetworkError(APIError):
"""Raised for network-related issues (e.g., connection errors, timeouts)."""
pass
# --- 3. Generic API Client Class ---
class GenericAPIClient:
"""
A generic and robust client for interacting with RESTful APIs.
Features:
- Configurable base URL and authentication.
- Automatic retry mechanism with exponential backoff for transient errors.
- Comprehensive error handling with custom exceptions.
- Integrated logging for requests, responses, and errors.
"""
def __init__(self,
base_url: str,
api_key: Optional[str] = None,
auth_header_name: str = "Authorization",
auth_header_prefix: str = "Bearer",
timeout: int = 30,
max_retries: int = 3,
backoff_factor: float = 0.5,
initial_delay: float = 1.0,
custom_headers: Optional[Dict[str, str]] = None):
"""
Initializes the API client.
Args:
base_url (str): The base URL of the API (e.g., "https://api.example.com/v1").
api_key (Optional[str]): The API key or token for authentication.
If None, no API key header will be sent.
auth_header_name (str): The name of the header for API key/token (default: "Authorization").
auth_header_prefix (str): Prefix for the API key/token (e.g., "Bearer", "Token"). Set to "" if no prefix.
timeout (int): Default timeout for HTTP requests in seconds.
max_retries (int): Maximum number of retries for transient errors.
backoff_factor (float): Factor for exponential backoff (delay = backoff_factor * (2 ** (retry_num - 1))).
initial_delay (float): Initial delay before the first retry attempt.
custom_headers (Optional[Dict[str, str]]): A dictionary of additional headers to send with every request.
"""
if not base_url:
raise ValueError("Base URL cannot be empty.")
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.max_retries = max_retries
self.backoff_factor = backoff_factor
self.initial_delay = initial_delay
self.session = requests.Session() # Use a session for connection pooling and header persistence
# Set default headers, including authentication
self.session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json"
})
if api_key:
auth_value = f"{auth_header_prefix} {api_key}" if auth_header_prefix else api_key
self.session.headers.update({auth_header_name: auth_value})
logger.info(f"API Client initialized with {auth_header_name} authentication.")
else:
logger.warning("API Client initialized without an API key. Ensure the target API does not require authentication or uses another method.")
if custom_headers:
self.session.headers.update(custom_headers)
logger.info(f"Custom headers added: {custom_headers}")
logger.info(f"GenericAPIClient initialized for base URL: {self.base_url}")
def _handle_response(self, response: requests.Response) -> Any:
"""
Handles the API response, raising custom exceptions for errors.
"""
try:
response_data = response.json()
except requests.exceptions.JSONDecodeError:
response_data = response.text # Fallback to text if not JSON
if response.ok: # status_code is 2xx
return response_data
else:
error_message = f"API request failed with status {response.status_code}: {response.reason}"
if response_data:
error_message += f" | Details: {response_data}"
if response.status_code == 401:
raise AuthenticationError(error_message, response.status_code, response_data)
elif response.status_code == 403:
raise ForbiddenError(error_message, response.status_code, response_data)
elif response.status_code == 404:
raise NotFoundError(error_message, response.status_code, response_data)
elif response.status_code == 429:
# Optionally, parse 'Retry-After' header here
retry_after = response.headers.get("Retry-After")
logger.warning(f"Rate limit hit. Retry-After: {retry_after} seconds.")
raise RateLimitError(error_message, response.status_code, response_data)
elif 400 <= response.status_code < 500:
raise ClientError(error_message, response.status_code, response_data)
elif 500 <= response.status_code < 600:
raise ServerError(error_message, response.status_code, response_data)
else:
raise APIError(error_message, response.status_code, response_data)
def _request(self,
method: str,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
data: Optional[Union[Dict[str, Any], str]] = None,
json: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
**kwargs) -> Any:
"""
Internal method to make an HTTP request with retry logic.
"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
logger.debug(f"Attempting {method} request to {url}")
logger.debug(f"Params: {params}, Data: {data}, JSON: {json}")
combined_headers = {**self.session.headers, **(headers or {})}
for retry_num in range(self.max_retries + 1):
try:
response = self.session.request(
method=method,
url=url,
params=params,
data=data,
json=json,
headers=combined_headers,
timeout=self.timeout,
**kwargs
)
logger.debug(f"Received response for {method} {url} with status {response.status_code}")
return self._handle_response(response)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
if retry_num < self.max_retries:
delay = self.initial_delay * (self.backoff_factor ** retry_num)
logger.warning(f"Network error during {method} {url}: {e}. Retrying in {delay:.2f} seconds (attempt {retry_num + 1}/{self.max_retries}).")
time.sleep(delay)
else:
raise NetworkError(f"Failed to connect to {url} after {self.max_retries} retries: {e}") from e
except requests.exceptions.RequestException as e:
# Catch other requests library exceptions (e.g., TooManyRedirects)
raise APIError(f"An unexpected request error occurred:
* Rename .env.example to .env.
* Edit .env and replace your_secret_api_key_here with your actual API key and https://api.yourprojectmanager.com/v1 with the correct base URL for your external Project Manager API.
* API Endpoint: Verify and adjust the endpoint = "/projects" in src/api_client/project_manager_api.py to match your external API's documentation for project creation.
* Authentication: If your API uses a different authentication method (e.g., X-API-Key header, OAuth 2.0), modify the self.headers in src/api_client/project_manager_api.py accordingly.
* Payload Fields: Update src/models/project.py (ProjectCreatePayload) to precisely match the