python

exercises

exercises.py🐍
"""
Networking & APIs - Exercises
Practice HTTP requests, API interactions, and JSON handling
"""


# ============================================================
# EXERCISE 1: URL Parser
# ============================================================
"""
Create a function that parses a URL and returns its components.

Example:
>>> parse_url("https://api.example.com:8080/users/123?format=json&active=true#section1")
{
    'scheme': 'https',
    'host': 'api.example.com',
    'port': 8080,
    'path': '/users/123',
    'query': {'format': ['json'], 'active': ['true']},
    'fragment': 'section1'
}
"""

from urllib.parse import urlparse, parse_qs

def parse_url(url):
    # YOUR CODE HERE
    pass


# Test
# test_url = "https://api.example.com:8080/users/123?format=json&active=true#section1"
# print(parse_url(test_url))


# ============================================================
# EXERCISE 2: Query String Builder
# ============================================================
"""
Create a function that builds a URL with query parameters.
Handle special characters properly (URL encoding).

Example:
>>> build_url("https://api.example.com/search", {"q": "python programming", "page": 1, "filter": "name=test"})
'https://api.example.com/search?q=python+programming&page=1&filter=name%3Dtest'
"""

from urllib.parse import urlencode

def build_url(base_url, params):
    # YOUR CODE HERE
    pass


# Test
# print(build_url("https://api.example.com/search", {"q": "python programming", "page": 1}))


# ============================================================
# EXERCISE 3: JSON Data Validator
# ============================================================
"""
Create a function that validates if a string is valid JSON
and optionally checks if it contains required keys.

Example:
>>> validate_json('{"name": "John", "age": 30}')
(True, {'name': 'John', 'age': 30})

>>> validate_json('{"name": "John"}', required_keys=['name', 'age'])
(False, "Missing required keys: ['age']")

>>> validate_json('not valid json')
(False, "Invalid JSON: ...")
"""

import json

def validate_json(json_string, required_keys=None):
    # YOUR CODE HERE
    pass


# Test
# print(validate_json('{"name": "John", "age": 30}'))
# print(validate_json('{"name": "John"}', required_keys=['name', 'age']))
# print(validate_json('not json'))


# ============================================================
# EXERCISE 4: API Response Handler
# ============================================================
"""
Create a class that handles API responses with proper error handling,
status code checking, and data extraction.

Example:
>>> handler = APIResponseHandler(response)
>>> handler.is_success()
True
>>> handler.get_data()
{'users': [...]}
>>> handler.get_header('Content-Type')
'application/json'
"""

class APIResponseHandler:
    """Handle API responses with proper error checking."""
    
    def __init__(self, response):
        # YOUR CODE HERE
        pass
    
    def is_success(self):
        """Return True if status code is 2xx."""
        pass
    
    def get_status(self):
        """Return status code."""
        pass
    
    def get_data(self):
        """Return parsed JSON data or None."""
        pass
    
    def get_header(self, header_name, default=None):
        """Get a specific header value."""
        pass
    
    def get_error_message(self):
        """Return error message if not successful."""
        pass


# ============================================================
# EXERCISE 5: Rate Limiter
# ============================================================
"""
Create a rate limiter class that ensures API calls don't exceed
a specified rate (e.g., 10 requests per minute).

Example:
>>> limiter = RateLimiter(max_requests=10, period=60)  # 10 per minute
>>> limiter.can_proceed()
True
>>> limiter.record_request()
>>> # After 10 requests:
>>> limiter.can_proceed()
False
>>> limiter.wait_time()
45.5  # seconds to wait
"""

import time
from collections import deque

class RateLimiter:
    """Rate limiter for API requests."""
    
    def __init__(self, max_requests, period):
        """
        Args:
            max_requests: Maximum number of requests allowed
            period: Time period in seconds
        """
        # YOUR CODE HERE
        pass
    
    def can_proceed(self):
        """Check if a request can be made."""
        pass
    
    def record_request(self):
        """Record that a request was made."""
        pass
    
    def wait_time(self):
        """Return how long to wait before next request (in seconds)."""
        pass


