# ============================================ # Projet : API-KA # Fichier : src/database/db.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Engine SQLAlchemy, sessions, initialisation du schéma et healthcheck DB.""" from __future__ import annotations import argparse from collections.abc import Iterator from contextlib import contextmanager from sqlalchemy import Engine, create_engine, text from sqlalchemy.orm import Session, sessionmaker from src.config import get_settings, verify_node from src.database.models import Base _engine: Engine | None = None _session_factory: sessionmaker[Session] | None = None def get_engine() -> Engine: """Retourne l'engine SQLAlchemy (créé paresseusement, pool_pre_ping actif).""" global _engine if _engine is None: settings = get_settings() kwargs: dict = {"pool_pre_ping": True} if settings.database_url.startswith("sqlite"): kwargs["connect_args"] = {"check_same_thread": False} _engine = create_engine(settings.database_url, **kwargs) return _engine def get_session_factory() -> sessionmaker[Session]: """Retourne la fabrique de sessions liée à l'engine courant.""" global _session_factory if _session_factory is None: _session_factory = sessionmaker(bind=get_engine(), expire_on_commit=False) return _session_factory def reset_engine() -> None: """Réinitialise l'engine (utilisé par les tests après changement de config).""" global _engine, _session_factory if _engine is not None: _engine.dispose() _engine = None _session_factory = None @contextmanager def session_scope() -> Iterator[Session]: """Session transactionnelle : commit en sortie, rollback sur exception.""" session = get_session_factory()() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close() def get_db() -> Iterator[Session]: """Dépendance FastAPI fournissant une session par requête.""" session = get_session_factory()() try: yield session finally: session.close() def init_db() -> None: """Crée toutes les tables si elles n'existent pas (idempotent).""" Base.metadata.create_all(get_engine()) def healthcheck() -> bool: """Vérifie que la base répond (SELECT 1).""" try: with get_engine().connect() as conn: conn.execute(text("SELECT 1")) return True except Exception: return False def main() -> None: """Point d'entrée CLI : ``python -m src.database.db --init``.""" from src.utils.logger import get_logger parser = argparse.ArgumentParser(description="Gestion de la base API-KA (m3u96b)") parser.add_argument("--init", action="store_true", help="Créer les tables") args = parser.parse_args() verify_node() logger = get_logger("apika.db") if args.init: init_db() logger.info("Base initialisée", extra={"database_ok": healthcheck()}) else: logger.info("Healthcheck base", extra={"database_ok": healthcheck()}) if __name__ == "__main__": main()