from pydantic_settings import BaseSettings from pydantic import model_validator from pydantic import ConfigDict from typing import Optional class Settings(BaseSettings): model_config = ConfigDict(extra="ignore") # Required — fail fast at import time if unset. DATABASE_URL: str = "" SENDGRID_API_KEY: Optional[str] = None SENDGRID_FROM_EMAIL: str = "peter@research.bike" SENDGRID_REPLY_TO: str = "peter@research.bike" EMAIL_BACKEND: str = "console" LUCKY_CA_URL: str = "https://www.luckyncal.com" # Swiftly product API (replaces Playwright path). The bearer JWT is # auto-minted at request time via app.services.swiftly_auth.get_token(). LUCKY_STORE_ID: str = "757" SWIFTLY_API_BASE: str = "https://prod.swiftlyapi.net" SWIFTLY_CATEGORIES_URL: str = "https://luckysupermarkets.com/categories" AI_IMAGE_ENABLED: bool = False AI_IMAGE_PROVIDER: Optional[str] = None AI_IMAGE_API_KEY: Optional[str] = None LOG_LEVEL: str = "INFO" SECRET_KEY: str = "dev-secret-key" # Auth (R1-B+D) ADMIN_TOKEN: str = "" SESSION_PASSWORD: str = "" ADMIN_EMAIL: str = "" APP_BASE_URL: str = "http://localhost" # Ollama Cloud LLM (used for ingredient→grocery LLM matching second pass) OLLAMA_BASE_URL: str = "https://ollama.com/v1" OLLAMA_API_KEY: Optional[str] = None OLLAMA_MODEL: str = "gpt-oss:20b" # Sprint 12: Spoonacular external recipe search. Free tier is # 150 points/day. ComplexSearch = 1 point + 0.01 per result. The # /information endpoint = 1 point per call. The recipe_search # router gates calls to stay under 140 points/day to leave a # safety margin. SPOONACULAR_API_KEY: Optional[str] = None FAMILY_EMAIL_1: Optional[str] = None FAMILY_EMAIL_2: Optional[str] = None RECIPES_EMAIL: Optional[str] = None model_config = ConfigDict(extra="ignore", env_file=".env") @model_validator(mode="after") def _require_database_url(self) -> "Settings": if not self.DATABASE_URL: raise RuntimeError("DATABASE_URL is required") return self settings = Settings()