""" Configuration centralisée pour PriceWatch Phase 2. Gère la configuration de la base de données, Redis, et l'application globale. Utilise Pydantic Settings pour validation et chargement depuis variables d'environnement. Justification technique: - Pattern 12-factor app: configuration via env vars - Pydantic validation garantit config valide au démarrage - Valeurs par défaut pour développement local - Support .env file pour faciliter le setup """ from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict from pricewatch.app.core.logging import get_logger logger = get_logger("core.config") class DatabaseConfig(BaseSettings): """Configuration PostgreSQL.""" host: str = Field(default="localhost", description="PostgreSQL host") port: int = Field(default=5432, description="PostgreSQL port") database: str = Field(default="pricewatch", description="Database name") user: str = Field(default="pricewatch", description="Database user") password: str = Field(default="pricewatch", description="Database password") model_config = SettingsConfigDict( env_prefix="PW_DB_", # PW_DB_HOST, PW_DB_PORT, etc. env_file=".env", env_file_encoding="utf-8", extra="ignore", ) @property def url(self) -> str: """ SQLAlchemy connection URL. Format: postgresql://user:password@host:port/database """ return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" @property def url_async(self) -> str: """ Async SQLAlchemy connection URL (pour usage futur avec asyncpg). Format: postgresql+asyncpg://user:password@host:port/database """ return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" class RedisConfig(BaseSettings): """Configuration Redis pour RQ worker.""" host: str = Field(default="localhost", description="Redis host") port: int = Field(default=6379, description="Redis port") db: int = Field(default=0, description="Redis database number (0-15)") password: Optional[str] = Field(default=None, description="Redis password (optional)") model_config = SettingsConfigDict( env_prefix="PW_REDIS_", # PW_REDIS_HOST, PW_REDIS_PORT, etc. env_file=".env", env_file_encoding="utf-8", extra="ignore", ) @property def url(self) -> str: """ Redis connection URL pour RQ. Format: redis://[password@]host:port/db """ auth = f":{self.password}@" if self.password else "" return f"redis://{auth}{self.host}:{self.port}/{self.db}" class AppConfig(BaseSettings): """Configuration globale de l'application.""" # Mode debug debug: bool = Field( default=False, description="Enable debug mode (verbose logging, SQL echo)" ) # Worker configuration worker_timeout: int = Field( default=300, description="Worker job timeout in seconds (5 minutes)" ) worker_concurrency: int = Field( default=2, description="Number of concurrent worker processes" ) # Feature flags enable_db: bool = Field( default=True, description="Enable database persistence (can disable for testing)" ) enable_worker: bool = Field( default=True, description="Enable background worker functionality" ) # API auth api_token: Optional[str] = Field( default=None, description="API token simple (Bearer)" ) # Scraping defaults default_playwright_timeout: int = Field( default=60000, description="Default Playwright timeout in milliseconds" ) default_use_playwright: bool = Field( default=True, description="Use Playwright fallback by default" ) model_config = SettingsConfigDict( env_prefix="PW_", # PW_DEBUG, PW_WORKER_TIMEOUT, etc. env_file=".env", env_file_encoding="utf-8", extra="ignore", ) # Nested configs (instances, not classes) db: DatabaseConfig = Field(default_factory=DatabaseConfig) redis: RedisConfig = Field(default_factory=RedisConfig) def log_config(self) -> None: """Log la configuration active (sans password).""" logger.info("=== Configuration PriceWatch ===") logger.info(f"Debug mode: {self.debug}") logger.info(f"Database: {self.db.host}:{self.db.port}/{self.db.database}") logger.info(f"Redis: {self.redis.host}:{self.redis.port}/{self.redis.db}") logger.info(f"DB enabled: {self.enable_db}") logger.info(f"Worker enabled: {self.enable_worker}") logger.info(f"Worker timeout: {self.worker_timeout}s") logger.info(f"Worker concurrency: {self.worker_concurrency}") logger.info(f"API token configured: {bool(self.api_token)}") logger.info("================================") # Singleton global config instance _config: Optional[AppConfig] = None def get_config() -> AppConfig: """ Récupère l'instance globale de configuration (singleton). Returns: Instance AppConfig Justification: - Évite de recharger la config à chaque appel - Centralise la configuration pour toute l'application - Permet d'override pour les tests """ global _config if _config is None: _config = AppConfig() if _config.debug: _config.log_config() return _config def set_config(config: AppConfig) -> None: """ Override la configuration globale (principalement pour tests). Args: config: Instance AppConfig à utiliser """ global _config _config = config logger.debug("Configuration overridden") def reset_config() -> None: """Reset la configuration globale (pour tests).""" global _config _config = None logger.debug("Configuration reset")