import os from functools import lru_cache from urllib.parse import quote as urlquote from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") # URLs LOCAL_URL: str = "http://localhost" PUBLIC_URL: str = "" # Auth / JWT JWT_SECRET_KEY: str = "change-me-in-production" JWT_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days # Symmetric encryption for sensitive user prefs (ntfy password, etc.) # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # Falls back to JWT_SECRET_KEY derivation if not set (not recommended for production) ENCRYPTION_SECRET_KEY: str = "" # Database — built automatically from components (supports any characters in password) POSTGRES_USER: str = "congress" POSTGRES_PASSWORD: str = "congress" POSTGRES_DB: str = "pocketveto" POSTGRES_HOST: str = "postgres" POSTGRES_PORT: int = 5432 # Path to a file containing the raw DB password (any chars, no escaping needed). # When set, this takes priority over POSTGRES_PASSWORD. POSTGRES_PASSWORD_FILE: str = "" # Override these only if connecting to an external DB not managed by Docker Compose DATABASE_URL: str = "" SYNC_DATABASE_URL: str = "" # Redis REDIS_URL: str = "redis://redis:6379/0" # api.data.gov (shared key for Congress.gov and GovInfo) DATA_GOV_API_KEY: str = "" CONGRESS_POLL_INTERVAL_MINUTES: int = 30 # LLM LLM_PROVIDER: str = "openai" # openai | anthropic | gemini | ollama OPENAI_API_KEY: str = "" OPENAI_MODEL: str = "gpt-4o-mini" # gpt-4o-mini: excellent JSON quality at ~10x lower cost than gpt-4o ANTHROPIC_API_KEY: str = "" ANTHROPIC_MODEL: str = "claude-sonnet-4-6" # Sonnet matches Opus for structured tasks at ~5x lower cost GEMINI_API_KEY: str = "" GEMINI_MODEL: str = "gemini-2.0-flash" OLLAMA_BASE_URL: str = "http://host.docker.internal:11434" OLLAMA_MODEL: str = "llama3.1" # Max LLM requests per minute — Celery enforces this globally across all workers. # Defaults: free Gemini=15 RPM, Anthropic paid=50 RPM, OpenAI paid=500 RPM. # Lower this in .env if you hit rate limit errors on a restricted tier. LLM_RATE_LIMIT_RPM: int = 50 # Google Civic Information API (zip → representative lookup) # Free key: https://console.cloud.google.com/apis/library/civicinfo.googleapis.com CIVIC_API_KEY: str = "" # News NEWSAPI_KEY: str = "" # pytrends PYTRENDS_ENABLED: bool = True @model_validator(mode="after") def check_secrets_and_build_db_url(self) -> "Settings": if self.JWT_SECRET_KEY == "change-me-in-production": raise ValueError( "JWT_SECRET_KEY must be set to a secure random value in .env. " "Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\"" ) if not self.DATABASE_URL: if self.POSTGRES_PASSWORD_FILE and os.path.isfile(self.POSTGRES_PASSWORD_FILE): with open(self.POSTGRES_PASSWORD_FILE) as f: raw_pw = f.read().strip() else: raw_pw = self.POSTGRES_PASSWORD pw = urlquote(raw_pw, safe="") base = f"{self.POSTGRES_USER}:{pw}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" self.DATABASE_URL = f"postgresql+asyncpg://{base}" self.SYNC_DATABASE_URL = f"postgresql://{base}" return self # SMTP (Email notifications) SMTP_HOST: str = "" SMTP_PORT: int = 587 SMTP_USER: str = "" SMTP_PASSWORD: str = "" SMTP_FROM: str = "" # Defaults to SMTP_USER if blank SMTP_STARTTLS: bool = True @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()