codex
This commit is contained in:
238
pricewatch/app/db/connection.py
Executable file
238
pricewatch/app/db/connection.py
Executable file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user