š” 2.1 Construct a REST API Request
What is REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD operations (Create, Read, Update, Delete) on resources.
Components of a REST API Request
1. HTTP Methods
| Method | Purpose | Example Use Case |
|---|---|---|
| GET | Retrieve data from server | Get list of devices, retrieve user info |
| POST | Create new resource | Create new user, add device to network |
| PUT | Update/replace entire resource | Replace entire configuration |
| PATCH | Partially update resource | Update specific fields of a device |
| DELETE | Remove resource | Delete device, remove user account |
2. URL/Endpoint Structure
https://api.example.com"] B -->|API Version| B1["Version Number
v1, v2, etc."] C -->|Resource Type| C1["Collection Name
devices, users, products"] D -->|Resource ID| D1["Specific Item
UUID or numeric ID"] style A fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style B fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff style C fill:#339af0,stroke:#1864ab,stroke-width:2px,color:#fff style D fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,color:#fff style A1 fill:#e7f3ff,stroke:#667eea,stroke-width:2px style B1 fill:#e6fcef,stroke:#51cf66,stroke-width:2px style C1 fill:#d9f1ff,stroke:#339af0,stroke-width:2px style D1 fill:#ffe0e0,stroke:#ff6b6b,stroke-width:2px
https://api.example.com/v1/devices/12345
Breaking it down:
š Base URL: https://api.example.com
š Version: /v1
š¦ Resource: /devices
š ID: /12345
3. Headers (Python Script Format)
Headers provide metadata about the request. In Python, headers are sent as a dictionary:
#!/usr/bin/env python3
"""
API Headers Configuration Example
Demonstrates how to create and use headers in API requests
"""
import requests
# ==============================================================================
# METHOD 1: Bearer Token Authentication (OAuth 2.0)
# ==============================================================================
# Create headers dictionary with Bearer token
headers_bearer = {
"Content-Type": "application/json", # Sending JSON data to API
"Accept": "application/json", # Expecting JSON response
"Authorization": "Bearer <YOUR_JWT_TOKEN_HERE>"
# Replace <YOUR_JWT_TOKEN_HERE> with your actual token
# Format must be: "Bearer <token>"
}
# ==============================================================================
# METHOD 2: API Key Authentication
# ==============================================================================
headers_api_key = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Key": "<YOUR_API_KEY>"
# Replace <YOUR_API_KEY> with your API key
# Header name varies by API: X-API-Key, Api-Key, apikey, etc.
}
# ==============================================================================
# USAGE EXAMPLE: Making API Request with Headers
# ==============================================================================
def make_api_request():
"""
Make GET request with authentication headers
"""
url = "https://api.example.com/v1/devices" # API endpoint URL
try:
# Perform GET request (swap headers if needed)
response = requests.get(url, headers=headers_bearer)
# Raises exception if response is 4xx/5xx
response.raise_for_status()
# Print response details
print(f"ā
Status Code: {response.status_code}")
print(f"š Response Headers: {dict(response.headers)}")
print("š¦ Response Data:")
print(response.json())
return response.json()
except requests.exceptions.HTTPError as err:
print(f"ā HTTP Error: {err}")
except requests.exceptions.ConnectionError:
print("ā Connection Error: Could not connect to server")
except requests.exceptions.Timeout:
print("ā±ļø Timeout Error: Request timed out")
except Exception as err:
print(f"ā ļø Unexpected Error: {err}")
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
make_api_request()
4. Request Body (Python Script Format)
Request body contains the data you're sending to the server (for POST, PUT, PATCH):
#!/usr/bin/env python3
"""
Request Body Configuration Example
Demonstrates creating and sending JSON payloads in API requests
"""
import requests
# ==============================================================================
# METHOD 1: Simple Payload (Direct Dictionary)
# ==============================================================================
# Create request body as Python dictionary
payload = {
"name": "Router-01", # Device hostname
"ip_address": "192.168.1.1", # Device IP address
"status": "active", # Device status
"location": "Building A" # Physical location
}
# ==============================================================================
# METHOD 2: Nested Payload (Complex Data)
# ==============================================================================
payload_nested = {
"device": {
"name": "Switch-05",
"type": "switch",
"interfaces": [
{"name": "GigE0/0", "ip": "10.0.0.1"},
{"name": "GigE0/1", "ip": "10.0.0.2"}
]
},
"metadata": {
"created_by": "admin",
"tags": ["production", "core"]
}
}
# ==============================================================================
# USAGE EXAMPLE: Sending Request Body in POST Request
# ==============================================================================
def create_device(payload):
"""
Send POST request with JSON payload
"""
url = "https://api.example.com/v1/devices"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer <YOUR_TOKEN>"
}
try:
# Send POST request (json param auto-converts dict to JSON)
response = requests.post(url, json=payload, headers=headers)
# Raises exception if response is 4xx/5xx
response.raise_for_status()
# Print response
print(f"ā
Status Code: {response.status_code}")
print("š¦ Created Device:")
print(response.json())
return response.json()
except requests.exceptions.HTTPError as err:
print(f"ā HTTP Error: {err}")
except requests.exceptions.ConnectionError:
print("ā Connection Error: Could not connect to server")
except requests.exceptions.Timeout:
print("ā±ļø Timeout Error: Request timed out")
except Exception as err:
print(f"ā ļø Unexpected Error: {err}")
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
create_device(payload)
Practical Examples
Example 1: GET Request (Python Script Format)
#!/usr/bin/env python3
"""
GET Request Example
Retrieves list of devices from REST API
"""
import requests
# ==============================================================================
# CONFIGURATION
# ==============================================================================
API_URL = "https://api.example.com/v1/devices"
API_TOKEN = "<YOUR_JWT_TOKEN_HERE>"
# ==============================================================================
# GET REQUEST FUNCTION
# ==============================================================================
def get_devices():
"""
Retrieve all devices from API
Returns:
list: List of device dictionaries or None if failed
"""
# Prepare headers
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {API_TOKEN}"
}
try:
# Make GET request (retrieves data, doesn't modify)
response = requests.get(API_URL, headers=headers, timeout=10)
# Check if successful
response.raise_for_status()
# Parse and return JSON response
devices = response.json()
print(f"ā
Successfully retrieved {len(devices)} devices")
return devices
except requests.exceptions.HTTPError as err:
print(f"ā HTTP Error: {err}")
except requests.exceptions.ConnectionError:
print("ā Connection Error: Check network/URL")
except requests.exceptions.Timeout:
print("ā±ļø Timeout Error: Request timed out")
except Exception as err:
print(f"ā ļø Unexpected Error: {err}")
return None
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
print("š Fetching devices from API...\n")
devices = get_devices()
if devices:
print(f"\nš Retrieved {len(devices)} devices")
for device in devices:
print(f" - {device.get('name', 'N/A')}: {device.get('ip_address', 'N/A')}")
else:
print("ā Failed to retrieve devices")
Example 2: POST Request with Python
#!/usr/bin/env python3
"""
POST Request Example - Create New Device
Demonstrates creating a resource via REST API
"""
import requests
# ==============================================================================
# CONFIGURATION
# ==============================================================================
url = "https://api.example.com/v1/devices" # API endpoint
headers = {
"Content-Type": "application/json", # Sending JSON data
"Authorization": "Bearer <YOUR_TOKEN>" # Your auth token
}
payload = {
"name": "Switch-05", # Device hostname
"ip_address": "10.1.1.50", # Device IP
"type": "switch" # Device type
}
# ==============================================================================
# MAKE POST REQUEST
# ==============================================================================
# Send POST request (json param converts dict to JSON automatically)
response = requests.post(url, json=payload, headers=headers)
# Check status code
print(f"Status Code: {response.status_code}") # 201=Created, 200=OK
# Display response body
print(f"Response: {response.json()}") # Created device details
Example 3: PATCH Request with JavaScript
#!/usr/bin/env node
/**
* PATCH Request Example - Update Device
* Demonstrates partial update via REST API using JavaScript fetch
*/
// ==============================================================================
// CONFIGURATION
// ==============================================================================
const API_URL = 'https://api.example.com/v1/devices/12345'; // Device ID in URL
const API_TOKEN = '<YOUR_JWT_TOKEN_HERE>';
// ==============================================================================
// PATCH REQUEST FUNCTION
// ==============================================================================
const updateDevice = async () => {
try {
// Make PATCH request (partial update - only changed fields)
const response = await fetch(API_URL, {
method: 'PATCH', // HTTP method for partial update
headers: {
'Content-Type': 'application/json', // Sending JSON data
'Authorization': `Bearer ${API_TOKEN}` // Auth token
},
body: JSON.stringify({
status: 'inactive', // Only update status field
location: 'Building B' // Only update location field
})
});
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Parse JSON response
const data = await response.json();
// Display results
console.log(`ā
Status: ${response.status}`);
console.log('š¦ Updated Device:', data);
return data;
} catch (error) {
console.error('ā Error:', error.message);
throw error;
}
};
// ==============================================================================
// MAIN EXECUTION
// ==============================================================================
updateDevice()
.then(device => console.log('⨠Update complete!', device))
.catch(err => console.error('ā ļø Update failed:', err));
Query Parameters (Python Script Format)
Filter, sort, or paginate results using query parameters:
#!/usr/bin/env python3
"""
Query Parameters Examples
Demonstrates filtering, sorting, and pagination in API requests
"""
import requests
# ==============================================================================
# CONFIGURATION
# ==============================================================================
BASE_URL = "https://api.example.com/v1/devices"
HEADERS = {
"Authorization": "Bearer <YOUR_TOKEN>",
"Accept": "application/json"
}
# ==============================================================================
# Example 1: Filter by Status
# ==============================================================================
params = {"status": "active"} # Only active devices
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}") # Shows: ?status=active
print(f"Found: {len(response.json())} devices\n")
# ==============================================================================
# Example 2: Pagination
# ==============================================================================
params = {
"page": 2, # Get page 2
"limit": 50 # 50 items per page
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}") # Shows: ?page=2&limit=50
print(f"Page 2: {len(response.json())} devices\n")
# ==============================================================================
# Example 3: Sorting
# ==============================================================================
params = {
"sort": "name", # Sort by name field
"order": "asc" # Ascending order
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}") # Shows: ?sort=name&order=asc
print(f"Sorted: {len(response.json())} devices\n")
# ==============================================================================
# Example 4: Multiple Filters Combined
# ==============================================================================
params = {
"status": "active", # Only active
"location": "Building A", # In Building A
"type": "router" # Router type only
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}")
# Shows: ?status=active&location=Building+A&type=router
print(f"Filtered: {len(response.json())} devices")
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource successfully created |
| 204 | No Content | Success but no content to return |
| 400 | Bad Request | Invalid request syntax or parameters |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error occurred |
š 2.2 Common Usage Patterns Related to Webhooks
What are Webhooks?
Webhooks are HTTP callbacks that allow applications to send real-time data to other applications when specific events occur. Instead of continuously polling for data (asking "did something happen?"), webhooks push data to your application when events happen.
Polling vs Webhooks
| Aspect | Polling | Webhooks |
|---|---|---|
| Communication | Pull - Client requests data | Push - Server sends data |
| Frequency | Periodic requests | Event-triggered |
| Efficiency | Less efficient (many empty responses) | More efficient (only when needed) |
| Latency | Higher (depends on polling interval) | Lower (real-time notification) |
| Resource Usage | Higher (continuous requests) | Lower (event-based) |
Common Webhook Usage Patterns
1. Event Notification Pattern
Receive notifications when specific events occur:
- GitHub: Notify when code is pushed, PR is created, issue is opened
- Stripe: Alert on payment success, failure, or refund
- Meraki: Network device status changes
- Webex: New message in room, user joins meeting
2. Data Synchronization Pattern
Keep systems in sync by receiving updates:
- Update local database when remote data changes
- Sync user information across multiple systems
- Maintain inventory consistency across platforms
3. Workflow Automation Pattern
Trigger automated workflows based on events:
- Deploy application when code is merged
- Send notifications to Slack/Teams when alert occurs
- Create ticket in ITSM when device goes down
- Run backup when configuration changes
Webhook Implementation
Setting Up a Webhook Endpoint (Python/Flask Script)
#!/usr/bin/env python3
"""
Webhook Endpoint with Security
Flask application that receives and validates webhook POST requests
"""
from flask import Flask, request, jsonify
import hashlib
import hmac
import logging
# ==============================================================================
# CONFIGURATION
# ==============================================================================
WEBHOOK_SECRET = "your_secret_key_here" # Shared secret (keep secure!)
app = Flask(__name__)
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ==============================================================================
# SECURITY FUNCTIONS
# ==============================================================================
def verify_signature(payload, signature):
"""
Verify webhook signature using HMAC-SHA256
Args:
payload (bytes): Raw request body
signature (str): Signature from request header
Returns:
bool: True if signature is valid
"""
if not signature:
return False
# Calculate expected signature
expected = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Securely compare (prevents timing attacks)
return hmac.compare_digest(signature, expected)
# ==============================================================================
# EVENT HANDLERS
# ==============================================================================
def handle_device_status(data):
"""Handle device status change event"""
device_name = data.get('device_name')
new_status = data.get('status')
logger.info(f"Device {device_name} status changed to {new_status}")
# Add your business logic here
def handle_alert(data):
"""Handle alert triggered event"""
alert_type = data.get('alert_type')
severity = data.get('severity')
logger.warning(f"Alert: {alert_type} (Severity: {severity})")
# Add your notification logic here
# ==============================================================================
# WEBHOOK ENDPOINT
# ==============================================================================
@app.route('/webhook', methods=['POST'])
def webhook_handler():
"""
Main webhook endpoint that receives POST requests
Returns:
JSON response with status code
"""
# Get signature from header
signature = request.headers.get('X-Hub-Signature')
# Verify webhook authenticity
if not verify_signature(request.data, signature):
logger.warning("Invalid signature")
return jsonify({'error': 'Invalid signature'}), 401
# Parse JSON payload
try:
data = request.json
except Exception as err:
logger.error(f"Invalid JSON: {err}")
return jsonify({'error': 'Invalid JSON'}), 400
# Get event type
event_type = request.headers.get('X-Event-Type', 'unknown')
logger.info(f"Received: {event_type}")
# Route to appropriate handler
if event_type == 'device.status.changed':
handle_device_status(data)
elif event_type == 'alert.triggered':
handle_alert(data)
else:
logger.warning(f"Unknown event: {event_type}")
# Return success immediately
return jsonify({'status': 'success', 'event_type': event_type}), 200
# ==============================================================================
# HEALTH CHECK ENDPOINT
# ==============================================================================
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({'status': 'healthy'}), 200
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
print("š Starting webhook server...")
print("š Webhook: http://localhost:5000/webhook")
print("š Health: http://localhost:5000/health")
app.run(host='0.0.0.0', port=5000, debug=True)
Registering a Webhook (Python Script)
#!/usr/bin/env python3
"""
Register Webhook with API Service
Subscribe to webhook events by registering your endpoint URL
"""
import requests
import json
# ==============================================================================
# CONFIGURATION
# ==============================================================================
WEBHOOK_API_URL = "https://api.service.com/v1/webhooks"
YOUR_API_TOKEN = "<YOUR_BEARER_TOKEN>"
YOUR_WEBHOOK_URL = "https://your-domain.com/webhook" # Must be publicly accessible
WEBHOOK_SECRET = "your_webhook_secret_here"
# ==============================================================================
# WEBHOOK REGISTRATION FUNCTION
# ==============================================================================
def register_webhook(webhook_url, events, secret):
"""
Register webhook with API service
Args:
webhook_url (str): Your endpoint URL
events (list): Event types to subscribe to
secret (str): Shared secret for verification
Returns:
dict: Registration response or None
"""
headers = {
"Authorization": f"Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"url": webhook_url,
"events": events,
"secret": secret
}
try:
print(f"š¤ Registering webhook...")
print(f"šÆ URL: {webhook_url}")
print(f"š Events: {', '.join(events)}")
response = requests.post(
WEBHOOK_API_URL,
headers=headers,
json=payload,
timeout=10
)
response.raise_for_status()
webhook_data = response.json()
print(f"\nā
Webhook registered!")
print(f"š ID: {webhook_data.get('id', 'N/A')}")
print(json.dumps(webhook_data, indent=2))
return webhook_data
except requests.exceptions.HTTPError as err:
print(f"ā HTTP Error: {err}")
except requests.exceptions.RequestException as err:
print(f"ā ļø Request Error: {err}")
except Exception as err:
print(f"ā Error: {err}")
return None
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
events = ["device.created", "device.updated", "device.deleted"]
result = register_webhook(YOUR_WEBHOOK_URL, events, WEBHOOK_SECRET)
if result:
print("\n⨠Webhook is now active!")
else:
print("\nā ļø Registration failed!")
Webhook Best Practices
- Verify signatures: Always validate webhook signatures using HMAC
- Use HTTPS: Ensure webhook endpoints use secure connections
- Validate payload: Check data structure and types
- IP whitelisting: Restrict access to known IP addresses
- Respond quickly: Return 200 OK immediately, process asynchronously
- Implement retry logic: Handle failed webhook deliveries
- Use queues: Queue webhook processing for heavy operations
- Idempotency: Handle duplicate webhook deliveries gracefully
Example: Handling Retries and Idempotency (Python Script)
#!/usr/bin/env python3
"""
Idempotent Webhook Handler with Redis
Prevents duplicate processing of webhook events
"""
from flask import Flask, request, jsonify
import redis
import logging
from datetime import datetime
# ==============================================================================
# CONFIGURATION
# ==============================================================================
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Redis connection for event tracking
redis_client = redis.Redis(
host='localhost',
port=6379,
db=0,
decode_responses=True
)
EVENT_TTL = 86400 # 24 hours in seconds
# ==============================================================================
# IDEMPOTENCY FUNCTIONS
# ==============================================================================
def is_event_processed(event_id):
"""Check if event was already processed"""
key = f'webhook:{event_id}'
return redis_client.exists(key) > 0
def mark_event_processed(event_id):
"""Mark event as processed in Redis with TTL"""
key = f'webhook:{event_id}'
value = datetime.now().isoformat()
redis_client.setex(key, EVENT_TTL, value)
logger.info(f"Marked event {event_id} as processed")
# ==============================================================================
# WEBHOOK ENDPOINT
# ==============================================================================
@app.route('/webhook', methods=['POST'])
def webhook():
"""Idempotent webhook handler - ensures events processed only once"""
# Get unique event ID
event_id = request.headers.get('X-Event-ID')
if not event_id:
return jsonify({'error': 'Missing X-Event-ID header'}), 400
# Check if already processed
if is_event_processed(event_id):
logger.info(f"Event {event_id} already processed")
return jsonify({'status': 'already_processed'}), 200
# Mark as processed
mark_event_processed(event_id)
# Process webhook data
try:
data = request.json
logger.info(f"Processing: {data}")
# TODO: Add to message queue here
except Exception as err:
logger.error(f"Error: {err}")
# Return success immediately
return jsonify({'status': 'received', 'event_id': event_id}), 200
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
print("š Webhook Server with Idempotency")
print("š Endpoint: http://localhost:5000/webhook")
print("š Redis-based idempotency enabled\n")
app.run(host='0.0.0.0', port=5000, debug=True)
Testing Webhooks
Tools for testing webhook implementations:
- ngrok: Expose local server to internet for testing
- webhook.site: Inspect webhook payloads
- RequestBin: Collect and inspect HTTP requests
- Postman: Send test webhook payloads
#!/usr/bin/env python3
"""
Testing Webhooks Locally with ngrok
Step-by-step guide and Python code for webhook testing
"""
import requests
# ==============================================================================
# STEP 1: Install ngrok
# ==============================================================================
# Download: https://ngrok.com/download
# Or use package manager:
# Windows: choco install ngrok
# Mac: brew install ngrok
# Linux: snap install ngrok
# ==============================================================================
# STEP 2: Run local webhook server (Terminal 1)
# ==============================================================================
# python webhook_server.py
# Server runs on: http://localhost:5000
# ==============================================================================
# STEP 3: Start ngrok tunnel (Terminal 2)
# ==============================================================================
# Command: ngrok http 5000
# Creates tunnel: public URL -> localhost:5000
# ==============================================================================
# STEP 4: Get public URL from ngrok output
# ==============================================================================
# ngrok displays:
# Forwarding: https://abc123.ngrok.io -> http://localhost:5000
# Use: https://abc123.ngrok.io/webhook
# ==============================================================================
# STEP 5: Register webhook using ngrok URL
# ==============================================================================
def register_ngrok_webhook(ngrok_url):
"""Register ngrok URL as webhook endpoint"""
webhook_config = {
"url": f"{ngrok_url}/webhook",
"events": ["device.created", "device.updated"],
"secret": "test-secret-123"
}
response = requests.post(
"https://api.service.com/v1/webhooks",
headers={"Authorization": "Bearer <YOUR_TOKEN>"},
json=webhook_config
)
return response.json()
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
# Replace with your actual ngrok URL
ngrok_url = "https://abc123def456.ngrok.io"
print(f"š Registering webhook: {ngrok_url}")
result = register_ngrok_webhook(ngrok_url)
print(f"ā
Registered!")
print(f"š Receiving at: {ngrok_url}/webhook")
print(f"š” Keep ngrok running!")
ā ļø 2.3 Identify Constraints When Consuming APIs
Overview
When working with APIs, various constraints can affect how you design and implement your applications. Understanding these limitations is crucial for building reliable and efficient systems.
1. Rate Limiting
Rate limiting restricts the number of API calls a client can make within a specific time period.
Common Rate Limit Types:
| Type | Description | Example |
|---|---|---|
| Per Second | Limits requests per second | 10 requests/second |
| Per Minute | Limits requests per minute | 100 requests/minute |
| Per Hour | Limits requests per hour | 1,000 requests/hour |
| Per Day | Daily request quota | 10,000 requests/day |
| Concurrent | Simultaneous connections | 5 concurrent requests |
Rate Limit Headers:
# Common rate limit response headers returned by API X-RateLimit-Limit: 100 # Total number of requests allowed in the current time window X-RateLimit-Remaining: 75 # Number of requests remaining in current time window X-RateLimit-Reset: 1640995200 # Unix timestamp when the rate limit will reset Retry-After: 60 # Number of seconds to wait before making another request
Handling Rate Limits in Code:
#!/usr/bin/env python3
"""
Rate Limit Handler with Auto-Retry
Handles 429 errors and respects Retry-After headers
"""
import time
import requests
from requests.exceptions import HTTPError
# ==============================================================================
# API CALL WITH RETRY LOGIC
# ==============================================================================
def api_call_with_retry(url, headers, max_retries=3):
"""Make API call with automatic retry on rate limit"""
for attempt in range(max_retries):
try:
# Make GET request
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise error if 4xx/5xx
# Check remaining rate limit
remaining = int(response.headers.get('X-RateLimit-Remaining', 1))
if remaining < 10:
print(f"ā ļø Warning: Only {remaining} requests remaining")
return response.json() # Return parsed response
except HTTPError as err:
if err.response.status_code == 429: # Rate limited
retry_after = int(err.response.headers.get('Retry-After', 60))
print(f"ā±ļø Rate limited. Waiting {retry_after}s (Attempt {attempt+1}/{max_retries})")
time.sleep(retry_after)
else:
raise # Different error, re-raise
raise Exception("Max retries exceeded")
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
url = "https://api.example.com/v1/devices"
headers = {"Authorization": "Bearer <YOUR_TOKEN>"}
try:
data = api_call_with_retry(url, headers)
print(f"ā
Success! Retrieved {len(data)} devices")
except Exception as err:
print(f"ā Failed: {err}")
2. Authentication and Authorization
Common Authentication Methods:
| Method | How It Works | Pros/Cons |
|---|---|---|
| API Keys | Static key in header/query | ā
Simple ā Less secure, no expiration |
| OAuth 2.0 | Token-based with refresh | ā
Secure, scoped access ā Complex setup |
| JWT | JSON Web Tokens | ā
Stateless, portable ā Can't revoke easily |
| Basic Auth | Base64 encoded username:password | ā
Simple ā Insecure without HTTPS |
Token Management Example:
#!/usr/bin/env python3
"""
OAuth Token Management with Auto-Refresh
Automatically refreshes expired tokens
"""
import requests
import time
# ==============================================================================
# API CLIENT WITH TOKEN MANAGEMENT
# ==============================================================================
class APIClient:
"""API Client with automatic token refresh"""
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None # Will be fetched on first use
self.token_expiry = 0 # Unix timestamp
def get_token(self):
"""Get valid token (fetches new one if expired)"""
# Check if current token is still valid
if self.access_token and time.time() < self.token_expiry:
return self.access_token # Use existing token
# Request new token from OAuth endpoint
response = requests.post(
'https://api.example.com/oauth/token',
data={
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret
}
)
data = response.json()
# Save new token and expiry
self.access_token = data['access_token']
self.token_expiry = time.time() + data['expires_in'] - 60 # 60s buffer
print(f"š New token acquired (expires in {data['expires_in']}s)")
return self.access_token
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
# Create client
client = APIClient(
client_id="your_client_id",
client_secret="your_client_secret"
)
# Get token (will fetch new one)
token = client.get_token()
print(f"Token: {token[:20]}...")
# Get token again (will reuse existing)
token2 = client.get_token()
print(f"Same token reused: {token == token2}")
3. Payload Size Limits
APIs often impose limits on request and response sizes:
- Request body: Often 1MB - 10MB maximum
- URL length: Typically 2048 characters for GET requests
- Response size: May be limited to prevent memory issues
- File uploads: Separate limits for media/attachments
Handling Large Datasets with Pagination:
#!/usr/bin/env python3
"""
Pagination Handler
Retrieves all data from paginated API endpoints
"""
import requests
# ==============================================================================
# PAGINATION FUNCTION
# ==============================================================================
def get_all_devices(base_url, headers):
"""Fetch all devices across all pages"""
all_devices = [] # Accumulator for all devices
page = 1 # Start at page 1
per_page = 100 # Max items per page
while True:
# Request current page
response = requests.get(
f"{base_url}/devices",
params={'page': page, 'per_page': per_page},
headers=headers
)
data = response.json() # Parse response
devices = data['devices'] # Extract devices array
all_devices.extend(devices) # Add to collection
print(f"š Page {page}: {len(devices)} devices")
# Check if last page
if len(devices) < per_page: # Fewer items = last page
break
page += 1 # Next page
return all_devices
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
base_url = "https://api.example.com"
headers = {"Authorization": "Bearer <YOUR_TOKEN>"}
print("š Fetching all devices with pagination...\n")
devices = get_all_devices(base_url, headers)
print(f"\nā
Total devices retrieved: {len(devices)}")
4. Timeout Constraints
APIs may terminate long-running requests:
- Connection timeout: Time to establish connection (typically 10-30s)
- Read timeout: Time to receive response (typically 30-120s)
- Total timeout: Overall request duration limit
#!/usr/bin/env python3
"""
Timeout Configuration Example
Demonstrates setting connection and read timeouts
"""
import requests
# ==============================================================================
# CONFIGURATION
# ==============================================================================
url = "https://api.example.com/v1/devices"
headers = {"Authorization": "Bearer <YOUR_TOKEN>"}
# ==============================================================================
# REQUEST WITH TIMEOUT
# ==============================================================================
try:
response = requests.get(
url,
headers=headers,
timeout=(10, 30) # (connect_timeout, read_timeout)
# 10s to establish connection, 30s to receive response
)
print(f"ā
Status: {response.status_code}")
print(response.json())
except requests.exceptions.Timeout:
print("ā±ļø Timeout: Server took too long to respond")
except requests.exceptions.ConnectionError:
print("ā Connection Error: Check network/URL")
except requests.exceptions.RequestException as err:
print(f"ā ļø Error: {err}")
5. API Versioning
APIs evolve over time, requiring version management:
Versioning Strategies:
| Method | Example | Description |
|---|---|---|
| URL Path | /api/v1/devices | Most common, explicit |
| Header | API-Version: 2.0 | Clean URLs, flexible |
| Query Param | /api/devices?version=1 | Simple but clutters URL |
| Content Type | Accept: application/vnd.api.v1+json | RESTful but complex |
Best Practices for Handling Constraints
- Implement caching: Reduce API calls with local caching
- Use pagination: Request data in manageable chunks
- Implement retry logic: Handle transient failures gracefully
- Monitor usage: Track API consumption and limits
- Implement circuit breakers: Prevent cascading failures
- Use webhooks: Replace polling where possible
- Batch requests: Combine multiple operations when supported
- Handle errors gracefully: Provide meaningful error messages
Complete Example: Resilient API Client (Python Script)
#!/usr/bin/env python3
"""
Resilient API Client with Retry, Caching, and Rate Limiting
Production-ready API client with all best practices
"""
import requests
import time
import logging
from cachetools import TTLCache
# ==============================================================================
# CONFIGURATION
# ==============================================================================
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ==============================================================================
# RESILIENT API CLIENT CLASS
# ==============================================================================
class ResilientAPIClient:
"""API Client with retry, caching, and rate limiting"""
def __init__(self, base_url, api_key, cache_ttl=300, max_retries=3):
"""Initialize API client with configuration"""
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.max_retries = max_retries
self.cache = TTLCache(maxsize=100, ttl=cache_ttl)
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
'Accept': 'application/json'
})
def _make_request(self, method, endpoint, **kwargs):
"""Make HTTP request with retry and rate limit handling"""
url = f"{self.base_url}{endpoint}"
backoff = 1
for attempt in range(1, self.max_retries + 1):
try:
response = self.session.request(method, url, timeout=(10, 30), **kwargs)
# Handle rate limiting
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', backoff))
logger.warning(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
backoff *= 2
continue
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
if attempt == self.max_retries:
raise
time.sleep(backoff)
backoff *= 2
except requests.exceptions.RequestException as err:
logger.error(f"Request failed: {err}")
raise
raise Exception(f"Failed after {self.max_retries} attempts")
def get(self, endpoint, use_cache=True, **kwargs):
"""Make GET request with optional caching"""
if use_cache and endpoint in self.cache:
logger.debug(f"Cache hit: {endpoint}")
return self.cache[endpoint]
result = self._make_request('GET', endpoint, **kwargs)
if use_cache:
self.cache[endpoint] = result
return result
def post(self, endpoint, data=None, **kwargs):
"""Make POST request"""
return self._make_request('POST', endpoint, json=data, **kwargs)
def patch(self, endpoint, data=None, **kwargs):
"""Make PATCH request"""
return self._make_request('PATCH', endpoint, json=data, **kwargs)
def delete(self, endpoint, **kwargs):
"""Make DELETE request"""
return self._make_request('DELETE', endpoint, **kwargs)
# ==============================================================================
# MAIN EXECUTION
# ==============================================================================
if __name__ == "__main__":
# Initialize client
client = ResilientAPIClient(
base_url="https://api.example.com",
api_key="<YOUR_API_KEY>",
cache_ttl=300,
max_retries=3
)
try:
# GET request with caching
print("š” Making GET request...")
devices = client.get("/v1/devices")
print(f"ā
Retrieved {len(devices)} devices")
# Same request (from cache)
devices_cached = client.get("/v1/devices")
print(f"ā” Cached: {len(devices_cached)} devices")
# POST request
new_device = client.post("/v1/devices", data={
"name": "Router-10",
"ip": "10.0.0.10"
})
print(f"ā
Created: {new_device.get('name')}")
except Exception as err:
print(f"ā Error: {err}")
print("\n⨠Demo completed!")
š Interactive Python Interpreter
Sample Code Templates
- Practice JSON data manipulation
- Test Python dictionaries and lists
- Simulate API response handling
- Learn Python basics for DevNet