""" Application configuration management. Simple configuration with sensible defaults. SQLite by default for development, PostgreSQL supported via DATABASE_URL environment variable. ## Usage from app.core.config import get_config config = get_config() print(config.DATABASE_URL) ## Environment Variables All configuration values can be overridden via environment variables. The .env file in the project root is also loaded automatically. | Variable | Default | Description | |---|---|---| | APP_NAME | MTG Online | Application display name | | APP_VERSION | 0.2.0 | Application version | | DEBUG | false | Enable debug mode | | SECRET_KEY | change-me-in-production | Application secret key | | JWT_SECRET_KEY | (same as SECRET_KEY) | JWT signing key | | DATABASE_URL | sqlite:///./mtgonline.db | Database connection string | | JWT_ALGORITHM | HS256 | JWT signing algorithm | | JWT_ACCESS_TOKEN_EXPIRE_MINUTES | 60 | Access token lifetime | | JWT_REFRESH_TOKEN_EXPIRE_DAYS | 7 | Refresh token lifetime | | CORS_ORIGINS | ["http://localhost:3000", "http://localhost:8000"] | Allowed CORS origins | | BCRYPT_ROUNDS | 12 | Bcrypt hashing rounds | | LOG_LEVEL | INFO | Logging level | | LOG_FORMAT | text | Log output format (text or json) | | DATA_DIR | /app/data | Data directory for MTGJSON files | | UPLOAD_DIR | /app/uploads | Upload directory | """ import os from typing import Optional def _load_env_file(path: str) -> None: """Load environment variables from a .env file if it exists. Args: path: Absolute or relative path to the .env file. """ if os.path.isfile(path): with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue if "=" not in line: continue key, _, value = line.partition("=") key = key.strip() value = value.strip().strip("\"'") if key and value and key not in os.environ: os.environ[key] = value # Load .env from project root (one level up from backend/app/core/) _project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) _env_path = os.path.join(_project_root, ".env") _load_env_file(_env_path) # Also try backend/.env _backend_env = os.path.join(_project_root, "..", ".env") _load_env_file(os.path.abspath(_backend_env)) class Config: """Application configuration with sensible defaults. All values can be overridden via environment variables. This is a simple class (not pydantic-settings) for minimal dependencies. Attributes: APP_NAME: Application display name. APP_VERSION: Application version string. DEBUG: Enable debug mode (enables SQL echo, etc.). SECRET_KEY: Application secret key for signing. JWT_SECRET_KEY: JWT signing key (falls back to SECRET_KEY). DATABASE_URL: SQLAlchemy database connection string. Defaults to SQLite for development. JWT_ALGORITHM: JWT signing algorithm (default HS256). JWT_ACCESS_TOKEN_EXPIRE_MINUTES: Access token lifetime in minutes. JWT_REFRESH_TOKEN_EXPIRE_DAYS: Refresh token lifetime in days. CORS_ORIGINS: List of allowed CORS origin URLs. BCRYPT_ROUNDS: Number of bcrypt hashing rounds. LOG_LEVEL: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). LOG_FORMAT: Log output format (text or json). DATA_DIR: Directory for MTGJSON data files. UPLOAD_DIR: Directory for user uploads. """ # Application APP_NAME: str = "MTG Online" APP_VERSION: str = "0.2.0" DEBUG: bool = False SECRET_KEY: str = "change-me-in-production" JWT_SECRET_KEY: str = "" # Falls back to SECRET_KEY if empty # Database - single unified connection string DATABASE_URL: str = "sqlite+aiosqlite:///./mtgonline.db" # JWT Configuration JWT_ALGORITHM: str = "HS256" JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7 # CORS CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:8000"] # Security BCRYPT_ROUNDS: int = 12 # Logging LOG_LEVEL: str = "INFO" LOG_FORMAT: str = "text" # Directories DATA_DIR: str = "/app/data" UPLOAD_DIR: str = "/app/uploads" def __init__(self) -> None: """Initialize configuration from environment variables.""" self._apply_env_overrides() def _apply_env_overrides(self) -> None: """Override configuration values from environment variables.""" env_map: dict[str, type] = { "APP_NAME": str, "APP_VERSION": str, "DEBUG": bool, "SECRET_KEY": str, "JWT_SECRET_KEY": str, "DATABASE_URL": str, "JWT_ALGORITHM": str, "JWT_ACCESS_TOKEN_EXPIRE_MINUTES": int, "JWT_REFRESH_TOKEN_EXPIRE_DAYS": int, "BCRYPT_ROUNDS": int, "LOG_LEVEL": str, "LOG_FORMAT": str, "DATA_DIR": str, "UPLOAD_DIR": str, } for attr, type_cast in env_map.items(): value = os.environ.get(attr) if value is not None: if type_cast == bool: value = value.lower() in ("true", "1", "yes", "on") else: value = type_cast(value) setattr(self, attr, value) # CORS_ORIGINS: parse comma-separated from env or keep default list cors_env = os.environ.get("CORS_ORIGINS") if cors_env: self.CORS_ORIGINS = [ origin.strip() for origin in cors_env.split(",") if origin.strip() ] @property def jwt_secret(self) -> str: """Get the effective JWT secret key. Returns JWT_SECRET_KEY if set, otherwise falls back to SECRET_KEY. Returns: The JWT secret key string. """ return self.JWT_SECRET_KEY or self.SECRET_KEY @property def is_sqlite(self) -> bool: """Check if the database is SQLite. Returns: True if using SQLite, False otherwise. """ return self.DATABASE_URL.startswith("sqlite") @property def is_postgresql(self) -> bool: """Check if the database is PostgreSQL. Returns: True if using PostgreSQL, False otherwise. """ return "postgresql" in self.DATABASE_URL # Singleton instance _config_instance: Optional[Config] = None def get_config() -> Config: """Get the application configuration singleton. Returns a cached Config instance. Subsequent calls return the same object. Returns: The application configuration instance. Example: >>> config = get_config() >>> print(config.DATABASE_URL) sqlite:///./mtgonline.db """ global _config_instance if _config_instance is None: _config_instance = Config() return _config_instance