""" shared/config.py ──────────────── Application configuration loaded from environment variables. Uses Pydantic Settings so every variable is validated + type-safe. Supabase support: • DATABASE_URL should point to Supabase PostgreSQL (port 5432 direct, or port 6543 for the Connection Pooler in Transaction mode). • SUPABASE_URL / SUPABASE_ANON_KEY are optional — only needed if you use the Supabase Python client SDK for storage, auth, or realtime features. """ from __future__ import annotations from functools import lru_cache import os # Detect Kaggle environment and auto-load secrets into environment variables if "KAGGLE_KERNEL_RUN_TYPE" in os.environ or os.path.exists("/kaggle"): try: from kaggle_secrets import UserSecretsClient user_secrets = UserSecretsClient() for key in ["DATABASE_URL", "RABBITMQ_URL", "USE_MOCK_MODEL"]: try: val = user_secrets.get_secret(key) if val: os.environ[key] = val except Exception: pass except ImportError: pass from pathlib import Path from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict # Resolve absolute path to the .env file in the project root _ROOT_DIR = Path(__file__).resolve().parent.parent.parent _ENV_FILE = _ROOT_DIR / ".env" class Settings(BaseSettings): """ Central configuration object. Priority order (highest → lowest): 1. OS environment variables 2. .env file in the project root 3. Default values defined here """ model_config = SettingsConfigDict( env_file=str(_ENV_FILE), env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # ── Database ────────────────────────────────────────────────────────────── database_url: str = Field( default="sqlite+aiosqlite:///./bp_monitoring.db", description=( "Async database connection string. " "• postgresql+asyncpg://postgres.[ref]:[pw]@[host]:5432/postgres (Supabase direct) " "• postgresql+asyncpg://postgres.[ref]:[pw]@[host]:6543/postgres (Supabase pooler) " "• sqlite+aiosqlite:///./bp_monitoring.db (local dev)" ), ) # Connection pool tuning (ignored for SQLite) db_pool_size: int = Field( default=5, description=( "SQLAlchemy connection pool size. " "Supabase free tier allows up to 60 connections; keep this ≤ 10." ), ) db_max_overflow: int = Field( default=10, description="Extra connections allowed above pool_size during peak load.", ) db_pool_recycle: int = Field( default=1800, description="Recycle idle connections after N seconds (30 min default).", ) # ── Supabase (optional — for SDK features beyond raw SQL) ───────────────── supabase_url: str = Field( default="", description="Supabase project URL (https://[project-ref].supabase.co). Optional.", ) supabase_anon_key: str = Field( default="", description="Supabase anonymous/public API key. Optional.", ) # ── Message Broker ──────────────────────────────────────────────────────── rabbitmq_url: str = Field( default="amqp://guest:guest@localhost:5672/", description="RabbitMQ connection URL. Use amqps:// for CloudAMQP (SSL).", ) # ── FastAPI Server ──────────────────────────────────────────────────────── app_host: str = Field(default="0.0.0.0", description="Bind host.") app_port: int = Field(default=7860, description="Bind port (7860 for HF Spaces).") # ── Application ─────────────────────────────────────────────────────────── debug: bool = Field(default=False, description="Enable debug mode.") log_level: str = Field(default="INFO", description="Logging level.") # ── AI Model ────────────────────────────────────────────────────────────── gan_checkpoint_path: str = Field( default="./models/gan_checkpoint.pt", description="Path to GAN model checkpoint.", ) vgtlnet_checkpoint_path: str = Field( default="./models/vgtlnet_checkpoint.pt", description="Path to VGTL-Net model checkpoint.", ) use_mock_model: bool = Field( default=True, description=( "Use MockModelService instead of real GAN+VGTL-Net. " "Set to false only when checkpoints are available." ), ) # ── Validators ──────────────────────────────────────────────────────────── @field_validator("log_level") @classmethod def validate_log_level(cls, v: str) -> str: valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} upper = v.upper() if upper not in valid: raise ValueError(f"log_level must be one of {valid}") return upper @field_validator("app_port") @classmethod def validate_port(cls, v: int) -> int: if not (1 <= v <= 65535): raise ValueError("app_port must be between 1 and 65535") return v @field_validator("db_pool_size") @classmethod def validate_pool_size(cls, v: int) -> int: if v < 1: raise ValueError("db_pool_size must be at least 1") return v # ── Computed Helpers ────────────────────────────────────────────────────── @property def is_supabase(self) -> bool: """True when DATABASE_URL points to Supabase (supabase.co host).""" return "supabase.co" in self.database_url @property def is_sqlite(self) -> bool: """True when DATABASE_URL uses SQLite (local dev).""" return self.database_url.startswith("sqlite") @property def uses_pooler(self) -> bool: """True when connecting via Supabase's pgBouncer pooler (port 6543).""" return ":6543/" in self.database_url @lru_cache(maxsize=1) def get_settings() -> Settings: """ Return a cached singleton Settings instance. """ return Settings()