239 lines
6.3 KiB
Python
239 lines
6.3 KiB
Python
"""
|
|
Gestion des connexions PostgreSQL pour PriceWatch Phase 2.
|
|
|
|
Fournit:
|
|
- Engine SQLAlchemy avec connection pooling
|
|
- Session factory avec context manager
|
|
- Initialisation des tables
|
|
- Health check
|
|
|
|
Justification technique:
|
|
- Connection pooling: réutilisation connexions pour performance
|
|
- Context manager: garantit fermeture session (pas de leak)
|
|
- pool_pre_ping: vérifie connexion avant usage (robustesse)
|
|
- echo=debug: logs SQL en mode debug
|
|
"""
|
|
|
|
from contextlib import contextmanager
|
|
from typing import Generator, Optional
|
|
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.engine import Engine
|
|
from sqlalchemy.engine.url import make_url
|
|
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from pricewatch.app.core.config import AppConfig, get_config
|
|
from pricewatch.app.core.logging import get_logger
|
|
from pricewatch.app.db.models import Base
|
|
|
|
logger = get_logger("db.connection")
|
|
|
|
# Global engine instance (singleton)
|
|
_engine: Optional[Engine] = None
|
|
_session_factory: Optional[sessionmaker] = None
|
|
|
|
|
|
def get_engine(config: Optional[AppConfig] = None) -> Engine:
|
|
"""
|
|
Récupère ou crée l'Engine SQLAlchemy (singleton).
|
|
|
|
Args:
|
|
config: Configuration app (utilise get_config() si None)
|
|
|
|
Returns:
|
|
Engine SQLAlchemy configuré
|
|
|
|
Justification:
|
|
- Singleton: une seule pool de connexions par application
|
|
- pool_pre_ping: vérifie connexion avant usage (évite "connection closed")
|
|
- pool_size=5, max_overflow=10: limite connexions (15 max)
|
|
- echo=debug: logs SQL pour debugging
|
|
"""
|
|
global _engine
|
|
|
|
if _engine is None:
|
|
if config is None:
|
|
config = get_config()
|
|
|
|
db_url = config.db.url
|
|
url = make_url(db_url)
|
|
is_sqlite = url.get_backend_name() == "sqlite"
|
|
|
|
logger.info(f"Creating database engine: {db_url}")
|
|
|
|
engine_kwargs = {
|
|
"pool_pre_ping": True,
|
|
"pool_recycle": 3600,
|
|
"echo": config.debug,
|
|
}
|
|
|
|
if not is_sqlite:
|
|
engine_kwargs.update(
|
|
{
|
|
"pool_size": 5,
|
|
"max_overflow": 10,
|
|
}
|
|
)
|
|
|
|
_engine = create_engine(db_url, **engine_kwargs)
|
|
|
|
logger.info("Database engine created successfully")
|
|
|
|
return _engine
|
|
|
|
|
|
def init_db(config: Optional[AppConfig] = None) -> None:
|
|
"""
|
|
Initialise la base de données (crée toutes les tables).
|
|
|
|
Args:
|
|
config: Configuration app (utilise get_config() si None)
|
|
|
|
Raises:
|
|
OperationalError: Si connexion impossible
|
|
SQLAlchemyError: Si création tables échoue
|
|
|
|
Note:
|
|
Utilise Base.metadata.create_all() - idempotent (ne crash pas si tables existent)
|
|
"""
|
|
if config is None:
|
|
config = get_config()
|
|
|
|
logger.info("Initializing database...")
|
|
|
|
try:
|
|
engine = get_engine(config)
|
|
|
|
# Créer toutes les tables définies dans Base.metadata
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
logger.info("Database initialized successfully")
|
|
logger.info(f"Tables created: {', '.join(Base.metadata.tables.keys())}")
|
|
|
|
except OperationalError as e:
|
|
logger.error(f"Failed to connect to database: {e}")
|
|
raise
|
|
except SQLAlchemyError as e:
|
|
logger.error(f"Failed to create tables: {e}")
|
|
raise
|
|
|
|
|
|
def get_session_factory(config: Optional[AppConfig] = None) -> sessionmaker:
|
|
"""
|
|
Récupère ou crée la session factory (singleton).
|
|
|
|
Args:
|
|
config: Configuration app (utilise get_config() si None)
|
|
|
|
Returns:
|
|
Session factory SQLAlchemy
|
|
|
|
Justification:
|
|
- expire_on_commit=False: objets restent accessibles après commit
|
|
- autocommit=False, autoflush=False: contrôle explicite
|
|
"""
|
|
global _session_factory
|
|
|
|
if _session_factory is None:
|
|
engine = get_engine(config)
|
|
|
|
_session_factory = sessionmaker(
|
|
bind=engine,
|
|
expire_on_commit=False, # Objets restent accessibles après commit
|
|
autocommit=False, # Contrôle explicite du commit
|
|
autoflush=False, # Contrôle explicite du flush
|
|
)
|
|
|
|
logger.debug("Session factory created")
|
|
|
|
return _session_factory
|
|
|
|
|
|
@contextmanager
|
|
def get_session(config: Optional[AppConfig] = None) -> Generator[Session, None, None]:
|
|
"""
|
|
Context manager pour session SQLAlchemy.
|
|
|
|
Args:
|
|
config: Configuration app (utilise get_config() si None)
|
|
|
|
Yields:
|
|
Session SQLAlchemy
|
|
|
|
Usage:
|
|
with get_session() as session:
|
|
product = session.query(Product).filter_by(reference="B08N5WRWNW").first()
|
|
session.commit()
|
|
|
|
Justification:
|
|
- Context manager: garantit fermeture session (pas de leak)
|
|
- Rollback automatique sur exception
|
|
- Close automatique en fin de bloc
|
|
"""
|
|
factory = get_session_factory(config)
|
|
session = factory()
|
|
|
|
try:
|
|
logger.debug("Session opened")
|
|
yield session
|
|
except Exception as e:
|
|
logger.error(f"Session error, rolling back: {e}")
|
|
session.rollback()
|
|
raise
|
|
finally:
|
|
logger.debug("Session closed")
|
|
session.close()
|
|
|
|
|
|
def check_db_connection(config: Optional[AppConfig] = None) -> bool:
|
|
"""
|
|
Vérifie la connexion à la base de données (health check).
|
|
|
|
Args:
|
|
config: Configuration app (utilise get_config() si None)
|
|
|
|
Returns:
|
|
True si connexion OK, False sinon
|
|
|
|
Note:
|
|
Execute une query simple: SELECT 1
|
|
"""
|
|
if config is None:
|
|
config = get_config()
|
|
|
|
try:
|
|
engine = get_engine(config)
|
|
|
|
with engine.connect() as conn:
|
|
result = conn.execute(text("SELECT 1"))
|
|
result.scalar()
|
|
|
|
logger.info("Database connection OK")
|
|
return True
|
|
|
|
except OperationalError as e:
|
|
logger.error(f"Database connection failed: {e}")
|
|
return False
|
|
except SQLAlchemyError as e:
|
|
logger.error(f"Database health check failed: {e}")
|
|
return False
|
|
|
|
|
|
def reset_engine() -> None:
|
|
"""
|
|
Reset l'engine global (pour tests).
|
|
|
|
Note:
|
|
Dispose l'engine et reset les singletons.
|
|
"""
|
|
global _engine, _session_factory
|
|
|
|
if _engine is not None:
|
|
logger.debug("Disposing database engine")
|
|
_engine.dispose()
|
|
_engine = None
|
|
|
|
_session_factory = None
|
|
logger.debug("Engine reset complete")
|