Files
scrap/pricewatch/app/core/config.py
2026-01-14 07:03:38 +01:00

187 lines
5.6 KiB
Python
Executable File

"""
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"
)
# 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("================================")
# 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")