This deliverable provides a comprehensive, production-ready Python codebase designed to facilitate robust and secure integration with external RESTful APIs. This output serves as the foundational step (Step 1 of 2) in your "API Integration Builder" workflow, offering a modular, extensible, and well-documented solution that can be directly adapted to your specific API requirements.
Integrating with external APIs is a critical component of modern software systems, enabling data exchange, service consumption, and extended functionality. This document presents a pre-built Python client designed with best practices in mind, focusing on:
The generated code provides a flexible framework that can be customized for virtually any REST API, demonstrating common patterns for GET, POST, PUT, and DELETE operations, along with two popular authentication methods: API Key and OAuth 2.0 Client Credentials.
The generated codebase adheres to the following principles to ensure a high-quality integration solution:
config.py) or environment variables, enhancing security and flexibility.ApiClient class encapsulates all HTTP communication logic, abstracting away the underlying requests library and providing a cleaner interface for API interactions.This section provides the Python code, organized into three files: config.py, api_client.py, and example_usage.py.
config.py - Configuration ManagementThis file centralizes all API-related configuration, making it easy to manage and update credentials or endpoints. For production environments, it is highly recommended to use environment variables for sensitive data.
#### 3.2 `api_client.py` - The Core API Client This file contains the `ApiClient` class, which handles all communication with the external API, including authentication, request construction, error handling, and retries.
python
import requests
import time
import logging
from functools import wraps
from typing import Dict, Any, Optional, Tuple, Callable
import config
logging.basicConfig(level=getattr(logging, config.LOG_LEVEL.upper(), logging.INFO),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class ApiException(Exception):
"""Custom exception for API 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
def __str__(self):
return (f"API Error: {self.message} (Status: {self.status_code}, "
f"Response: {self.response_data})" if self.status_code else self.message)
def retry_on_exception(max_retries: int, initial_delay: float):
"""
Decorator for retrying a function call upon specific exceptions or HTTP status codes.
Uses exponential backoff.
"""
def decorator(func: Callable):
@wraps(func)
def wrapper(self, args, *kwargs):
retries = 0
delay = initial_delay
while retries <= max_retries:
try:
return func(self, args, *kwargs)
except (requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.RequestException) as e:
logger.warning(f"Network or request error during {func.__name__}: {e}. Retrying in {delay:.2f}s...")
retries += 1
if retries > max_retries:
logger.error(f"Max retries ({max_retries}) exceeded for {func.__name__}.")
raise ApiException(f"Failed after {max_retries} retries due to network/request error.") from e
time.sleep(delay)
delay *= 2 # Exponential backoff
except ApiException as e:
# Retry specifically on server errors (5xx) or rate limits (429)
if e.status_code and (500 <= e.status_code < 600 or e.status_code == 429):
logger.warning(f"API error {e.status_code} during {func.__name__}: {e.message}. Retrying in {delay:.2f}s...")
retries += 1
if retries > max_retries:
logger.error(f"Max retries ({max_retries}) exceeded for {func.__name__}.")
raise e
time.sleep(delay)
delay *= 2
else:
raise e # Re-raise other API errors immediately
return wrapper
return decorator
class ApiClient:
"""
A robust client for interacting with an external RESTful API.
Supports API Key and OAuth 2.0 Client Credentials authentication.
"""
def __init__(self,
base_url: str = config.API_BASE_URL,
api_key: Optional[str] = config.API_KEY,
api_key_header_name: Optional[str] = config.API_KEY_HEADER_NAME,
oauth_token_url: Optional[str] = config.OAUTH_TOKEN_URL,
oauth_client_id: Optional[str] = config.OAUTH_CLIENT_ID,
oauth_client_secret: Optional[str] = config.OAUTH_CLIENT_SECRET,
oauth_scopes: Optional[list] = config.OAUTH_SCOPES,
timeout: float = config.REQUEST_TIMEOUT_SECONDS):
"""
Initializes the API client.
:param base_url: The base URL of the API.
:param api_key: The API key for authentication (if used).
:param api_key_header_name: The header name for the API key (e.g., 'X-API-KEY').
:param oauth_token_url: The URL for obtaining OAuth tokens (if used).
:param oauth_client_id: The client ID for OAuth (if used).
:param oauth_client_secret: The client secret for OAuth (if used).
:param oauth_scopes: A list of scopes for OAuth (if used).
:param timeout: Default request timeout in seconds.
"""
self.base_url = base_url
self.timeout = timeout
self._session = requests.Session()
# Authentication specific attributes
self.api_key = api_key
self.api_key_header_name = api_key_header_name
self.oauth_token_url = oauth_token_url
self.oauth_client_id = oauth_client_id
self.oauth_client_secret = oauth_client_secret
self.oauth_scopes = oauth_scopes
self._access_token = None
self._token_expires_at = 0 # Unix timestamp
# Determine authentication method based on provided credentials
self.auth_method = self._determine_auth_method()
logger.info(f"API Client initialized. Base URL: {self.base_url}, Auth method: {self.auth_method}")
def _determine_auth_method(self) -> str:
"""Determines the authentication method based on provided config."""
if self.oauth_client_id and self.oauth_client_secret and self.oauth_token_url:
return "oauth2_client_credentials"
elif self.api_key and self.api_key_header_name:
return "api_key"
else:
logger.warning("No valid authentication method configured. Proceeding without authentication.")
return "none"
def _get_api_key_headers(self) -> Dict[str, str]:
"""Returns headers for API Key authentication."""
if self.api_key and self.api_key_header_name:
return {self.api_key_header_name: self.api_key}
return {}
def _get_oauth_token(self) -> str:
"""Obtains an OAuth 2.0 access token using client credentials flow."""
if not (self.oauth_token_url and self.oauth_client_id and self.oauth_client_secret):
raise ApiException("OAuth 2.0 Client Credentials not fully configured.")
# Check if token is still valid
if self._access_token and self._token_expires_at > time.time() + 60: # Refresh 60 seconds before actual expiry
return self._access_token
logger.info("Obtaining new OAuth 2.0 access token...")
token_data = {
"grant_type": "client_credentials",
"client_id": self.oauth_client_id,
"client_secret": self.oauth_client_secret,
"scope": " ".join(self.oauth_scopes) if self.oauth_scopes else ""
}
try:
response = requests.post(self.oauth_token_url, data=token_data, timeout=self.timeout)
response.raise_for_status()
token_info = response.json()
self._access_token = token_info.get("access_token")
expires_in = token_info.get("expires_in", 3600) # Default to 1 hour if not provided
self._token_expires_at = time.time() + expires_in
if not self._access_token:
raise ApiException("Access token not found in OAuth response.")
logger.info("Successfully obtained new OAuth 2.0 access token.")
return self._access_token
except requests.exceptions.RequestException as e:
logger.error(f"Failed to obtain OAuth token: {e}")
raise ApiException(f"Could not obtain OAuth token: {e}") from e
except Exception as e:
logger.error(f"Unexpected error during OAuth token acquisition: {e}")
raise ApiException(f"Unexpected error during OAuth token acquisition: {e}") from e
def _get_headers(self) -> Dict[str, str]:
"""Constructs the full set of headers for an API request."""
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if self.auth_method == "api_key":
headers.update(self._get_api_key_headers())
elif self.auth_method == "oauth2_client_credentials":
access_token = self._get_oauth_token()
headers["Authorization"] = f"Bearer {access_token}"
return headers
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
"""
Internal method to make an HTTP request to
projectmanager → create_projectThis document details the implementation guide for integrating with an external Project Management API to execute the create_project function. This deliverable is Step 2 of 2 in your "API Integration Builder" workflow, providing the actionable code and instructions necessary to establish this specific API connection.
The API Integration Builder is designed to streamline the process of connecting your systems with external APIs. For this specific workflow, we are focusing on generating the necessary code and guidance to interact with a Project Management System's API, enabling programmatic project creation. This integration will allow your applications to automatically provision new projects within your chosen project management platform, enhancing automation and efficiency.
create_projectThe primary objective of this step is to provide a robust and ready-to-use solution for calling the create_project endpoint of a Project Management API. This will enable your system to:
create_project IntegrationTo successfully create a project via API, the following core components are essential:
create_project request will be sent. This is typically a POST request.This guide provides a step-by-step approach to integrate the create_project functionality. We will use a generic RESTful API example, which can be adapted to specific Project Management platforms like Jira, Asana, Trello, or custom solutions.
Before proceeding, ensure you have:
Identify the base URL and the specific endpoint for creating a project. Define the necessary HTTP headers, including authentication and content type.
import os
import requests
import json
# --- Configuration (replace with your actual values) ---
PM_API_BASE_URL = os.getenv("PM_API_BASE_URL", "https://api.example-pm.com/v1")
PM_API_KEY = os.getenv("PM_API_KEY", "YOUR_SECURE_API_KEY") # Or a Bearer token
CREATE_PROJECT_ENDPOINT = f"{PM_API_BASE_URL}/projects" # Common RESTful pattern
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {PM_API_KEY}" # Example: Bearer token authentication
# For API Key in header: "X-API-Key": PM_API_KEY
# For Basic Auth: requests.auth.HTTPBasicAuth(username, password)
}
Based on the API documentation, define the payload (JSON object) containing the data for the new project.
name).description, ownerId, startDate).
def create_project_payload(project_name: str, description: str = "", owner_id: str = None, start_date: str = None, end_date: str = None, status: str = "Active"):
"""
Constructs the JSON payload for creating a new project.
Adjust parameters based on your specific PM API documentation.
"""
payload = {
"name": project_name,
"description": description,
"status": status,
# Add other required/optional fields as per API documentation
}
if owner_id:
payload["ownerId"] = owner_id
if start_date:
payload["startDate"] = start_date # Format: "YYYY-MM-DD"
if end_date:
payload["endDate"] = end_date # Format: "YYYY-MM-DD"
return payload
# --- Example Usage ---
project_data = create_project_payload(
project_name="New Client Onboarding - Project X",
description="Automated project for onboarding client X's new integration.",
owner_id="user123",
start_date="2023-10-26",
status="Planning"
)
Use an HTTP client library (e.g., requests in Python, fetch in JavaScript) to send the POST request with the constructed payload and headers.
def execute_create_project(payload: dict):
"""
Sends the POST request to the PM API to create a project.
"""
try:
print(f"Attempting to create project: {payload.get('name')}...")
response = requests.post(CREATE_PROJECT_ENDPOINT, headers=HEADERS, json=payload)
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
return response.json()
except requests.exceptions.HTTPError as http_err:
print(f"HTTP error occurred: {http_err}")
print(f"Response content: {response.text}")
return None
except requests.exceptions.ConnectionError as conn_err:
print(f"Connection error occurred: {conn_err}")
return None
except requests.exceptions.Timeout as timeout_err:
print(f"Timeout error occurred: {timeout_err}")
return None
except requests.exceptions.RequestException as req_err:
print(f"An unexpected error occurred: {req_err}")
return None
# --- Execute the call ---
# new_project_response = execute_create_project(project_data)
Parse the API response to confirm project creation and extract any relevant information, such as the new project's ID.
# Assuming new_project_response holds the successful JSON response
# if new_project_response:
# print("\nProject created successfully!")
# project_id = new_project_response.get("id") # Common field for new resource ID
# project_url = new_project_response.get("url") # Link to the new project
# print(f"New Project ID: {project_id}")
# print(f"View Project at: {project_url}")
# else:
# print("\nFailed to create project.")
Robust error handling is critical for production systems.
This consolidated example provides a functional script for creating a project.
import os
import requests
import json
import logging
# --- Configure Logging ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- Configuration (Load from Environment Variables for Security) ---
PM_API_BASE_URL = os.getenv("PM_API_BASE_URL", "https://api.example-pm.com/v1")
PM_API_KEY = os.getenv("PM_API_KEY", "YOUR_SECURE_API_KEY") # Replace with your actual API Key or Bearer Token
# Ensure API key is set
if PM_API_KEY == "YOUR_SECURE_API_KEY" or not PM_API_KEY:
logging.error("PM_API_KEY environment variable not set. Please configure it.")
exit(1)
CREATE_PROJECT_ENDPOINT = f"{PM_API_BASE_URL}/projects" # Common RESTful pattern
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {PM_API_KEY}" # Example: Bearer token authentication
# Adjust authentication header based on your PM API documentation
# e.g., "X-API-Key": PM_API_KEY, or use `auth` parameter in requests.post for Basic Auth
}
def create_project_payload(project_name: str, description: str = "", owner_id: str = None, start_date: str = None, end_date: str = None, status: str = "Active"):
"""
Constructs the JSON payload for creating a new project.
Adjust parameters based on your specific PM API documentation.
"""
payload = {
"name": project_name,
"description": description,
"status": status,
}
if owner_id:
payload["ownerId"] = owner_id
if start_date:
payload["startDate"] = start_date # Format: "YYYY-MM-DD"
if end_date:
payload["endDate"] = end_date # Format: "YYYY-MM-DD"
return payload
def execute_create_project(payload: dict):
"""
Sends the POST request to the PM API to create a project.
"""
try:
logging.info(f"Attempting to create project: {payload.get('name')}...")
response = requests.post(CREATE_PROJECT_ENDPOINT, headers=HEADERS, json=payload)
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
logging.info(f"Project '{payload.get('name')}' created successfully!")
return response.json()
except requests.exceptions.HTTPError as http_err:
logging.error(f"HTTP error occurred: {http_err} for project '{payload.get('name')}'")
logging.error(f"Response status code: {response.status_code}")
logging.error(f"Response content: {response.text}")
except requests.exceptions.ConnectionError as conn_err:
logging.error(f"Connection error occurred: {conn_err} while connecting to {CREATE_PROJECT_ENDPOINT}")
except requests.exceptions.Timeout as timeout_err:
logging.error(f"Timeout error occurred: {timeout_err} while creating project '{payload.get('name')}'")
except requests.exceptions.RequestException as req_err:
logging.error(f"An unexpected error occurred: {req_err}")
return None
if __name__ == "__main__":
# --- Example Project Data ---
example_project = create_project_payload(
project_name="Marketing Campaign Q4 2023",
description="Plan and execute marketing activities for Q4 product launch.",
owner_id="marketing_lead_id_abc", # Replace with an actual user ID from your PM system
start_date="2023-10-01",
end_date="2023-12-31",
status="Planning"
)
# --- Execute the project creation ---
new_project_details = execute_create_project(example_project)
if new_project_details:
logging.info("\n--- Project Creation Summary ---")
logging.info(f"Project Name: {new_project_details.get('name', 'N/A')}")
logging.info(f"Project ID: {new_project_details.get('id', 'N/A')}")
logging.info(f"Status: {new_project_details.get('status', 'N/A')}")
logging.info(f"Link: {new_project_details.get('url', 'Not provided')}") # Many APIs return a direct link
logging.info("------------------------------")
else:
logging.error("Failed to retrieve details for the newly created project.")
create_project_payload function with various inputs to ensure the JSON structure is correct.execute_create_project function in a test environment (or with mock API responses) to verify the entire flow.PM_API_BASE_URL, PM_API_KEY, CREATE_PROJECT_ENDPOINT, HEADERS, and the create_project_payload function parameters to match your specific Project Management API documentation.PM_API_KEY (or other credentials) are loaded from secure environment variables or a secret management service, not hardcoded\n