193 lines
5.8 KiB
Python
193 lines
5.8 KiB
Python
"""
|
|
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")
|