"""Database engine and session plumbing. Synchronous SQLAlchemy on purpose: the background scheduler, the Telegram polling bot and the request handlers all touch the same tables, and one boring threadsafe sessionmaker is far easier to reason about than three event loops. FastAPI runs plain `def` endpoints in a threadpool, so nothing blocks. """ from __future__ import annotations from collections.abc import Iterator from contextlib import contextmanager from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from app.config import env_settings class Base(DeclarativeBase): pass def _engine_kwargs(url: str) -> dict: if url.startswith("sqlite"): # One file, many threads (scheduler + bot + requests). return {"connect_args": {"check_same_thread": False}} return {"pool_pre_ping": True, "pool_size": 5, "max_overflow": 10} engine = create_engine(env_settings().database_url, **_engine_kwargs(env_settings().database_url)) SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) def init_db() -> None: """Create any missing tables. The schema is additive, so this is enough.""" from app import models # noqa: F401 (register the mappers) Base.metadata.create_all(bind=engine) @contextmanager def session_scope() -> Iterator[Session]: """A transactional session for background jobs and scripts.""" session = SessionLocal() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close() def get_session() -> Iterator[Session]: """FastAPI dependency: one session per request, committed by the endpoint.""" session = SessionLocal() try: yield session finally: session.close()