This commit is contained in:
2026-01-14 07:03:38 +01:00
parent ecda149a4b
commit c91c0f1fc9
61 changed files with 4388 additions and 38 deletions

Binary file not shown.

186
pricewatch/app/core/config.py Executable file
View File

@@ -0,0 +1,186 @@
"""
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")