Files
fastapi-traffic/examples/11_config_loader.py
zanewalker fb23e3c7cf 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)
2026-02-01 13:59:32 +00:00

442 lines
15 KiB
Python

"""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)