Every modern application talks to APIs. Cloud providers expose APIs for infrastructure. Monitoring tools have APIs for alerts. ChatOps tools have APIs for notifications. Python's Requests library makes API integration simple and elegant. In this lesson, you'll learn to build robust API clients that handle authentication, rate limiting, and errors gracefully.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Make GET, POST, PUT, DELETE requests with Requests
  • Handle API authentication (API keys, tokens, OAuth)
  • Parse JSON responses and handle errors
  • Implement rate limiting and retries
  • Build reusable API clients
  • Integrate with cloud provider APIs

2. Why This Matters

Real-world scenario: Your team needs to automatically scale EC2 instances based on load. AWS provides an API. You need to call it from Python. The Requests library makes this straightforward.

3. Core Concepts

Basic HTTP Requests

import requests

# GET request
response = requests.get('https://api.github.com/users/octocat')
print(f'Status: {response.status_code}')
print(f'Data: {response.json()}')

# GET with parameters
params = {'q': 'python', 'page': 1, 'per_page': 10}
response = requests.get('https://api.github.com/search/repositories', params=params)

# GET with headers
headers = {'User-Agent': 'MyApp/1.0', 'Accept': 'application/json'}
response = requests.get('https://api.github.com/repos/python/cpython', headers=headers)

# POST request
data = {'title': 'New Issue', 'body': 'This is a test issue'}
response = requests.post('https://api.github.com/repos/owner/repo/issues', json=data, headers=headers)

# PUT request
data = {'title': 'Updated Title'}
response = requests.put('https://api.github.com/repos/owner/repo/issues/1', json=data)

# DELETE request
response = requests.delete('https://api.github.com/repos/owner/repo/issues/1')

# PATCH request
data = {'state': 'closed'}
response = requests.patch('https://api.github.com/repos/owner/repo/issues/1', json=data)

# Check response
if response.ok:
    print('Success!')
else:
    print(f'Failed: {response.status_code} - {response.text}')
Basic HTTP requests with Requests

Handling Authentication

# API Key in headers
headers = {'X-API-Key': 'your-api-key'}
response = requests.get('https://api.example.com/data', headers=headers)

# API Key in URL
response = requests.get('https://api.example.com/data?api_key=your-api-key')

# Basic Authentication
response = requests.get('https://api.example.com/private', auth=('username', 'password'))

# Bearer Token
headers = {'Authorization': 'Bearer your-token'}
response = requests.get('https://api.example.com/user', headers=headers)

# GitHub Token
github_token = 'ghp_your_token'
headers = {
    'Authorization': f'Bearer {github_token}',
    'Accept': 'application/vnd.github.v3+json'
}
response = requests.get('https://api.github.com/user/repos', headers=headers)

# AWS Signature (using boto3 for AWS is better)
# For other APIs needing custom auth, use requests-auth
from requests.auth import AuthBase

class CustomAuth(AuthBase):
    def __init__(self, token):
        self.token = token
    
    def __call__(self, r):
        r.headers['X-Custom-Token'] = self.token
        return r

response = requests.get('https://api.example.com/data', auth=CustomAuth('token123'))
API authentication methods

Error Handling and Retries

import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# Basic error handling
try:
    response = requests.get('https://api.example.com/data', timeout=10)
    response.raise_for_status()  # Raises exception for 4xx/5xx
    data = response.json()
except requests.exceptions.Timeout:
    print('Request timed out')
except requests.exceptions.ConnectionError:
    print('Connection error')
except requests.exceptions.HTTPError as e:
    print(f'HTTP error: {e}')
except requests.exceptions.RequestException as e:
    print(f'Other error: {e}')

# Automatic retries with backoff
session = requests.Session()
retry_strategy = Retry(
    total=3,                      # Total retries
    backoff_factor=1,             # Wait 1, 2, 4 seconds between retries
    status_forcelist=[429, 500, 502, 503, 504],  # Retry on these status codes
    allowed_methods=['GET', 'POST', 'PUT']
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount('http://', adapter)
session.mount('https://', adapter)

# Use the session
response = session.get('https://api.example.com/data')

# Custom retry with exponential backoff
def request_with_retry(url, max_retries=3, delay=1):
    for attempt in range(max_retries):
        try:
            response = requests.get(url, timeout=10)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f'Attempt {attempt + 1} failed: {e}')
            if attempt == max_retries - 1:
                raise
            time.sleep(delay * (2 ** attempt))  # Exponential backoff
    return None
Error handling and retry logic

Rate Limiting and Pagination

import time
from datetime import datetime

# Respect rate limits
class RateLimitedClient:
    def __init__(self, rate_limit=60, period=60):
        self.rate_limit = rate_limit
        self.period = period
        self.requests_made = 0
        self.period_start = datetime.now()
    
    def get(self, url, **kwargs):
        # Check rate limit
        now = datetime.now()
        elapsed = (now - self.period_start).total_seconds()
        
        if elapsed > self.period:
            # New period
            self.requests_made = 0
            self.period_start = now
        
        if self.requests_made >= self.rate_limit:
            wait_time = self.period - elapsed
            print(f'Rate limit reached. Waiting {wait_time:.1f} seconds...')
            time.sleep(wait_time)
            self.requests_made = 0
            self.period_start = datetime.now()
        
        self.requests_made += 1
        return requests.get(url, **kwargs)

