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.
- β 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
- π 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
- π 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
pip install requestforgepip install requestforge[django]git clone https://github.com/baratihd/requestforge.git
cd requestforge
pip install -e ".[dev]"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']}")response = client.post(
'/users',
json_data={
'name': 'John Doe',
'email': 'john@example.com',
}
)
if response.status_code == 201:
print(f"User created: {response.json()}")from requestforge import http_client
with http_client('https://api.example.com') as client:
response = client.get('/users')
users = response.json()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()
)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
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)
from requestforge import SimpleRetryStrategy
strategy = SimpleRetryStrategy(
max_retries=3,
delay=2.0 # Fixed 2-second delay
)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
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.attemptconfig = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_bearer_token('your-static-token')
.build()
)config = (
HttpClientConfigBuilder()
.with_api_key('your-api-key', header_name='X-API-Key')
.build()
)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')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
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()
)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()
)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 responseRequest 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 β
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)
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}")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}")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}")# 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}")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'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# 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 integrationimport 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()
)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)
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)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'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-
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
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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}")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}")Contributions are welcome! Please follow these guidelines:
# 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# 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β All tests passing β Code coverage > 90% β Ruff linting passing β Type hints added β Documentation updated β Changelog updated
- 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
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
This project is licensed under the MIT License - see the LICENSE file for details.
- Built on top of the excellent requests library
- Inspired by enterprise API client requirements
- Thanks to all contributors and users
- Documentation: https://requestforge.readthedocs.io
- Issues: GitHub Issues
- Discussions: GitHub Discussions
PyPI: https://pypi.org/project/requestforge/ GitHub: https://github.com/baratihd/requestforge Documentation: https://requestforge.readthedocs.io Changelog: CHANGELOG.md