"""Typed configuration via pydantic-settings (12-factor). Base `Settings` carries everything shared across services. A service imports this and reads only the fields it needs. All values come from environment variables (or `.env`); nothing is hardcoded. Per-service entrypoints may subclass `Settings` to add their own fields (e.g. the bot adds `BOT_TOKEN`, `API_URL`). See docs/ARCHITECTURE.md §11 for the full env reference. """ from __future__ import annotations from functools import lru_cache from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict Env = Literal["dev", "staging", "prod"] RefundPolicy = Literal["all", "infra_only"] class Settings(BaseSettings): """Application configuration. See docs/ARCHITECTURE.md §11.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, ) # --- runtime --- env: Env = "dev" log_level: str = "INFO" # --- datastores / brokers --- database_url: str = Field(..., description="async DSN: postgresql+asyncpg://...") redis_url: str = "redis://redis:6379/0" rabbitmq_url: str = Field(..., description="amqp://user:pass@host:5672//") # --- RabbitMQ tuning --- mq_prefetch_extract: int = 1 mq_prefetch_analyze: int = 3 mq_prefetch_notify: int = 5 mq_max_attempts: int = 5 mq_retry_base_ms: int = 2000 # --- object storage (MinIO) --- s3_endpoint_url: str s3_access_key: str s3_secret_key: str s3_bucket: str = "contract-check-docs" s3_region: str = "us-east-1" s3_server_side_encryption: bool = False doc_retention_days: int = 7 text_retention_days: int = 30 # --- billing --- refund_policy: RefundPolicy = "all" # --- observability (empty disables) --- sentry_dsn: str = "" otel_exporter_otlp_endpoint: str = "" otel_service_name: str = "contract-check" # --- LLM provider --- llm_provider: str = "ollama_cloud" ollama_host: str = "" ollama_api_key: str = "" ollama_model: str = "qwen2.5:14b" ollama_fallback_model: str = "qwen2.5:7b" ollama_temperature: float = 0.2 ollama_num_predict: int = 3072 ollama_timeout: float = 120.0 ollama_max_concurrency: int = 3 # --- API --- api_host: str = "0.0.0.0" api_port: int = 8000 api_metrics_port: int = 9100 b2b_default_rate_limit_rps: int = 3 # --- auth (JWT + Telegram identity verification) --- telegram_bot_token: str = Field( "", description="Telegram bot token; used to verify Login Widget / Mini App signatures" ) jwt_secret: str = Field(..., description="HS256 secret for signing user JWTs") jwt_algorithm: str = "HS256" jwt_access_ttl_minutes: int = 24 * 60 # 24 hours default; tune per env jwt_refresh_ttl_days: int = 30 # refresh-token lifetime for webUI auth # --- webUI auth (email + password) --- web_auth_enabled: bool = Field( default=True, description="Toggle for /api/v1/auth/{register,login,...} email-password routes", ) password_reset_ttl_minutes: int = 60 # reset-token validity window web_app_base_url: str = Field( default="http://localhost:5173", description="Base URL of the webUI SPA — used to build password-reset links", ) # Minimum password length enforced at register / reset. password_min_length: int = 8 # --- admin panel (server-rendered, mounted at /admin in the api) --- web_admin_enabled: bool = Field( default=True, description="Toggle for the /admin management UI (users, future subscriptions)", ) admin_required_role: str = Field( default="admin", description="users.role value required to enter /admin (must match the DB CHECK constraint)", ) admin_default_email: str = Field( default="admin@contract-check.local", description="Default admin panel login email (auto-created if no admin exists)", ) admin_default_password: str = Field( default="", description="Default admin panel password. Empty disables auto-admin creation.", ) # --- SMTP (notification transport; empty host disables sending) --- smtp_host: str = "" smtp_port: int = 587 smtp_username: str = "" smtp_password: str = "" smtp_from: str = Field( default="no-reply@contract-check.local", description="From: address used by the notify worker", ) smtp_use_tls: bool = True # STARTTLS on port 587; set false for plain SMTP # --- logging --- log_format: str = "json" # json | console # --- analysis --- chunk_size_chars: int = 10000 @property def json_logs(self) -> bool: """JSON logs in staging/prod, pretty console in dev. Override with LOG_FORMAT=json|console. """ fmt = self.log_format.lower() if fmt == "console": return False if fmt == "json": return True return self.env != "dev" @property def log_format_value(self) -> str: """Resolved log format: json in prod/staging unless explicitly console.""" fmt = self.log_format.lower() if fmt in ("json", "console"): return fmt return "json" if self.env != "dev" else "console" @lru_cache def get_settings() -> Settings: """Cached settings singleton. Call `get_settings.cache_clear()` to reset.""" return Settings() # type: ignore[call-arg]