# Pagination examples
# GitHub pagination (Link header)
def get_all_github_repos(username):
    repos = []
    url = f'https://api.github.com/users/{username}/repos?per_page=100'
    
    while url:
        response = requests.get(url)
        repos.extend(response.json())
        
        # Check for next page in Link header
        if 'next' in response.links:
            url = response.links['next']['url']
        else:
            url = None
    
    return repos

# Page-based pagination
def get_all_pages(base_url, params=None):
    if params is None:
        params = {}
    
    results = []
    page = 1
    
    while True:
        params['page'] = page
        response = requests.get(base_url, params=params)
        data = response.json()
        
        if not data:
            break
        
        results.extend(data)
        page += 1
    
    return results

# Cursor-based pagination
def paginate_with_cursor(base_url, limit=100):
    results = []
    cursor = None
    
    while True:
        params = {'limit': limit}
        if cursor:
            params['cursor'] = cursor
        
        response = requests.get(base_url, params=params)
        data = response.json()
        results.extend(data['items'])
        
        cursor = data.get('next_cursor')
        if not cursor:
            break
    
    return results
Rate limiting and pagination strategies

Building a Reusable API Client

class DevOpsAPIClient:
    """Reusable API client for DevOps tools"""
    
    def __init__(self, base_url, api_key=None, token=None, timeout=30):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.session = requests.Session()
        
        # Set headers
        if api_key:
            self.session.headers.update({'X-API-Key': api_key})
        if token:
            self.session.headers.update({'Authorization': f'Bearer {token}'})
        
        # Add retries
        retry_strategy = Retry(
            total=3,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504]
        )
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount('http://', adapter)
        self.session.mount('https://', adapter)
    
    def _request(self, method, endpoint, **kwargs):
        """Make API request with error handling"""
        url = f'{self.base_url}/{endpoint.lstrip("/")}'
        
        try:
            response = self.session.request(
                method, url, 
                timeout=self.timeout,
                **kwargs
            )
            response.raise_for_status()
            return response.json() if response.content else None
        except requests.exceptions.HTTPError as e:
            print(f'HTTP error: {e.response.status_code} - {e.response.text}')
            raise
        except requests.exceptions.RequestException as e:
            print(f'Request error: {e}')
            raise
    
    def get(self, endpoint, **kwargs):
        return self._request('GET', endpoint, **kwargs)
    
    def post(self, endpoint, data=None, **kwargs):
        return self._request('POST', endpoint, json=data, **kwargs)
    
    def put(self, endpoint, data=None, **kwargs):
        return self._request('PUT', endpoint, json=data, **kwargs)
    
    def delete(self, endpoint, **kwargs):
        return self._request('DELETE', endpoint, **kwargs)
    
    def get_paginated(self, endpoint, **kwargs):
        """Get all pages of results"""
        results = []
        params = kwargs.get('params', {})
        page = 1
        
        while True:
            params['page'] = page
            response = self._request('GET', endpoint, params=params)
            
            if not response:
                break
            
            if isinstance(response, list):
                results.extend(response)
            elif isinstance(response, dict) and 'items' in response:
                results.extend(response['items'])
            else:
                results.append(response)
            
            if len(response) < (params.get('per_page', 100)):
                break
            
            page += 1
        
        return results

# Example usage
client = DevOpsAPIClient(
    base_url='https://api.github.com',
    token='ghp_your_token'
)

# Get user info
user = client.get('/user')
print(f'User: {user["login"]}')

# Create an issue
issue = client.post(
    '/repos/owner/repo/issues',
    data={'title': 'Bug report', 'body': 'Description here'}
)

# Get all repos (paginated)
repos = client.get_paginated('/user/repos', params={'per_page': 100})
print(f'Total repos: {len(repos)}')
Building a reusable API client class

4. Complete Project: Multi-API Status Monitor

#!/usr/bin/env python3
"""
api_status_monitor.py - Monitor multiple API endpoints
"""

import requests
import json
import time
from datetime import datetime
from typing import Dict, List
from concurrent.futures import ThreadPoolExecutor, as_completed

