"""Runtime configuration read from environment variables. AppFactory injects variables through the generated runtime .env file, so every option below has a safe default and the service starts without any variable set. """ import os from dataclasses import dataclass, field from functools import lru_cache def _bool(name: str, default: bool) -> bool: raw = os.getenv(name) if raw is None or raw.strip() == "": return default return raw.strip().lower() in ("1", "true", "yes", "on") def _int(name: str, default: int) -> int: raw = os.getenv(name) if raw is None or raw.strip() == "": return default try: return int(raw) except ValueError: return default def _list(name: str) -> list[str]: raw = os.getenv(name, "") return [item.strip() for item in raw.split(",") if item.strip()] @dataclass(frozen=True) class Settings: app_name: str = field(default_factory=lambda: os.getenv("APP_NAME", "html-to-pdf")) app_version: str = field(default_factory=lambda: os.getenv("APP_VERSION", "1.0.0")) root_path: str = field( default_factory=lambda: os.getenv("ROOT_PATH") or os.getenv("BASE_PATH") or "" ) log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO").upper()) # Job queue workers: int = field(default_factory=lambda: _int("WORKERS", 2)) queue_max_size: int = field(default_factory=lambda: _int("QUEUE_MAX_SIZE", 100)) sync_timeout_seconds: int = field(default_factory=lambda: _int("SYNC_TIMEOUT_SECONDS", 60)) job_result_ttl_seconds: int = field(default_factory=lambda: _int("JOB_RESULT_TTL_SECONDS", 3600)) storage_dir: str = field(default_factory=lambda: os.getenv("STORAGE_DIR", "/tmp/html-to-pdf")) # Engines default_engine: str = field(default_factory=lambda: os.getenv("DEFAULT_ENGINE", "auto")) chromium_enabled: bool = field(default_factory=lambda: _bool("CHROMIUM_ENABLED", True)) chromium_restart_after_jobs: int = field( default_factory=lambda: _int("CHROMIUM_RESTART_AFTER_JOBS", 50) ) # Network fetch_timeout_seconds: int = field(default_factory=lambda: _int("FETCH_TIMEOUT_SECONDS", 30)) asset_timeout_seconds: int = field(default_factory=lambda: _int("ASSET_TIMEOUT_SECONDS", 10)) max_redirects: int = field(default_factory=lambda: _int("MAX_REDIRECTS", 5)) # SSRF protection. Blocking is on by default and can be widened or narrowed. ssrf_block_private: bool = field(default_factory=lambda: _bool("SSRF_BLOCK_PRIVATE", True)) ssrf_extra_blocked_cidrs: list[str] = field(default_factory=lambda: _list("SSRF_EXTRA_BLOCKED_CIDRS")) ssrf_allowed_hosts: list[str] = field(default_factory=lambda: _list("SSRF_ALLOWED_HOSTS")) # Optional limits. Zero means no limit, which is the default on purpose. max_pages: int = field(default_factory=lambda: _int("MAX_PAGES", 0)) max_html_bytes: int = field(default_factory=lambda: _int("MAX_HTML_BYTES", 0)) max_render_seconds: int = field(default_factory=lambda: _int("MAX_RENDER_SECONDS", 0)) # Callback delivery callback_timeout_seconds: int = field(default_factory=lambda: _int("CALLBACK_TIMEOUT_SECONDS", 15)) callback_retries: int = field(default_factory=lambda: _int("CALLBACK_RETRIES", 3)) @lru_cache(maxsize=1) def get_settings() -> Settings: return Settings()