# ============================================================
# EXERCISE 6: Retry Decorator
# ============================================================
"""
Create a decorator that retries a function on failure with
exponential backoff.

Example:
@retry(max_attempts=3, backoff_factor=2)
def fetch_data(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

# Will retry up to 3 times with delays: 1s, 2s, 4s
"""

import time
import functools

def retry(max_attempts=3, backoff_factor=2, exceptions=(Exception,)):
    """
    Retry decorator with exponential backoff.
    
    Args:
        max_attempts: Maximum number of attempts
        backoff_factor: Multiplier for delay between retries
        exceptions: Tuple of exceptions to catch and retry
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # YOUR CODE HERE
            pass
        return wrapper
    return decorator


# Test
# @retry(max_attempts=3, backoff_factor=2)
# def unreliable_function():
#     import random
#     if random.random() < 0.7:
#         raise Exception("Random failure")
#     return "Success!"


# ============================================================
# EXERCISE 7: Simple HTTP Client
# ============================================================
"""
Create a simple HTTP client class with common operations.
Use the requests library internally.

Example:
>>> client = SimpleHTTPClient("https://api.example.com")
>>> client.get("/users")
>>> client.post("/users", {"name": "John"})
>>> client.put("/users/1", {"name": "Jane"})
>>> client.delete("/users/1")
"""

class SimpleHTTPClient:
    """Simple HTTP client for API interactions."""
    
    def __init__(self, base_url, headers=None, timeout=30):
        """
        Args:
            base_url: Base URL for all requests
            headers: Default headers for all requests
            timeout: Default timeout in seconds
        """
        # YOUR CODE HERE
        pass
    
    def get(self, endpoint, params=None, **kwargs):
        """Make a GET request."""
        pass
    
    def post(self, endpoint, data=None, json_data=None, **kwargs):
        """Make a POST request."""
        pass
    
    def put(self, endpoint, data=None, json_data=None, **kwargs):
        """Make a PUT request."""
        pass
    
    def delete(self, endpoint, **kwargs):
        """Make a DELETE request."""
        pass
    
    def _request(self, method, endpoint, **kwargs):
        """Internal method to make requests."""
        pass


# ============================================================
# EXERCISE 8: JSON File Cache
# ============================================================
"""
Create a cache class that stores API responses in JSON files
with expiration support.

Example:
>>> cache = JSONFileCache("./cache", ttl=3600)  # 1 hour TTL
>>> cache.set("user_123", {"name": "John"})
>>> cache.get("user_123")
{"name": "John"}
>>> cache.get("expired_key")  # Returns None if expired
None
"""

import json
import os
import time
from pathlib import Path

class JSONFileCache:
    """File-based JSON cache with TTL support."""
    
    def __init__(self, cache_dir, ttl=3600):
        """
        Args:
            cache_dir: Directory to store cache files
            ttl: Time-to-live in seconds (default 1 hour)
        """
        # YOUR CODE HERE
        pass
    
    def _get_cache_path(self, key):
        """Get the file path for a cache key."""
        pass
    
    def set(self, key, value):
        """Store a value in the cache."""
        pass
    
    def get(self, key):
        """Get a value from the cache (None if missing/expired)."""
        pass
    
    def delete(self, key):
        """Delete a value from the cache."""
        pass
    
    def clear(self):
        """Clear all cached values."""
        pass


# ============================================================
# EXERCISE 9: API Pagination Iterator
# ============================================================
"""
Create a class that iterates through paginated API responses.

