A production-ready HTTP client with advanced features like retry strategies, authentication, and hooks
Project description
Request Forge
A production-ready, thread-safe Request Forge library for Python with advanced features like automatic retries, authentication management, request/response hooks, and multi-step token fetching pipelines.
Built following SOLID principles with comprehensive error handling, this library is designed for enterprise applications requiring robust HTTP communication with complex authentication flows.
๐ Features
Core Features
- โ Clean API: Intuitive interface for GET, POST, PUT, PATCH, DELETE requests
- โ Thread-Safe: Safe for use in multi-threaded environments (Django, Flask, FastAPI)
- โ Connection Pooling: Automatic connection pooling for optimal performance
- โ Retry Strategies: Exponential backoff, circuit breaker, custom strategies
- โ Error Handling: Comprehensive exception hierarchy with detailed context
- โ Request/Response Hooks: Extensible lifecycle hooks for cross-cutting concerns
- โ Concurrent Requests: Built-in support for parallel request execution
Authentication Features
- ๐ Token Management: Automatic token caching, refresh, and expiration handling
- ๐ Multi-Step Auth: Pipeline-based authentication for complex OAuth flows
- ๐ Auto-Retry on 401: Automatic token refresh and request retry
- ๐ Multiple Auth Types: Bearer tokens, API keys, Basic auth, custom schemes
- ๐ Token Storage: In-memory and Django cache backends
Developer Experience
- ๐ Type Hints: Full type annotations for IDE autocomplete
- ๐ Comprehensive Tests: 500+ test cases with 95%+ coverage
- ๐ Detailed Logging: Built-in logging hooks for debugging
- ๐ Context Managers: Clean resource management with
withstatements - ๐ Builder Pattern: Fluent configuration interface
๐ฆ Installation
Basic Installation
pip install requestforge
With Django Support
pip install requestforge[django]
Development Installation
git clone https://github.com/baratihd/requestforge.git
cd requestforge
pip install -e ".[dev]"
๐ฏ Quick Start
from requestforge import HttpClient, HttpClientConfigBuilder
# Create client with basic configuration
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_timeout(30.0)
.build()
)
client = HttpClient(config)
# Make GET request
response = client.get('/users/1')
if response.is_success:
user = response.json()
print(f"User: {user['name']}")
POST Request with JSON
response = client.post(
'/users',
json_data={
'name': 'John Doe',
'email': 'john@example.com',
}
)
if response.status_code == 201:
print(f"User created: {response.json()}")
Using Context Manager
from requestforge import http_client
with http_client('https://api.example.com') as client:
response = client.get('/users')
users = response.json()
๐ง Configuration
Builder Pattern Configuration
from requestforge import HttpClientConfigBuilder
config = (
HttpClientConfigBuilder()
# Base configuration
.with_base_url('https://api.example.com')
.with_timeout(30.0)
.with_verify_ssl(True)
# Headers
.with_header('User-Agent', 'MyApp/1.0')
.with_header('X-API-Version', 'v2')
# Authentication
.with_bearer_token('your-token-here')
# or
.with_api_key('your-api-key', header_name='X-API-Key')
# Retry configuration
.with_retry(
max_retries=3,
base_delay=1.0,
max_delay=60.0
)
# Connection pooling
.with_pool_connection(10)
.with_pool_maxsize(20)
# Logging
.with_logging(
log_headers=True,
log_body=False,
sensitive_keys={'authorization', 'x-api-key'}
)
.build()
)
All Configuration Options
Option | Type | Default Description base_url | str '' | Base URL for all requests default_timeout float 30.0 Default timeout in seconds default_headers dict {} Headers included in all requests verify_ssl bool True SSL certificate verification allow_redirects bool True Follow HTTP redirects max_redirects int 10 Maximum redirect hops pool_connection int 10 Connection pool size pool_maxsize int 20 Maximum pool size retry_strategy RetryStrategyInterface None Retry strategy implementation
๐ Retry Strategies
Exponential Backoff (Recommended)
from requestforge import HttpClientConfigBuilder, ExponentialBackoffRetryStrategy
strategy = ExponentialBackoffRetryStrategy(
max_retries=3,
base_delay=1.0, # Start with 1 second
max_delay=60.0, # Cap at 60 seconds
multiplier=2.0, # Double delay each retry
jitter=True, # Add randomization
retryable_status_codes={408, 429, 500, 502, 503, 504}
)
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_retry_strategy(strategy)
.build()
)
Retry Timeline:
- Attempt 1: Immediate
- Attempt 2: ~1 second delay (+ jitter)
- Attempt 3: ~2 second delay (+ jitter)
- Attempt 4: ~4 second delay (+ jitter)
Simple Retry
from requestforge import SimpleRetryStrategy
strategy = SimpleRetryStrategy(
max_retries=3,
delay=2.0 # Fixed 2-second delay
)
Circuit Breaker
from requestforge import CircuitBreakerRetryStrategy
strategy = CircuitBreakerRetryStrategy(
max_retries=3,
failure_threshold=5, # Open circuit after 5 failures
recovery_timeout=30.0, # Try again after 30 seconds
half_open_max_calls=3 # Test with 3 calls before fully closing
)
Circuit States:
- CLOSED: Normal operation
- OPEN: Too many failures, reject requests immediately
- HALF_OPEN: Testing if service recovered
Custom Retry Strategy
from requestforge import RetryStrategyInterface
from requestforge.models import RequestContext
class CustomRetryStrategy(RetryStrategyInterface):
def __init__(self, max_retries=3):
self._max_retries = max_retries
@property
def max_retries(self) -> int:
return self._max_retries
def should_retry(self, context: RequestContext, exception: Exception) -> bool:
# Custom logic: only retry on specific errors
if context.attempt >= self._max_retries:
return False
return isinstance(exception, TimeoutException)
def get_delay(self, context: RequestContext) -> float:
# Custom delay logic
return 2.0 ** context.attempt
๐ Authentication
Simple Bearer Token
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_bearer_token('your-static-token')
.build()
)
API Key Authentication
config = (
HttpClientConfigBuilder()
.with_api_key('your-api-key', header_name='X-API-Key')
.build()
)
Token Manager with Auto-Refresh
from requestforge import (
HttpClientConfigBuilder,
TokenManager,
ClientCredentialsTokenProvider,
InMemoryTokenStorage
)
# Setup token provider (OAuth2 client credentials)
provider = ClientCredentialsTokenProvider(
token_url='https://auth.example.com/oauth/token',
client_id='your-client-id',
client_secret='your-client-secret',
service_name='example-api',
scope='read write'
)
# Create token manager with caching
token_manager = TokenManager(
provider=provider,
storage=InMemoryTokenStorage()
)
# Configure client with auto-refresh
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_token_auth(
token_manager=token_manager,
excluded_paths={'/health', '/public/*'} # Don't auth these
)
.build()
)
client = HttpClient(config)
# Token is automatically:
# 1. Fetched on first request
# 2. Cached for subsequent requests
# 3. Refreshed when expired
# 4. Refreshed and retried on 401 errors
response = client.get('/protected-resource')
Multi-Step Authentication Pipeline
For complex authentication flows (e.g., get app token โ get user token โ access API):
from requestforge import TokenFetchPipeline, PipelineTokenProvider
from requestforge.fetcher import BodyTokenFetcher
from requestforge.token_manager import InMemoryTokenStorage, TokenManager
from datetime import timedelta
# Step 1: Fetch application token
app_token_fetcher = BodyTokenFetcher(
name='app_token',
base_url='https://auth.example.com',
endpoint='/v1/app/token',
method='POST',
request_data={
'grant_type': 'client_credentials',
'client_id': 'your-app-id',
'client_secret': 'your-app-secret',
},
token_field='access_token',
expires_in_field='expires_in',
ttl=timedelta(hours=1) # Cache for 1 hour
)
# Step 2: Fetch user access token (using app token)
class UserTokenFetcher(BodyTokenFetcher):
def _build_request_headers(self, context):
# Inject app token from previous step
headers = super()._build_request_headers(context)
if context and 'app_token' in context:
headers['X-App-Token'] = context['app_token'].access_token
return headers
user_token_fetcher = UserTokenFetcher(
name='user_token',
base_url='https://auth.example.com',
endpoint='/v1/user/token',
method='POST',
request_data={
'username': 'user@example.com',
'password': 'password123',
},
token_field='access_token',
ttl=timedelta(minutes=30),
depends_on=['app_token'] # Requires app_token
)
# Create pipeline
pipeline = TokenFetchPipeline(
steps=[app_token_fetcher, user_token_fetcher],
storage=InMemoryTokenStorage(),
cache_key_prefix='myapp'
)
# Wrap in provider
provider = PipelineTokenProvider(pipeline, service_name='myapp')
# Use with TokenManager
token_manager = TokenManager(provider)
# Configure client
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_token_auth(token_manager=token_manager)
.build()
)
client = HttpClient(config)
# Pipeline automatically:
# 1. Fetches app_token (step 1)
# 2. Fetches user_token using app_token (step 2)
# 3. Caches both tokens
# 4. Uses user_token for API requests
# 5. Refreshes tokens when expired
response = client.get('/user/profile')
Pipeline Benefits:
โ Automatic Dependency Resolution: Steps execute in correct order โ Per-Step Caching: Each token cached with its own TTL โ Cascading Invalidation: Invalidating step 1 clears dependent steps โ Partial Cache Hits: Reuse cached tokens when possible
๐ช Hooks & Lifecycle
Built-in Hooks
from requestforge import (
LoggingRequestHook,
LoggingResponseHook,
LoggingErrorHook,
CorrelationIdHook
)
config = (
HttpClientConfigBuilder()
.with_request_hook(LoggingRequestHook(log_headers=True))
.with_request_hook(CorrelationIdHook(header_name='X-Request-ID'))
.with_response_hook(LoggingResponseHook(log_body=True))
.with_error_hook(LoggingErrorHook())
.build()
)
Custom Request Hook
from requestforge.interfaces import RequestHookInterface
from requestforge.models import HttpRequest, RequestContext
class CustomHeaderHook(RequestHookInterface):
def before_request(self, request: HttpRequest, context: RequestContext) -> HttpRequest:
# Add custom logic before request
custom_header = f"request-{context.attempt}-{time.time()}"
return request.with_headers({
'X-Custom-Header': custom_header,
'X-Timestamp': str(time.time())
})
config = (
HttpClientConfigBuilder()
.with_request_hook(CustomHeaderHook())
.build()
)
Custom Response Hook
from requestforge.interfaces import ResponseHookInterface
class MetricsHook(ResponseHookInterface):
def after_response(self, response: HttpResponse, context: RequestContext) -> HttpResponse:
# Send metrics to monitoring system
metrics.timing('api.request.duration', response.elapsed_ms)
metrics.increment(f'api.status.{response.status_code}')
if not response.is_success:
metrics.increment('api.errors')
return response
Hook Execution Order
Request Flow:
- Request Hooks (in registration order)
- Auth Hook (token injection)
- โ HTTP Request โ
- Response Hooks (in registration order)
Error Flow:
- Error Hooks (in registration order)
- โ Exception Raised โ
๐ฆ Error Handling
Exception Hierarchy
HttpClientException (base)
โโโ MaxRetryException # Retries exhausted
โโโ TimeoutException # Request timeout
โโโ ConnectionException # Network error
โโโ SSLException # SSL/TLS error
โโโ ResponseParseException # JSON parse error
โโโ AuthenticationException # Auth failure
โโโ HttpStatusException # HTTP error status
โโโ BadRequestException (400)
โโโ UnauthorizedException (401)
โโโ ForbiddenException (403)
โโโ NotFoundException (404)
โโโ ServerErrorException (5xx)
Error Handling Examples
from requestforge import (
HttpClient,
TimeoutException,
ConnectionException,
UnauthorizedException,
HttpStatusException
)
try:
response = client.get('/users/1')
user = response.json()
except UnauthorizedException:
# Handle authentication errors
print("Authentication failed - please login")
except NotFoundException:
# Handle 404 specifically
print("User not found")
except HttpStatusException as e:
# Handle other HTTP errors
print(f"HTTP error {e.status_code}: {e.response_body}")
except TimeoutException:
# Handle timeouts
print("Request timed out")
except ConnectionException:
# Handle connection errors
print("Network connection failed")
except HttpClientException as e:
# Catch-all for HTTP client errors
print(f"Request failed: {e}")
if e.original_exception:
print(f"Original error: {e.original_exception}")
Accessing Error Context
try:
response = client.get('/users/1')
except MaxRetryException as e:
print(f"Failed after {e.attempts} attempts")
print(f"Original error: {e.original_exception}")
print(f"Context: {e.context}")
๐ Concurrent Requests
Parallel Request Execution
from requestforge import HttpRequest, HttpMethod
# Prepare multiple requests
requests = [
HttpRequest(method=HttpMethod.GET, url='/users/1'),
HttpRequest(method=HttpMethod.GET, url='/users/2'),
HttpRequest(method=HttpMethod.GET, url='/users/3'),
HttpRequest(method=HttpMethod.POST, url='/users', json_data={'name': 'John'}),
]
# Execute concurrently with 5 workers
results = client.request_many(requests, max_workers=5, fail_fast=False)
# Process results
for index, result in results:
if isinstance(result, HttpResponse):
print(f"Request {index}: {result.status_code}")
else:
print(f"Request {index} failed: {result}")
Fail-Fast Mode
# Stop on first error
try:
results = client.request_many(requests, max_workers=5, fail_fast=True)
# All requests succeeded
except HttpClientException as e:
# First error occurred
print(f"Request failed: {e}")
๐งช Testing
Mocking HTTP Requests
import responses
from requestforge import HttpClient, HttpClientConfigBuilder
@responses.activate
def test_get_user():
# Mock the HTTP response
responses.add(
responses.GET,
'https://api.example.com/users/1',
json={'id': 1, 'name': 'John Doe'},
status=200
)
# Create client and make request
config = HttpClientConfigBuilder().with_base_url('https://api.example.com').build()
client = HttpClient(config)
response = client.get('/users/1')
assert response.status_code == 200
assert response.json()['name'] == 'John Doe'
Testing with Custom Hooks
from unittest.mock import Mock
def test_custom_hook():
# Create mock hook
mock_hook = Mock()
mock_hook.before_request = Mock(side_effect=lambda req, ctx: req)
# Configure client with hook
config = (
HttpClientConfigBuilder()
.with_request_hook(mock_hook)
.build()
)
client = HttpClient(config)
# Make request (with responses mock)
response = client.get('/test')
# Verify hook was called
assert mock_hook.before_request.called
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=requestforge --cov-report=html
# Run specific test file
pytest tests/test_client.py -v
# Run specific test class
pytest tests/test_client.py::TestHttpClientBasicRequests -v
# Run with markers
pytest -m unit
pytest -m integration
๐ Logging
Enable Logging
import logging
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Enable HTTP client logging
config = (
HttpClientConfigBuilder()
.with_logging(
log_headers=True,
log_body=True,
sensitive_keys={'authorization', 'x-api-key', 'cookie'}
)
.build()
)
Log Output Example
2026-05-25 10:30:45,123 - requestforge.hooks - INFO - [a3f2c1b4] HTTP GET /users
2026-05-25 10:30:45,124 - requestforge.hooks - DEBUG - [a3f2c1b4] Headers: {'User-Agent': 'MyApp/1.0', 'Authorization': '***'}
2026-05-25 10:30:45,325 - requestforge.hooks - INFO - [a3f2c1b4] Response: 200 (201.23ms)
๐จ Advanced Usage
Custom Retry Logic per Request
from requestforge import SimpleRetryStrategy
# Configure client with default retry
config = (
HttpClientConfigBuilder()
.with_retry(max_retries=3)
.build()
)
client = HttpClient(config)
# Override retry for specific request
custom_strategy = SimpleRetryStrategy(max_retries=5, delay=2.0)
request = HttpRequest(
method=HttpMethod.GET,
url='/critical-endpoint',
timeout=60.0
)
# Set custom retry in context (requires modification to support per-request retry)
response = client.request(request)
Converting Request to cURL
request = HttpRequest(
method=HttpMethod.POST,
url='https://api.example.com/users',
headers={'Authorization': 'Bearer token'},
json_data={'name': 'John'},
params={'notify': 'true'}
)
curl_command = request.to_curl()
print(curl_command)
# Output: curl -X 'POST' -H 'Authorization: Bearer token' -H 'Content-Type: application/json' --data '{"name": "John"}' 'https://api.example.com/users?notify=true'
Session Sharing (Advanced)
import requests
# Create shared session
session = requests.Session()
session.headers.update({'User-Agent': 'CustomAgent/1.0'})
# Share session across multiple clients
client1 = HttpClient(config, session=session)
client2 = HttpClient(config, session=session)
# Both clients use the same connection pool
๐๏ธ Architecture
Design Principles
-
SOLID Principles
- Single Responsibility: Each class has one clear purpose
- Open/Closed: Extensible via hooks and strategies
- Liskov Substitution: Interfaces are interchangeable
- Interface Segregation: Small, focused interfaces
- Dependency Inversion: Depend on abstractions
-
Design Patterns
- Builder Pattern: Fluent configuration
- Strategy Pattern: Pluggable retry strategies
- Chain of Responsibility: Hook pipeline
- Factory Pattern: Client creation
- Template Method: Base fetcher classes
Component Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Request Forge โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ Retry Logic โ โ Hook Pipeline โ โ Auth System โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Token Management โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ Token Manager โ โ Token Storage โ โ Provider โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Multi-Step Auth Pipeline โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Step 1 โ โ โ Step 2 โ โ โ Step 3 โ โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ (App Token) (User Token) (Access Token) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Migration from requests
Before (using requests)
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
# Manual session setup
session = requests.Session()
retry = Retry(total=3, backoff_factor=1)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
session.headers.update({'Authorization': 'Bearer token'})
# Manual error handling
try:
response = session.get('https://api.example.com/users', timeout=30)
response.raise_for_status()
users = response.json()
except requests.Timeout:
print("Timeout!")
except requests.HTTPError as e:
print(f"HTTP error: {e}")
After (using http_client)
from requestforge import http_client
with http_client('https://api.example.com') as client:
try:
response = client.get('/users')
users = response.json()
except TimeoutException:
print("Timeout!")
except HttpStatusException as e:
print(f"HTTP error: {e.status_code}")
๐ค Contributing
Contributions are welcome! Please follow these guidelines:
Development Setup
# Clone repository
git clone https://github.com/baratihd/requestforge.git
cd requestforge
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Install pre-commit hooks
pre-commit install
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=requestforge --cov-report=html --cov-report=term-missing
# Run linting
ruff check src/ tests/
# Format code
ruff format src/ tests/
# Run tox for all Python versions
tox
Code Quality Checklist
โ All tests passing โ Code coverage > 90% โ Ruff linting passing โ Type hints added โ Documentation updated โ Changelog updated
Pull Request Process
- Fork the repository
- Create a feature branch (git checkout -b feature/amazing-feature)
- Commit your changes (git commit -m 'Add amazing feature')
- Push to branch (git push origin feature/amazing-feature)
- Open a Pull Request
๐ Changelog
See CHANGELOG.md for detailed version history.
Latest Version (1.0.0)
Added:
โ Initial release โ Core HTTP client with retry strategies โ Token management with auto-refresh โ Multi-step authentication pipelines โ Comprehensive test suite (500+ tests) โ Full type hints and documentation
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Built on top of the excellent requests library
- Inspired by enterprise API client requirements
- Thanks to all contributors and users
๐ Support
- Documentation: https://requestforge.readthedocs.io
- Issues: GitHub Issues
- Discussions: GitHub Discussions
๐ Links
PyPI: https://pypi.org/project/requestforge/ GitHub: https://github.com/baratihd/requestforge Documentation: https://requestforge.readthedocs.io Changelog: CHANGELOG.md
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file requestforge-1.0.0.tar.gz.
File metadata
- Download URL: requestforge-1.0.0.tar.gz
- Upload date:
- Size: 615.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a02fbab7413820b1fe2853b6641191ecbddf48d61a9621f8018c18553e3b597
|
|
| MD5 |
3b3ce029694d2149c7ade1f01acda639
|
|
| BLAKE2b-256 |
9ed5e9b7f8bea7b55e70eb9a6fc95bf4d92e082177f148e6ac9d05d58b0ae7c7
|
File details
Details for the file requestforge-1.0.0-py3-none-any.whl.
File metadata
- Download URL: requestforge-1.0.0-py3-none-any.whl
- Upload date:
- Size: 36.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84394ed266430ceb960380c6bed2bf4ad8d4863dff0336ab068cd2af1f4b76a2
|
|
| MD5 |
a70e42ecd32c500ba96d8914a7067cfc
|
|
| BLAKE2b-256 |
c40fdf4f87696d26e159725812d723c72edc505f1b0b215bfbaaf8196a7337f2
|