"""Application configuration using Pydantic Settings. This module provides environment-based configuration for the Mantimon TCG backend. Configuration values can be set via environment variables or .env files. Environment Detection: - dev: Local development (default) - staging: Pre-production testing - prod: Production environment Example .env file: ENVIRONMENT=dev DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mantimon REDIS_URL=redis://localhost:6379/0 SECRET_KEY=your-secret-key-here Usage: from app.config import settings print(settings.database_url) print(settings.is_production) """ from functools import lru_cache from typing import Literal from pydantic import Field, PostgresDsn, RedisDsn, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Application settings loaded from environment variables. All settings can be overridden via environment variables. Prefix: None (direct variable names) Attributes: environment: Current environment (dev/staging/prod). debug: Enable debug mode (auto-set based on environment). database_url: PostgreSQL connection URL. database_pool_size: Connection pool size. database_max_overflow: Max overflow connections. redis_url: Redis connection URL. redis_max_connections: Max Redis connections. secret_key: Secret key for JWT signing. jwt_algorithm: JWT signing algorithm. jwt_expire_minutes: JWT token expiration in minutes. google_client_id: Google OAuth client ID. google_client_secret: Google OAuth client secret. discord_client_id: Discord OAuth client ID. discord_client_secret: Discord OAuth client secret. cors_origins: Allowed CORS origins. """ model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # Environment environment: Literal["dev", "staging", "prod"] = Field( default="dev", description="Current environment", ) debug: bool = Field( default=False, description="Enable debug mode", ) # Database (PostgreSQL) database_url: PostgresDsn = Field( default="postgresql+asyncpg://mantimon:mantimon@localhost:5433/mantimon", description="PostgreSQL connection URL", ) database_pool_size: int = Field( default=5, ge=1, le=50, description="Database connection pool size", ) database_max_overflow: int = Field( default=10, ge=0, le=100, description="Max overflow connections beyond pool size", ) database_echo: bool = Field( default=False, description="Echo SQL statements (for debugging)", ) # Redis redis_url: RedisDsn = Field( default="redis://localhost:6380/0", description="Redis connection URL", ) redis_max_connections: int = Field( default=10, ge=1, le=100, description="Max Redis connections in pool", ) # Security secret_key: SecretStr = Field( default="dev-secret-key-change-in-production", description="Secret key for JWT and other cryptographic operations", ) jwt_algorithm: str = Field( default="HS256", description="JWT signing algorithm", ) jwt_expire_minutes: int = Field( default=30, ge=1, description="JWT access token expiration in minutes", ) jwt_refresh_expire_days: int = Field( default=7, ge=1, description="JWT refresh token expiration in days", ) # OAuth - Google google_client_id: str | None = Field( default=None, description="Google OAuth 2.0 client ID", ) google_client_secret: SecretStr | None = Field( default=None, description="Google OAuth 2.0 client secret", ) # OAuth - Discord discord_client_id: str | None = Field( default=None, description="Discord OAuth 2.0 client ID", ) discord_client_secret: SecretStr | None = Field( default=None, description="Discord OAuth 2.0 client secret", ) # CORS cors_origins: list[str] = Field( default=["http://localhost:3000", "http://localhost:3001", "http://localhost:5173"], description="Allowed CORS origins", ) # Base URL (for OAuth callbacks and external links) base_url: str = Field( default="http://localhost:8000", description="Base URL of the API server (for OAuth callbacks)", ) # Game Settings turn_timeout_seconds: int = Field( default=120, ge=30, le=600, description="Turn timeout in seconds (2 minutes default)", ) game_cache_ttl_seconds: int = Field( default=86400, # 24 hours ge=3600, le=604800, # 1 week max description="Game state cache TTL in Redis (seconds)", ) # Card Data card_data_path: str = Field( default="data/cards", description="Path to bundled card JSON files", ) # Admin admin_api_key: SecretStr | None = Field( default=None, description="API key for admin endpoints (required in production)", ) @field_validator("debug", mode="before") @classmethod def set_debug_from_environment(cls, v: bool, info) -> bool: """Auto-enable debug in dev environment if not explicitly set.""" if v is not None: return v env = info.data.get("environment", "dev") return env == "dev" @property def is_production(self) -> bool: """Check if running in production environment.""" return self.environment == "prod" @property def is_development(self) -> bool: """Check if running in development environment.""" return self.environment == "dev" @property def database_url_sync(self) -> str: """Get synchronous database URL (for Alembic migrations). Converts asyncpg:// to psycopg2:// for sync operations. """ url = str(self.database_url) return url.replace("postgresql+asyncpg://", "postgresql://") @lru_cache def get_settings() -> Settings: """Get cached settings instance. Uses lru_cache to ensure settings are only loaded once. Returns: Settings instance loaded from environment. Example: settings = get_settings() print(settings.database_url) """ return Settings() # Convenience alias for direct import settings = get_settings()