LIBRE / src /shared /config.py
RyZ
fix: resolve env path and respect dependency overrides in lifespan
77f2d58
"""
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()