442 lines
15 KiB
Python
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)
|