""" Configuración de base de datos con soporte multi-tenant - Base de datos compartida (core_db) para tenants pequeños/medianos - Bases de datos dedicadas para clientes enterprise """ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, Session from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from typing import Generator, Dict, Optional, AsyncGenerator from contextlib import contextmanager from .config import settings # Base declarativa para modelos ORM Base = declarative_base() # Engine y SessionLocal para base de datos core (sincrónico) core_engine = create_engine( settings.core_database_url, pool_pre_ping=True, pool_size=10, max_overflow=20, echo=settings.DEBUG ) CoreSessionLocal = sessionmaker( autocommit=False, autoflush=False, bind=core_engine ) # Engine asíncrono para operaciones async async_core_engine = create_async_engine( settings.async_core_database_url, pool_pre_ping=True, pool_size=10, max_overflow=20, echo=settings.DEBUG ) AsyncCoreSessionLocal = async_sessionmaker( async_core_engine, class_=AsyncSession, expire_on_commit=False ) # Cache de engines para tenants con BD dedicada _tenant_engines: Dict[str, any] = {} def get_core_db() -> Generator[Session, None, None]: """ Dependency para obtener sesión de base de datos core (compartida) Uso en FastAPI: db: Session = Depends(get_core_db) """ db = CoreSessionLocal() try: yield db finally: db.close() async def get_async_core_db() -> AsyncGenerator[AsyncSession, None]: """ Dependency para obtener sesión asíncrona de base de datos core """ async with AsyncCoreSessionLocal() as session: try: yield session finally: await session.close() def get_tenant_engine(tenant_id: int, db_config: dict): """ Obtiene o crea un engine para un tenant con BD dedicada Args: tenant_id: ID del tenant db_config: Configuración de BD {host, port, name, user, password} Returns: Engine de SQLAlchemy para el tenant """ if tenant_id not in _tenant_engines: db_url = f"postgresql://{db_config['user']}:{db_config['password']}@{db_config['host']}:{db_config['port']}/{db_config['name']}" _tenant_engines[tenant_id] = create_engine( db_url, pool_pre_ping=True, pool_size=5, max_overflow=10 ) return _tenant_engines[tenant_id] @contextmanager def get_tenant_db(tenant_id: int, db_config: Optional[dict] = None) -> Generator[Session, None, None]: """ Context manager para obtener sesión de BD de un tenant específico Si db_config es None, usa la BD core (compartida) Si db_config está presente, usa la BD dedicada del tenant Uso: with get_tenant_db(tenant_id, config) as db: # operaciones con db """ if db_config is None: # Tenant en BD compartida db = CoreSessionLocal() else: # Tenant con BD dedicada engine = get_tenant_engine(tenant_id, db_config) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) db = SessionLocal() try: yield db finally: db.close() def init_db(): """ Inicializa las tablas de la base de datos core """ Base.metadata.create_all(bind=core_engine) async def init_async_db(): """ Inicializa las tablas de la base de datos core (async) """ async with async_core_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)