chore: sync project files

This commit is contained in:
Gilles Soulier
2026-01-13 19:49:04 +01:00
parent 53f8227941
commit ecda149a4b
149 changed files with 65272 additions and 1 deletions

191
pricewatch/app/core/registry.py Executable file
View File

@@ -0,0 +1,191 @@
"""
Registry pour la détection automatique des stores.
Le Registry maintient une liste de tous les stores disponibles et
peut détecter automatiquement quel store correspond à une URL donnée.
"""
from typing import Optional
from pricewatch.app.core.logging import get_logger
from pricewatch.app.stores.base import BaseStore
logger = get_logger("core.registry")
class StoreRegistry:
"""
Registry central pour tous les stores.
Permet d'enregistrer des stores et de détecter automatiquement
le bon store depuis une URL via la méthode match().
Justification technique:
- Pattern Registry pour découpler la détection des stores du code métier
- Extensible: ajouter un nouveau store = juste register() un nouvel objet
- Pas de dépendances hardcodées entre modules
"""
def __init__(self):
"""Initialise un registry vide."""
self._stores: list[BaseStore] = []
logger.debug("Registry initialisé")
def register(self, store: BaseStore) -> None:
"""
Enregistre un nouveau store dans le registry.
Args:
store: Instance de BaseStore à enregistrer
"""
if not isinstance(store, BaseStore):
raise TypeError(f"Expected BaseStore, got {type(store)}")
# Éviter les doublons
if any(s.store_id == store.store_id for s in self._stores):
logger.warning(f"Store '{store.store_id}' déjà enregistré, remplacement")
self._stores = [s for s in self._stores if s.store_id != store.store_id]
self._stores.append(store)
logger.info(f"Store enregistré: {store.store_id}")
def unregister(self, store_id: str) -> bool:
"""
Retire un store du registry.
Args:
store_id: ID du store à retirer
Returns:
True si le store a été retiré, False s'il n'était pas présent
"""
initial_count = len(self._stores)
self._stores = [s for s in self._stores if s.store_id != store_id]
removed = len(self._stores) < initial_count
if removed:
logger.info(f"Store désenregistré: {store_id}")
else:
logger.warning(f"Store non trouvé pour désenregistrement: {store_id}")
return removed
def get_store(self, store_id: str) -> Optional[BaseStore]:
"""
Récupère un store par son ID.
Args:
store_id: ID du store à récupérer
Returns:
Instance du store ou None si non trouvé
"""
for store in self._stores:
if store.store_id == store_id:
return store
return None
def detect_store(self, url: str) -> Optional[BaseStore]:
"""
Détecte automatiquement le store correspondant à une URL.
Args:
url: URL à analyser
Returns:
Store avec le meilleur score, ou None si aucun match
Justification technique:
- Teste tous les stores enregistrés avec leur méthode match()
- Retourne celui avec le score le plus élevé (> 0)
- Permet de gérer les ambiguïtés (ex: sous-domaines multiples)
"""
if not url or not url.strip():
logger.warning("URL vide fournie pour détection")
return None
if not self._stores:
logger.warning("Aucun store enregistré dans le registry")
return None
best_store: Optional[BaseStore] = None
best_score = 0.0
logger.debug(f"Détection du store pour: {url}")
for store in self._stores:
try:
score = store.match(url)
logger.debug(f" {store.store_id}: score={score:.2f}")
if score > best_score:
best_score = score
best_store = store
except Exception as e:
logger.error(f"Erreur lors du match de {store.store_id}: {e}")
continue
if best_store:
logger.info(
f"Store détecté: {best_store.store_id} (score={best_score:.2f})"
)
else:
logger.warning(f"Aucun store trouvé pour: {url}")
return best_store
def list_stores(self) -> list[str]:
"""
Liste tous les stores enregistrés.
Returns:
Liste des IDs de stores
"""
return [store.store_id for store in self._stores]
def __len__(self) -> int:
"""Retourne le nombre de stores enregistrés."""
return len(self._stores)
def __repr__(self) -> str:
stores_list = ", ".join(self.list_stores())
return f"<StoreRegistry stores=[{stores_list}]>"
# Instance globale du registry
# Les stores s'y enregistreront lors de leur import
_global_registry = StoreRegistry()
def get_registry() -> StoreRegistry:
"""
Retourne l'instance globale du registry.
Returns:
Registry singleton
"""
return _global_registry
def register_store(store: BaseStore) -> None:
"""
Enregistre un store dans le registry global.
Args:
store: Instance de BaseStore
"""
_global_registry.register(store)
def detect_store(url: str) -> Optional[BaseStore]:
"""
Détecte le store depuis le registry global.
Args:
url: URL à analyser
Returns:
Store détecté ou None
"""
return _global_registry.detect_store(url)