from __future__ import annotations import os from dataclasses import dataclass from urllib.parse import urlparse from dotenv import load_dotenv class ConfigError(Exception): """Raised when configuration is missing or invalid.""" @dataclass class Config: base_url: str token: str | None = None session_cookie: str | None = None csrf_token: str | None = None total_timeout: float | None = None connect_timeout: float | None = None read_timeout: float | None = None username: str | None = None password: str | None = None @property def auth_header(self) -> dict[str, str]: return {"Authorization": f"Token {self.token}"} if self.token else {} def _parse_timeout(env_key: str) -> float | None: raw = os.getenv(env_key) if not raw: return None try: return float(raw) except ValueError as exc: # noqa: B904 - more readable error raise ConfigError(f"{env_key} must be a number (seconds).") from exc def load_config() -> Config: """Load config from environment or .env and validate values.""" load_dotenv() base_url = os.getenv("CTFD_URL") token = os.getenv("CTFD_TOKEN") session_cookie = os.getenv("CTFD_SESSION") csrf_token = os.getenv("CTFD_CSRF_TOKEN") username = os.getenv("CTFD_USERNAME") password = os.getenv("CTFD_PASSWORD") total_timeout = _parse_timeout("CTFD_TIMEOUT") connect_timeout = _parse_timeout("CTFD_CONNECT_TIMEOUT") read_timeout = _parse_timeout("CTFD_READ_TIMEOUT") if not base_url: raise ConfigError( "CTFD_URL is not set. Provide full URL to your CTFd instance." ) parsed = urlparse(base_url) if not parsed.scheme or not parsed.netloc: raise ConfigError("CTFD_URL must be a full URL, e.g. https://ctfd.example.com") # Precedence: username/password > token > session cookie. if username and password: token = None session_cookie = None csrf_token = None elif token: session_cookie = None csrf_token = None if not token and not session_cookie and not (username and password): raise ConfigError( "Set CTFD_TOKEN, CTFD_SESSION or both CTFD_USERNAME/CTFD_PASSWORD." ) return Config( base_url.rstrip("/"), token.strip() if token else None, session_cookie.strip() if session_cookie else None, csrf_token.strip() if csrf_token else None, total_timeout=total_timeout, connect_timeout=connect_timeout, read_timeout=read_timeout, username=username.strip() if username else None, password=password.strip() if password else None, )