"""Async engine + session factory, created lazily from settings. The engine is process-global and cached. The API binds a session per request (see ``app.api.deps``); workers and scripts use :func:`session_scope`. """ from collections.abc import AsyncIterator from contextlib import asynccontextmanager from functools import lru_cache from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from app.core.config import get_settings @lru_cache def get_engine() -> AsyncEngine: settings = get_settings() return create_async_engine( settings.database_url, echo=settings.db_echo, pool_size=settings.db_pool_size, max_overflow=settings.db_max_overflow, pool_pre_ping=True, # survive Postgres restarts / idle drops ) @lru_cache def get_sessionmaker() -> async_sessionmaker[AsyncSession]: return async_sessionmaker( bind=get_engine(), expire_on_commit=False, autoflush=False, ) @asynccontextmanager async def session_scope() -> AsyncIterator[AsyncSession]: """Transactional session for workers/scripts: commit on success, rollback on error.""" session = get_sessionmaker()() try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() async def dispose_engine() -> None: """Dispose the pooled engine on shutdown. Safe to call if never initialized.""" if get_engine.cache_info().currsize: await get_engine().dispose() get_engine.cache_clear() get_sessionmaker.cache_clear()