192 lines
5.4 KiB
Python
Executable File
192 lines
5.4 KiB
Python
Executable File
"""
|
|
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)
|