"""SQLAlchemy engine + session factory. The DB password lives in ``/run/secrets/pg_password``; we read it from there (or ``$PG_PASSWORD`` for local dev) and splice it into ``DATABASE_URL`` so the password never has to be in plaintext in ``compose.yml`` or process environment listings. """ from __future__ import annotations import os from pathlib import Path from typing import Generator from urllib.parse import quote_plus, urlparse, urlunparse from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from app.config import get_settings def _resolve_password() -> str | None: inline = os.environ.get("PG_PASSWORD") if inline: return inline.strip() path = os.environ.get("PG_PASSWORD_FILE") if path and Path(path).exists(): return Path(path).read_text(encoding="utf-8").strip() return None def _build_url(base_url: str) -> str: """Inject the resolved password into ``base_url`` if absent.""" parsed = urlparse(base_url) if parsed.password: return base_url pw = _resolve_password() if pw is None: return base_url netloc = f"{parsed.username or ''}:{quote_plus(pw)}@{parsed.hostname}" if parsed.port: netloc += f":{parsed.port}" return urlunparse(parsed._replace(netloc=netloc)) _settings = get_settings() engine = create_engine(_build_url(_settings.database_url), pool_pre_ping=True, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False) class Base(DeclarativeBase): """Declarative base for ORM models.""" def get_session() -> Generator[Session, None, None]: """FastAPI dependency. Commits on success, rolls back on exception.""" session = SessionLocal() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close()