""" Production environment configuration for Aniworld application. This module provides production-specific settings including security hardening, performance optimizations, and operational configurations. Environment Variables: JWT_SECRET_KEY: Secret key for JWT token signing (REQUIRED) PASSWORD_SALT: Salt for password hashing (REQUIRED) DATABASE_URL: Production database connection string LOG_LEVEL: Logging level (default: WARNING) CORS_ORIGINS: Comma-separated list of allowed CORS origins API_RATE_LIMIT: API rate limit per minute (default: 60) WORKERS: Number of Uvicorn worker processes (default: 4) WORKER_TIMEOUT: Worker timeout in seconds (default: 120) """ from typing import List from pydantic import Field, validator from pydantic_settings import BaseSettings class ProductionSettings(BaseSettings): """Production environment configuration settings.""" # ============================================================================ # Security Settings # ============================================================================ jwt_secret_key: str = Field(..., env="JWT_SECRET_KEY") """Secret key for JWT token signing. MUST be set in production.""" password_salt: str = Field(..., env="PASSWORD_SALT") """Salt for password hashing. MUST be set in production.""" master_password_hash: str = Field(..., env="MASTER_PASSWORD_HASH") """Hash of the master password for authentication.""" allowed_hosts: List[str] = Field( default=["*"], env="ALLOWED_HOSTS" ) """List of allowed hostnames for CORS and security checks.""" cors_origins: str = Field(default="", env="CORS_ORIGINS") """Comma-separated list of allowed CORS origins.""" # ============================================================================ # Database Settings # ============================================================================ database_url: str = Field( default="postgresql://user:password@localhost/aniworld", env="DATABASE_URL" ) """Database connection URL. Defaults to PostgreSQL for production.""" database_pool_size: int = Field(default=20, env="DATABASE_POOL_SIZE") """Database connection pool size.""" database_max_overflow: int = Field(default=10, env="DATABASE_MAX_OVERFLOW") """Maximum overflow connections for database pool.""" database_pool_recycle: int = Field( default=3600, env="DATABASE_POOL_RECYCLE" ) """Recycle database connections every N seconds.""" # ============================================================================ # API Settings # ============================================================================ api_rate_limit: int = Field(default=60, env="API_RATE_LIMIT") """API rate limit per minute per IP address.""" api_timeout: int = Field(default=30, env="API_TIMEOUT") """API request timeout in seconds.""" # ============================================================================ # Logging Settings # ============================================================================ log_level: str = Field(default="WARNING", env="LOG_LEVEL") """Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).""" log_file: str = Field(default="logs/production.log", env="LOG_FILE") """Path to production log file.""" log_rotation_size: int = Field(default=10_485_760, env="LOG_ROTATION_SIZE") """Log file rotation size in bytes (default: 10MB).""" log_retention_days: int = Field(default=30, env="LOG_RETENTION_DAYS") """Number of days to retain log files.""" # ============================================================================ # Performance Settings # ============================================================================ workers: int = Field(default=4, env="WORKERS") """Number of Uvicorn worker processes.""" worker_timeout: int = Field(default=120, env="WORKER_TIMEOUT") """Worker timeout in seconds.""" max_request_size: int = Field(default=104_857_600, env="MAX_REQUEST_SIZE") """Maximum request body size in bytes (default: 100MB).""" session_timeout_hours: int = Field(default=24, env="SESSION_TIMEOUT_HOURS") """Session timeout in hours.""" # ============================================================================ # Provider Settings # ============================================================================ default_provider: str = Field( default="aniworld.to", env="DEFAULT_PROVIDER" ) """Default content provider.""" provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT") """Provider request timeout in seconds.""" provider_retries: int = Field(default=3, env="PROVIDER_RETRIES") """Number of retry attempts for provider requests.""" # ============================================================================ # Download Settings # ============================================================================ max_concurrent_downloads: int = Field( default=3, env="MAX_CONCURRENT_DOWNLOADS" ) """Maximum concurrent downloads.""" download_timeout: int = Field(default=3600, env="DOWNLOAD_TIMEOUT") """Download timeout in seconds (default: 1 hour).""" # ============================================================================ # Application Paths # ============================================================================ anime_directory: str = Field(..., env="ANIME_DIRECTORY") """Directory where anime is stored.""" temp_directory: str = Field(default="/tmp/aniworld", env="TEMP_DIRECTORY") """Temporary directory for downloads and cache.""" # ============================================================================ # Validators # ============================================================================ @validator("log_level") @classmethod def validate_log_level(cls, v: str) -> str: """Validate log level is valid.""" valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} if v.upper() not in valid_levels: raise ValueError( f"Invalid log level '{v}'. Must be one of: {valid_levels}" ) return v.upper() @validator("database_url") @classmethod def validate_database_url(cls, v: str) -> str: """Validate database URL is set and not SQLite.""" if not v or v.startswith("sqlite"): raise ValueError( "Production database must not use SQLite. " "Use PostgreSQL or MySQL instead." ) return v @validator("cors_origins") @classmethod def parse_cors_origins(cls, v: str) -> str: """Parse comma-separated CORS origins.""" if not v: return "" return v # ============================================================================ # Configuration # ============================================================================ class Config: """Pydantic config.""" env_file = ".env.production" extra = "ignore" case_sensitive = False # ============================================================================ # Properties # ============================================================================ @property def parsed_cors_origins(self) -> List[str]: """Get parsed CORS origins as list.""" if not self.cors_origins: return ["http://localhost", "http://127.0.0.1"] return [origin.strip() for origin in self.cors_origins.split(",")] @property def is_production(self) -> bool: """Check if running in production mode.""" return True @property def debug_enabled(self) -> bool: """Check if debug mode is enabled.""" return False @property def reload_enabled(self) -> bool: """Check if auto-reload is enabled.""" return False def get_production_settings() -> ProductionSettings: """ Get production settings instance. This is a factory function that should be called when settings are needed, rather than instantiating at module level to avoid requiring all environment variables at import time. Returns: ProductionSettings instance configured from environment variables Raises: ValidationError: If required environment variables are missing """ return ProductionSettings()