"""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 logging 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 """ 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" """ 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: 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 } """ 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: 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( # pyright: ignore[reportUnusedFunction] _: 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( # pyright: ignore[reportUnusedFunction] _: Request, ) -> dict[str, str]: return {"message": "Hello from config-loaded app!"} @app.get("/health") async def _health() -> dict[str, str]: # pyright: ignore[reportUnusedFunction] """Health check - exempt from rate limiting.""" return {"status": "healthy"} @app.get("/api/data") @rate_limit(limit=50, window_size=60) async def _get_data( # pyright: ignore[reportUnusedFunction] _: Request, ) -> dict[str, str]: return {"data": "Some API data"} return app app = create_app_with_config() # ============================================================================= # Run all examples # ============================================================================= logger = logging.getLogger(__name__) def run_examples() -> None: """Run all configuration loading examples.""" logging.basicConfig(level=logging.INFO, format="%(message)s") logger.info("=" * 60) logger.info("FastAPI Traffic - Configuration Loader Examples") logger.info("=" * 60) logger.info("\n1. Loading from environment variables:") logger.info("-" * 40) example_env_variables() logger.info("\n2. Loading GlobalConfig from environment:") logger.info("-" * 40) example_global_config_env() logger.info("\n3. Loading from .env file:") logger.info("-" * 40) example_dotenv_file() logger.info("\n4. Loading from JSON file:") logger.info("-" * 40) example_json_file() logger.info("\n5. Loading GlobalConfig from JSON:") logger.info("-" * 40) example_global_config_json() logger.info("\n6. Using custom environment prefix:") logger.info("-" * 40) example_custom_prefix() logger.info("\n7. Validation and error handling:") logger.info("-" * 40) example_validation() logger.info("\n8. Environment-based configuration:") logger.info("-" * 40) example_environment_based_config() logger.info("\n" + "=" * 60) logger.info("All examples completed!") logger.info("=" * 60) if __name__ == "__main__": import argparse import uvicorn parser = argparse.ArgumentParser(description="Config loader example") parser.add_argument( "--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)" ) parser.add_argument( "--port", type=int, default=8011, help="Port to bind to (default: 8011)" ) parser.add_argument( "--demo", action="store_true", help="Run configuration examples instead of server", ) args = parser.parse_args() if args.demo: run_examples() else: logging.basicConfig(level=logging.INFO, format="%(message)s") logger.info("Starting FastAPI app with config loader...") logger.info("Run with --demo flag to see configuration examples") uvicorn.run(app, host=args.host, port=args.port)