class APIStatusMonitor:
    """Monitor multiple API endpoints with health checks"""
    
    def __init__(self, config_file=None):
        self.endpoints = self.load_config(config_file)
        self.results = []
    
    def load_config(self, config_file):
        """Load API endpoints from config"""
        if config_file:
            with open(config_file, 'r') as f:
                return json.load(f)['endpoints']
        
        # Default configuration
        return [
            {'name': 'GitHub API', 'url': 'https://api.github.com/health', 'method': 'GET', 'expected_status': 200},
            {'name': 'Google', 'url': 'https://www.google.com', 'method': 'GET', 'expected_status': 200},
            {'name': 'Local Service', 'url': 'http://localhost:8080/health', 'method': 'GET', 'expected_status': 200, 'timeout': 5}
        ]
    
    def check_endpoint(self, endpoint: Dict) -> Dict:
        """Check a single endpoint"""
        start_time = time.time()
        result = {
            'name': endpoint['name'],
            'url': endpoint['url'],
            'timestamp': datetime.now().isoformat(),
            'status': 'unknown',
            'response_time': None,
            'status_code': None,
            'error': None
        }
        
        try:
            method = endpoint.get('method', 'GET').upper()
            timeout = endpoint.get('timeout', 10)
            expected_status = endpoint.get('expected_status', 200)
            
            response = requests.request(
                method, 
                endpoint['url'],
                timeout=timeout
            )
            
            result['status_code'] = response.status_code
            result['response_time'] = round((time.time() - start_time) * 1000, 2)
            
            if response.status_code == expected_status:
                result['status'] = 'healthy'
            elif 400 <= response.status_code < 500:
                result['status'] = 'client_error'
            elif 500 <= response.status_code < 600:
                result['status'] = 'server_error'
            else:
                result['status'] = 'unexpected'
                
        except requests.exceptions.Timeout:
            result['status'] = 'timeout'
            result['error'] = f'Timeout after {timeout}s'
        except requests.exceptions.ConnectionError:
            result['status'] = 'connection_error'
            result['error'] = 'Failed to connect'
        except Exception as e:
            result['status'] = 'error'
            result['error'] = str(e)
        
        return result
    
    def check_all(self, parallel=True):
        """Check all endpoints"""
        self.results = []
        
        if parallel:
            # Check endpoints in parallel
            with ThreadPoolExecutor(max_workers=10) as executor:
                futures = {executor.submit(self.check_endpoint, ep): ep for ep in self.endpoints}
                for future in as_completed(futures):
                    self.results.append(future.result())
        else:
            # Sequential checks
            for endpoint in self.endpoints:
                self.results.append(self.check_endpoint(endpoint))
        
        return self.results
    
    def generate_report(self):
        """Generate status report"""
        if not self.results:
            self.check_all()
        
        report = {
            'timestamp': datetime.now().isoformat(),
            'summary': {
                'total': len(self.results),
                'healthy': sum(1 for r in self.results if r['status'] == 'healthy'),
                'unhealthy': sum(1 for r in self.results if r['status'] != 'healthy'),
                'average_response': round(sum(r.get('response_time', 0) for r in self.results) / len(self.results), 2)
            },
            'endpoints': self.results
        }
        
        return report
    
    def print_report(self):
        """Print colorful report to console"""
        report = self.generate_report()
        
        print("\n" + "="*60)
        print(f"API STATUS REPORT - {report['timestamp']}")
        print("="*60)
        print(f"Total: {report['summary']['total']} | Healthy: {report['summary']['healthy']} | Unhealthy: {report['summary']['unhealthy']}")
        print(f"Average Response: {report['summary']['average_response']}ms")
        print("-"*60)
        
        for endpoint in report['endpoints']:
            status_color = {
                'healthy': 'āœ…',
                'timeout': 'ā°',
                'connection_error': 'šŸ”Œ',
                'client_error': 'āš ļø',
                'server_error': 'šŸ”„',
                'error': 'āŒ'
            }.get(endpoint['status'], 'ā“')
            
            response_info = f"{endpoint['response_time']}ms" if endpoint['response_time'] else 'N/A'
            status_info = f"HTTP {endpoint['status_code']}" if endpoint['status_code'] else ''
            
            print(f"{status_color} {endpoint['name']:20} {endpoint['status']:15} {response_info:10} {status_info}")
            if endpoint.get('error'):
                print(f"   └─ Error: {endpoint['error']}")
        
        print("="*60 + "\n")
    
    def save_report(self, filepath='api_status_report.json'):
        """Save report to JSON file"""
        report = self.generate_report()
        with open(filepath, 'w') as f:
            json.dump(report, f, indent=2)
        print(f"Report saved to {filepath}")
    
    def run_continuous(self, interval=60):
        """Run monitoring continuously"""
        print(f"Starting continuous monitoring (interval: {interval}s)")
        print("Press Ctrl+C to stop\n")
        
        try:
            while True:
                self.check_all()
                self.print_report()
                time.sleep(interval)
        except KeyboardInterrupt:
            print("\nMonitoring stopped")


def main():
    import argparse
    
    parser = argparse.ArgumentParser(description='API Status Monitor')
    parser.add_argument('--config', '-c', help='Config file with endpoints')
    parser.add_argument('--continuous', action='store_true', help='Run continuously')
    parser.add_argument('--interval', type=int, default=60, help='Check interval in seconds')
    parser.add_argument('--output', '-o', help='Save report to file')
    
    args = parser.parse_args()
    
    monitor = APIStatusMonitor(args.config)
    
    if args.continuous:
        monitor.run_continuous(args.interval)
    else:
        monitor.check_all()
        monitor.print_report()
        
        if args.output:
            monitor.save_report(args.output)

if __name__ == '__main__':
    main()
Complete multi-API status monitor

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Python Lesson 4.5: System Administration (os, subprocess)

You'll learn to:

  • Run system commands from Python
  • Manage environment variables
  • Work with process management
  • Build system administration tools

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen January 29, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment