| """ |
| Shortlist β Application Configuration |
| |
| All secrets and config loaded from environment variables. |
| NEVER hardcode secrets. NEVER commit .env files. |
| """ |
|
|
| from pydantic_settings import BaseSettings |
| from pydantic import Field, field_validator, computed_field |
| from functools import lru_cache |
| from typing import Optional |
| import secrets |
|
|
| class Settings(BaseSettings): |
| """ |
| Immutable, validated application configuration. |
| All values sourced from environment variables or .env file. |
| """ |
|
|
| |
| APP_NAME: str = "Shortlist" |
| APP_VERSION: str = "0.1.0" |
| ENVIRONMENT: str = Field(default="development", pattern="^(development|testing|staging|production)$") |
| DEBUG: bool = False |
| LOG_LEVEL: str = Field(default="INFO", pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$") |
|
|
| |
| SECRET_KEY: str = Field(default_factory=lambda: secrets.token_urlsafe(64)) |
| ALLOWED_ORIGINS: str = Field(default="http://localhost:3000") |
| RATE_LIMIT_PER_MINUTE: int = 60 |
| RATE_LIMIT_BURST: int = 10 |
| MAX_REQUEST_SIZE_MB: int = 10 |
|
|
| @computed_field |
| @property |
| def allowed_origins_list(self) -> list[str]: |
| """Parse comma-separated origins into a list.""" |
| return [o.strip() for o in self.ALLOWED_ORIGINS.split(",") if o.strip()] |
|
|
| |
| SUPABASE_URL: str = "" |
| SUPABASE_ANON_KEY: str = "" |
| SUPABASE_SERVICE_KEY: str = "" |
| SUPABASE_JWT_SECRET: str = "" |
|
|
| |
| GROQ_API_KEY: str = "" |
| OPENAI_API_KEY: Optional[str] = None |
|
|
| |
| LLM_ANALYSIS_MODEL: str = "llama-3.3-70b-versatile" |
| LLM_CODE_MODEL: str = "llama-3.3-70b-versatile" |
| LLM_TEMPERATURE: float = Field(default=0.15, ge=0.0, le=2.0) |
| LLM_MAX_TOKENS: int = Field(default=8192, ge=256, le=32768) |
|
|
| |
| GITHUB_TOKEN: Optional[str] = None |
| REPO_CLONE_TIMEOUT_SECONDS: int = 120 |
| REPO_ANALYSIS_TIMEOUT_SECONDS: int = 60 |
| REPO_MAX_SIZE_MB: int = 500 |
| TEMP_CLONE_DIR: str = "/tmp/shortlist_repos" |
|
|
| |
| @field_validator("SUPABASE_URL") |
| @classmethod |
| def validate_supabase_url(cls, v: str) -> str: |
| if v and not v.startswith("https://"): |
| raise ValueError("SUPABASE_URL must use HTTPS") |
| return v |
|
|
| @field_validator("SECRET_KEY") |
| @classmethod |
| def validate_secret_key(cls, v: str, info) -> str: |
| """Ensure production uses an explicitly set secret key.""" |
| env = info.data.get("ENVIRONMENT", "development") |
| if env == "production" and (not v or len(v) < 32): |
| raise ValueError( |
| "SECRET_KEY must be explicitly set (β₯32 chars) in production. " |
| "Generate with: python -c \"import secrets; print(secrets.token_urlsafe(64))\"" |
| ) |
| return v |
|
|
| model_config = { |
| "env_file": ".env", |
| "env_file_encoding": "utf-8", |
| "case_sensitive": True, |
| "extra": "ignore", |
| } |
|
|
| @lru_cache() |
| def get_settings() -> Settings: |
| """ |
| Singleton settings instance β cached after first load. |
| Reload requires process restart (secure by design). |
| """ |
| return Settings() |
|
|