promptlooper/backend/config.py
John Lightner 309bbacb5d MAESTRO: Create backend/config.py with Pydantic Settings and SQLite/in-process fallback
All 13 environment variables from the spec defined with proper defaults.
SQLite fallback when DATABASE_URL is unset, in-process queue flag when
REDIS_URL is unset, JWT_SECRET auto-generation, empty API_KEY normalization.
13 unit tests covering all configuration paths.
2026-04-07 01:46:30 -05:00

76 lines
2 KiB
Python

"""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()