Example:
>>> paginator = APIPaginator(
...     client,
...     "/users",
...     per_page=50
... )
>>> for user in paginator:
...     print(user['name'])
"""

class APIPaginator:
    """Iterate through paginated API responses."""
    
    def __init__(self, client, endpoint, per_page=10, max_pages=None):
        """
        Args:
            client: HTTP client instance
            endpoint: API endpoint to paginate
            per_page: Items per page
            max_pages: Maximum pages to fetch (None for all)
        """
        # YOUR CODE HERE
        pass
    
    def __iter__(self):
        """Return iterator."""
        pass
    
    def __next__(self):
        """Get next item."""
        pass
    
    def _fetch_page(self, page):
        """Fetch a specific page."""
        pass


# ============================================================
# EXERCISE 10: Webhook Handler
# ============================================================
"""
Create a simple webhook handler that validates and processes
incoming webhook payloads.

Example:
>>> handler = WebhookHandler(secret="my-secret")
>>> handler.add_handler("user.created", handle_user_created)
>>> handler.add_handler("order.completed", handle_order_completed)
>>> handler.process(payload, signature)
"""

import hashlib
import hmac
import json

class WebhookHandler:
    """Handle incoming webhooks with validation."""
    
    def __init__(self, secret):
        """
        Args:
            secret: Secret key for signature validation
        """
        # YOUR CODE HERE
        pass
    
    def add_handler(self, event_type, handler_func):
        """Register a handler for an event type."""
        pass
    
    def validate_signature(self, payload, signature):
        """Validate webhook signature."""
        pass
    
    def process(self, payload, signature=None):
        """Process an incoming webhook."""
        pass


# ============================================================
# CHALLENGE: Build a REST API Client for JSONPlaceholder
# ============================================================
"""
JSONPlaceholder (https://jsonplaceholder.typicode.com) is a free
fake REST API for testing. Build a complete client for it.

The client should support:
- Posts: list, get, create, update, delete
- Comments: list by post, create
- Users: list, get
- Todos: list by user

Include:
- Proper error handling
- Response caching
- Rate limiting
- Logging

Example usage:
>>> client = JSONPlaceholderClient()
>>> posts = client.posts.list(limit=5)
>>> post = client.posts.get(1)
>>> new_post = client.posts.create(title="Hello", body="World", user_id=1)
>>> client.posts.update(1, title="Updated")
>>> client.posts.delete(1)
>>> comments = client.comments.list_by_post(1)
>>> user = client.users.get(1)
>>> todos = client.todos.list_by_user(1)
"""

class JSONPlaceholderClient:
    """Client for JSONPlaceholder API."""
    
    BASE_URL = "https://jsonplaceholder.typicode.com"
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    # Implement Posts, Comments, Users, Todos resources
    # with proper caching, error handling, and rate limiting


# ============================================================
# SOLUTIONS (Uncomment to check your work)
# ============================================================

"""
# Solution 1: URL Parser
def parse_url(url):
    parsed = urlparse(url)
    port = parsed.port if parsed.port else (443 if parsed.scheme == 'https' else 80)
    
    return {
        'scheme': parsed.scheme,
        'host': parsed.hostname,
        'port': port,
        'path': parsed.path,
        'query': parse_qs(parsed.query),
        'fragment': parsed.fragment
    }


# Solution 2: Query String Builder
def build_url(base_url, params):
    if not params:
        return base_url
    query_string = urlencode(params)
    return f"{base_url}?{query_string}"


# Solution 3: JSON Data Validator
def validate_json(json_string, required_keys=None):
    try:
        data = json.loads(json_string)
    except json.JSONDecodeError as e:
        return (False, f"Invalid JSON: {e}")
    
    if required_keys:
        missing = [key for key in required_keys if key not in data]
        if missing:
            return (False, f"Missing required keys: {missing}")
    
    return (True, data)


# Solution 4: API Response Handler
class APIResponseHandler:
    def __init__(self, response):
        self.response = response
        self._data = None
    
    def is_success(self):
        return 200 <= self.response.status_code < 300
    
    def get_status(self):
        return self.response.status_code
    
    def get_data(self):
        if self._data is None:
            try:
                self._data = self.response.json()
            except:
                self._data = None
        return self._data
    
    def get_header(self, header_name, default=None):
        return self.response.headers.get(header_name, default)
    
    def get_error_message(self):
        if self.is_success():
            return None
        data = self.get_data()
        if data and 'error' in data:
            return data['error']
        if data and 'message' in data:
            return data['message']
        return f"HTTP {self.get_status()}"


# Solution 5: Rate Limiter
class RateLimiter:
    def __init__(self, max_requests, period):
        self.max_requests = max_requests
        self.period = period
        self.requests = deque()
    
    def _clean_old_requests(self):
        now = time.time()
        while self.requests and now - self.requests[0] > self.period:
            self.requests.popleft()
    
    def can_proceed(self):
        self._clean_old_requests()
        return len(self.requests) < self.max_requests
    
    def record_request(self):
        self.requests.append(time.time())
    
    def wait_time(self):
        self._clean_old_requests()
        if len(self.requests) < self.max_requests:
            return 0
        oldest = self.requests[0]
        return self.period - (time.time() - oldest)


# Solution 6: Retry Decorator
def retry(max_attempts=3, backoff_factor=2, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            delay = 1
            last_exception = None
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
                        delay *= backoff_factor
            
            raise last_exception
        return wrapper
    return decorator


# Solution 7: Simple HTTP Client
import requests

class SimpleHTTPClient:
    def __init__(self, base_url, headers=None, timeout=30):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.session = requests.Session()
        if headers:
            self.session.headers.update(headers)
    
    def _request(self, method, endpoint, **kwargs):
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        kwargs.setdefault('timeout', self.timeout)
        response = self.session.request(method, url, **kwargs)
        response.raise_for_status()
        return response.json()
    
    def get(self, endpoint, params=None, **kwargs):
        return self._request('GET', endpoint, params=params, **kwargs)
    
    def post(self, endpoint, data=None, json_data=None, **kwargs):
        return self._request('POST', endpoint, data=data, json=json_data, **kwargs)
    
    def put(self, endpoint, data=None, json_data=None, **kwargs):
        return self._request('PUT', endpoint, data=data, json=json_data, **kwargs)
    
    def delete(self, endpoint, **kwargs):
        return self._request('DELETE', endpoint, **kwargs)


# Solution 8: JSON File Cache
class JSONFileCache:
    def __init__(self, cache_dir, ttl=3600):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.ttl = ttl
    
    def _get_cache_path(self, key):
        safe_key = key.replace('/', '_').replace('\\\\', '_')
        return self.cache_dir / f"{safe_key}.json"
    
    def set(self, key, value):
        cache_data = {
            'data': value,
            'timestamp': time.time()
        }
        with open(self._get_cache_path(key), 'w') as f:
            json.dump(cache_data, f)
    
    def get(self, key):
        path = self._get_cache_path(key)
        if not path.exists():
            return None
        
        with open(path, 'r') as f:
            cache_data = json.load(f)
        
        if time.time() - cache_data['timestamp'] > self.ttl:
            path.unlink()
            return None
        
        return cache_data['data']
    
    def delete(self, key):
        path = self._get_cache_path(key)
        if path.exists():
            path.unlink()
    
    def clear(self):
        for path in self.cache_dir.glob('*.json'):
            path.unlink()
"""


if __name__ == "__main__":
    print("Networking & APIs Exercises")
    print("=" * 50)
    print("\nComplete the exercises above to practice:")
    print("- URL parsing and building")
    print("- JSON validation")
    print("- API response handling")
    print("- Rate limiting")
    print("- Retry patterns")
    print("- HTTP client design")
    print("- Caching strategies")
    print("- Pagination handling")
    print("- Webhook processing")
    print("\nUncomment the solutions to check your work!")
Exercises - Python Tutorial | DeepML