chore: sync project files
This commit is contained in:
191
pricewatch/app/core/registry.py
Executable file
191
pricewatch/app/core/registry.py
Executable 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)
|
||||
Reference in New Issue
Block a user