"""PromptLooper configuration — Pydantic Settings loaded from environment.""" import secrets from pathlib import Path from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) # --- Database --- database_url: str | None = None # --- Redis --- redis_url: str | None = None # --- Server --- host: str = "0.0.0.0" port: int = 8400 # --- Auth --- jwt_secret: str = "" api_key: str | None = None # --- Default LLM Endpoint --- default_endpoint_url: str | None = None default_endpoint_key: str | None = None # --- Limits --- max_concurrent_runs: int = 4 max_tokens_per_sweep: int = 0 # 0 = unlimited # --- Storage --- data_dir: str = "/data" # --- MCP --- mcp_enabled: bool = True mcp_port: int = 8401 def model_post_init(self, __context: object) -> None: # Auto-generate JWT secret if not provided if not self.jwt_secret: self.jwt_secret = secrets.token_urlsafe(32) @property def effective_database_url(self) -> str: """Return DATABASE_URL or construct a SQLite URL from DATA_DIR.""" if self.database_url: return self.database_url db_path = Path(self.data_dir) / "promptlooper.db" return f"sqlite:///{db_path}" @property def is_sqlite(self) -> bool: return self.effective_database_url.startswith("sqlite") @property def use_in_process_queue(self) -> bool: """When Redis is unavailable, use in-process task execution.""" return self.redis_url is None @field_validator("api_key", mode="before") @classmethod def empty_string_to_none(cls, v: str | None) -> str | None: if v is not None and v.strip() == "": return None return v settings = Settings()