feat: add configuration loader for .env and JSON files
- Add ConfigLoader class for loading RateLimitConfig and GlobalConfig - Support .env files with FASTAPI_TRAFFIC_* prefixed variables - Support JSON configuration files with type validation - Add convenience functions: load_rate_limit_config, load_global_config - Add load_rate_limit_config_from_env, load_global_config_from_env - Support custom environment variable prefixes - Add comprehensive error handling with ConfigurationError - Add 47 tests for configuration loading - Add example 11_config_loader.py with 9 usage patterns - Update examples/README.md with config loader documentation - Update CHANGELOG.md with new feature - Fix typo in limiter.py (errant 'fi' on line 4)
This commit is contained in:
441
examples/11_config_loader.py
Normal file
441
examples/11_config_loader.py
Normal file
@@ -0,0 +1,441 @@
|
||||
"""Examples demonstrating configuration loading from .env and JSON files.
|
||||
|
||||
This module shows how to load rate limiting configuration from external files,
|
||||
making it easy to manage settings across different environments (dev, staging, prod).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from fastapi_traffic import (
|
||||
ConfigLoader,
|
||||
MemoryBackend,
|
||||
RateLimiter,
|
||||
RateLimitExceeded,
|
||||
load_global_config,
|
||||
load_global_config_from_env,
|
||||
load_rate_limit_config,
|
||||
load_rate_limit_config_from_env,
|
||||
rate_limit,
|
||||
)
|
||||
from fastapi_traffic.core.config import GlobalConfig, RateLimitConfig
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
from fastapi_traffic.exceptions import ConfigurationError
|
||||
|
||||
# =============================================================================
|
||||
# Example 1: Loading RateLimitConfig from environment variables
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_env_variables() -> RateLimitConfig:
|
||||
"""Load rate limit config from environment variables.
|
||||
|
||||
Set these environment variables before running:
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT=100
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_WINDOW_SIZE=60.0
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_ALGORITHM=sliding_window_counter
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_KEY_PREFIX=myapi
|
||||
"""
|
||||
# Using the convenience function
|
||||
config = load_rate_limit_config_from_env(
|
||||
# You can provide overrides for values not in env vars
|
||||
limit=50, # Default if FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT not set
|
||||
)
|
||||
print(f"Loaded config: limit={config.limit}, window={config.window_size}s")
|
||||
return config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 2: Loading GlobalConfig from environment variables
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_global_config_env() -> GlobalConfig:
|
||||
"""Load global config from environment variables.
|
||||
|
||||
Set these environment variables:
|
||||
export FASTAPI_TRAFFIC_GLOBAL_ENABLED=true
|
||||
export FASTAPI_TRAFFIC_GLOBAL_DEFAULT_LIMIT=200
|
||||
export FASTAPI_TRAFFIC_GLOBAL_DEFAULT_WINDOW_SIZE=120.0
|
||||
export FASTAPI_TRAFFIC_GLOBAL_EXEMPT_IPS=127.0.0.1,10.0.0.1
|
||||
export FASTAPI_TRAFFIC_GLOBAL_EXEMPT_PATHS=/health,/metrics
|
||||
"""
|
||||
config = load_global_config_from_env()
|
||||
print(f"Global config: enabled={config.enabled}, limit={config.default_limit}")
|
||||
print(f"Exempt IPs: {config.exempt_ips}")
|
||||
print(f"Exempt paths: {config.exempt_paths}")
|
||||
return config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 3: Loading from .env file
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_dotenv_file() -> RateLimitConfig:
|
||||
"""Load rate limit config from a .env file.
|
||||
|
||||
Example .env file contents:
|
||||
# Rate limiting configuration
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT=100
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_WINDOW_SIZE=60.0
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_ALGORITHM=token_bucket
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_BURST_SIZE=20
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_KEY_PREFIX=api_v1
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_INCLUDE_HEADERS=true
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_ERROR_MESSAGE="Too many requests, please slow down"
|
||||
"""
|
||||
# Create a sample .env file for demonstration
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
f.write("# Rate limit configuration\n")
|
||||
f.write("FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT=100\n")
|
||||
f.write("FASTAPI_TRAFFIC_RATE_LIMIT_WINDOW_SIZE=60.0\n")
|
||||
f.write("FASTAPI_TRAFFIC_RATE_LIMIT_ALGORITHM=token_bucket\n")
|
||||
f.write("FASTAPI_TRAFFIC_RATE_LIMIT_BURST_SIZE=20\n")
|
||||
f.write('FASTAPI_TRAFFIC_RATE_LIMIT_ERROR_MESSAGE="Rate limit exceeded"\n')
|
||||
env_path = f.name
|
||||
|
||||
try:
|
||||
# Load using auto-detection (detects .env suffix)
|
||||
config = load_rate_limit_config(env_path)
|
||||
print(f"From .env: limit={config.limit}, algorithm={config.algorithm}")
|
||||
print(f"Burst size: {config.burst_size}")
|
||||
return config
|
||||
finally:
|
||||
Path(env_path).unlink()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 4: Loading from JSON file
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_json_file() -> RateLimitConfig:
|
||||
"""Load rate limit config from a JSON file.
|
||||
|
||||
Example config.json:
|
||||
{
|
||||
"limit": 500,
|
||||
"window_size": 300.0,
|
||||
"algorithm": "sliding_window_counter",
|
||||
"key_prefix": "production",
|
||||
"include_headers": true,
|
||||
"status_code": 429,
|
||||
"cost": 1
|
||||
}
|
||||
"""
|
||||
# Create a sample JSON file for demonstration
|
||||
config_data = {
|
||||
"limit": 500,
|
||||
"window_size": 300.0,
|
||||
"algorithm": "sliding_window_counter",
|
||||
"key_prefix": "production",
|
||||
"include_headers": True,
|
||||
"status_code": 429,
|
||||
"skip_on_error": False,
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
# Load using auto-detection (detects .json suffix)
|
||||
config = load_rate_limit_config(json_path)
|
||||
print(f"From JSON: limit={config.limit}, window={config.window_size}s")
|
||||
print(f"Algorithm: {config.algorithm.value}")
|
||||
return config
|
||||
finally:
|
||||
Path(json_path).unlink()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: Loading GlobalConfig from JSON
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_global_config_json() -> GlobalConfig:
|
||||
"""Load global config from a JSON file.
|
||||
|
||||
Example global_config.json:
|
||||
{
|
||||
"enabled": true,
|
||||
"default_limit": 1000,
|
||||
"default_window_size": 60.0,
|
||||
"default_algorithm": "sliding_window_counter",
|
||||
"key_prefix": "myapp",
|
||||
"include_headers": true,
|
||||
"exempt_ips": ["127.0.0.1", "::1", "10.0.0.0/8"],
|
||||
"exempt_paths": ["/health", "/ready", "/metrics", "/docs"]
|
||||
}
|
||||
"""
|
||||
config_data = {
|
||||
"enabled": True,
|
||||
"default_limit": 1000,
|
||||
"default_window_size": 60.0,
|
||||
"default_algorithm": "sliding_window_counter",
|
||||
"key_prefix": "myapp",
|
||||
"exempt_ips": ["127.0.0.1", "::1"],
|
||||
"exempt_paths": ["/health", "/ready", "/metrics"],
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
config = load_global_config(json_path)
|
||||
print(f"Global: enabled={config.enabled}, limit={config.default_limit}")
|
||||
print(f"Exempt paths: {config.exempt_paths}")
|
||||
return config
|
||||
finally:
|
||||
Path(json_path).unlink()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 6: Using ConfigLoader class with custom prefix
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_custom_prefix() -> RateLimitConfig:
|
||||
"""Use ConfigLoader with a custom environment variable prefix.
|
||||
|
||||
Useful when you want to namespace your config variables differently,
|
||||
e.g., for different services or to avoid conflicts.
|
||||
"""
|
||||
# Create a loader with custom prefix
|
||||
loader = ConfigLoader(env_prefix="MYAPP_RATELIMIT_")
|
||||
|
||||
# Simulated environment variables with custom prefix
|
||||
env_vars = {
|
||||
"MYAPP_RATELIMIT_RATE_LIMIT_LIMIT": "250",
|
||||
"MYAPP_RATELIMIT_RATE_LIMIT_WINDOW_SIZE": "30.0",
|
||||
"MYAPP_RATELIMIT_RATE_LIMIT_ALGORITHM": "fixed_window",
|
||||
}
|
||||
|
||||
config = loader.load_rate_limit_config_from_env(env_vars)
|
||||
print(f"Custom prefix: limit={config.limit}, algorithm={config.algorithm}")
|
||||
return config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 7: Validation and error handling
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_validation() -> None:
|
||||
"""Demonstrate configuration validation and error handling."""
|
||||
loader = ConfigLoader()
|
||||
|
||||
# Example 1: Invalid algorithm value
|
||||
try:
|
||||
loader.load_rate_limit_config_from_env(
|
||||
{
|
||||
"FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT": "100",
|
||||
"FASTAPI_TRAFFIC_RATE_LIMIT_ALGORITHM": "invalid_algo",
|
||||
}
|
||||
)
|
||||
except ConfigurationError as e:
|
||||
print(f"Validation error (invalid algorithm): {e}")
|
||||
|
||||
# Example 2: Invalid numeric value
|
||||
try:
|
||||
loader.load_rate_limit_config_from_env(
|
||||
{"FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT": "not_a_number"}
|
||||
)
|
||||
except ConfigurationError as e:
|
||||
print(f"Validation error (invalid number): {e}")
|
||||
|
||||
# Example 3: Missing required field
|
||||
try:
|
||||
loader.load_rate_limit_config_from_env(
|
||||
{"FASTAPI_TRAFFIC_RATE_LIMIT_WINDOW_SIZE": "60.0"}
|
||||
)
|
||||
except ConfigurationError as e:
|
||||
print(f"Validation error (missing limit): {e}")
|
||||
|
||||
# Example 4: Non-loadable field (callables can't be loaded from config)
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump({"limit": 100, "key_extractor": "some_function"}, f)
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
loader.load_rate_limit_config_from_json(json_path)
|
||||
except ConfigurationError as e:
|
||||
print(f"Validation error (non-loadable field): {e}")
|
||||
finally:
|
||||
Path(json_path).unlink()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 8: Environment-based configuration (dev/staging/prod)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def example_environment_based_config() -> RateLimitConfig:
|
||||
"""Load different configurations based on environment.
|
||||
|
||||
This pattern is useful for having different rate limits in
|
||||
development, staging, and production environments.
|
||||
"""
|
||||
env = os.getenv("APP_ENV", "development")
|
||||
|
||||
# In a real app, these would be actual files
|
||||
configs = {
|
||||
"development": {"limit": 1000, "window_size": 60.0, "skip_on_error": True},
|
||||
"staging": {"limit": 500, "window_size": 60.0, "skip_on_error": True},
|
||||
"production": {"limit": 100, "window_size": 60.0, "skip_on_error": False},
|
||||
}
|
||||
|
||||
config_data = configs.get(env, configs["development"])
|
||||
|
||||
# Create temp file with the config
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump(config_data, f)
|
||||
config_path = f.name
|
||||
|
||||
try:
|
||||
config = load_rate_limit_config(config_path)
|
||||
print(f"Environment '{env}': limit={config.limit}")
|
||||
return config
|
||||
finally:
|
||||
Path(config_path).unlink()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 9: Full FastAPI application with config loading
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def create_app_with_config() -> FastAPI:
|
||||
"""Create a FastAPI app with configuration loaded from files."""
|
||||
# In production, load from actual config files:
|
||||
# global_config = load_global_config("config/global.json")
|
||||
# rate_config = load_rate_limit_config("config/rate_limit.json")
|
||||
|
||||
# For this example, create inline configs
|
||||
global_config = GlobalConfig(
|
||||
enabled=True,
|
||||
default_limit=100,
|
||||
default_window_size=60.0,
|
||||
exempt_paths={"/health", "/docs", "/openapi.json"},
|
||||
)
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend, config=global_config)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await limiter.initialize()
|
||||
set_limiter(limiter)
|
||||
yield
|
||||
await limiter.close()
|
||||
|
||||
app = FastAPI(
|
||||
title="Config Loader Example",
|
||||
description="Rate limiting with external configuration",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(_: Request, exc: RateLimitExceeded) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": exc.message,
|
||||
"retry_after": exc.retry_after,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
@rate_limit(limit=10, window_size=60)
|
||||
async def root(_: Request) -> dict[str, str]:
|
||||
return {"message": "Hello from config-loaded app!"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
"""Health check - exempt from rate limiting."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get("/api/data")
|
||||
@rate_limit(limit=50, window_size=60)
|
||||
async def get_data(_: Request) -> dict[str, str]:
|
||||
return {"data": "Some API data"}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# Create the app instance
|
||||
app = create_app_with_config()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Run all examples
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def run_examples() -> None:
|
||||
"""Run all configuration loading examples."""
|
||||
print("=" * 60)
|
||||
print("FastAPI Traffic - Configuration Loader Examples")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n1. Loading from environment variables:")
|
||||
print("-" * 40)
|
||||
example_env_variables()
|
||||
|
||||
print("\n2. Loading GlobalConfig from environment:")
|
||||
print("-" * 40)
|
||||
example_global_config_env()
|
||||
|
||||
print("\n3. Loading from .env file:")
|
||||
print("-" * 40)
|
||||
example_dotenv_file()
|
||||
|
||||
print("\n4. Loading from JSON file:")
|
||||
print("-" * 40)
|
||||
example_json_file()
|
||||
|
||||
print("\n5. Loading GlobalConfig from JSON:")
|
||||
print("-" * 40)
|
||||
example_global_config_json()
|
||||
|
||||
print("\n6. Using custom environment prefix:")
|
||||
print("-" * 40)
|
||||
example_custom_prefix()
|
||||
|
||||
print("\n7. Validation and error handling:")
|
||||
print("-" * 40)
|
||||
example_validation()
|
||||
|
||||
print("\n8. Environment-based configuration:")
|
||||
print("-" * 40)
|
||||
example_environment_based_config()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--demo":
|
||||
# Run the demo examples
|
||||
run_examples()
|
||||
else:
|
||||
# Run the FastAPI app
|
||||
import uvicorn
|
||||
|
||||
print("Starting FastAPI app with config loader...")
|
||||
print("Run with --demo flag to see configuration examples")
|
||||
uvicorn.run(app, host="127.0.0.1", port=8011)
|
||||
@@ -5,13 +5,17 @@ This directory contains comprehensive examples demonstrating how to use the `fas
|
||||
## Basic Examples
|
||||
|
||||
### 01_quickstart.py
|
||||
|
||||
Minimal setup to get rate limiting working. Start here if you're new to the library.
|
||||
|
||||
- Basic backend and limiter setup
|
||||
- Exception handler for rate limit errors
|
||||
- Simple decorator usage
|
||||
|
||||
### 02_algorithms.py
|
||||
|
||||
Demonstrates all available rate limiting algorithms:
|
||||
|
||||
- **Fixed Window** - Simple, resets at fixed intervals
|
||||
- **Sliding Window** - Most precise, stores timestamps
|
||||
- **Sliding Window Counter** - Balance of precision and efficiency (default)
|
||||
@@ -19,13 +23,17 @@ Demonstrates all available rate limiting algorithms:
|
||||
- **Leaky Bucket** - Smooths out traffic
|
||||
|
||||
### 03_backends.py
|
||||
|
||||
Shows different storage backends:
|
||||
|
||||
- **MemoryBackend** - Fast, ephemeral (default)
|
||||
- **SQLiteBackend** - Persistent, single-instance
|
||||
- **RedisBackend** - Distributed, multi-instance
|
||||
|
||||
### 04_key_extractors.py
|
||||
|
||||
Custom key extractors for different rate limiting strategies:
|
||||
|
||||
- Rate limit by IP address (default)
|
||||
- Rate limit by API key
|
||||
- Rate limit by user ID
|
||||
@@ -34,7 +42,9 @@ Custom key extractors for different rate limiting strategies:
|
||||
- Composite keys (user + action)
|
||||
|
||||
### 05_middleware.py
|
||||
|
||||
Middleware-based rate limiting for global protection:
|
||||
|
||||
- Basic middleware setup
|
||||
- Custom configuration options
|
||||
- Path and IP exemptions
|
||||
@@ -43,35 +53,45 @@ Middleware-based rate limiting for global protection:
|
||||
## Advanced Examples
|
||||
|
||||
### 06_dependency_injection.py
|
||||
|
||||
Using FastAPI's dependency injection system:
|
||||
|
||||
- Basic rate limit dependency
|
||||
- Tier-based rate limiting
|
||||
- Combining multiple rate limits
|
||||
- Conditional exemptions
|
||||
|
||||
### 07_redis_distributed.py
|
||||
|
||||
Redis backend for distributed deployments:
|
||||
|
||||
- Multi-instance rate limiting
|
||||
- Shared counters across nodes
|
||||
- Health checks and statistics
|
||||
- Fallback to memory backend
|
||||
|
||||
### 08_tiered_api.py
|
||||
|
||||
Production-ready tiered API example:
|
||||
|
||||
- Free, Starter, Pro, Enterprise tiers
|
||||
- Different limits per tier
|
||||
- Feature gating based on tier
|
||||
- API key validation
|
||||
|
||||
### 09_custom_responses.py
|
||||
|
||||
Customizing rate limit responses:
|
||||
|
||||
- Custom JSON error responses
|
||||
- Logging/monitoring callbacks
|
||||
- Different response formats (JSON, HTML, plain text)
|
||||
- Rate limit headers
|
||||
|
||||
### 10_advanced_patterns.py
|
||||
|
||||
Real-world patterns and use cases:
|
||||
|
||||
- **Cost-based limiting** - Different operations cost different amounts
|
||||
- **Priority exemptions** - Premium users exempt from limits
|
||||
- **Resource-based limiting** - Limit by resource ID + user
|
||||
@@ -81,6 +101,23 @@ Real-world patterns and use cases:
|
||||
- **Time-of-day limits** - Peak vs off-peak hours
|
||||
- **Cascading limits** - Per-second, per-minute, per-hour
|
||||
|
||||
### 11_config_loader.py
|
||||
|
||||
Loading configuration from external files:
|
||||
|
||||
- **Environment variables** - Load from `FASTAPI_TRAFFIC_*` env vars
|
||||
- **.env files** - Load from dotenv files for local development
|
||||
- **JSON files** - Load from JSON for structured configuration
|
||||
- **Custom prefixes** - Use custom env var prefixes
|
||||
- **Validation** - Automatic type validation and error handling
|
||||
- **Environment-based config** - Different configs for dev/staging/prod
|
||||
|
||||
Run with `--demo` flag to see all configuration examples:
|
||||
|
||||
```bash
|
||||
python examples/11_config_loader.py --demo
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
Each example is a standalone FastAPI application. Run with:
|
||||
|
||||
Reference in New Issue
Block a user