Files
veye-lalwa/pipeline/collecteurs/base.py
T

109 lines
3.5 KiB
Python
Raw Normal View History

"""Contrat commun à tous les collecteurs."""
from __future__ import annotations
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date
from pipeline.journal import logger
from pipeline.modeles import DecisionCC, Evenement, Source, Statut, TypeTexte
log = logger("collecteur")
@dataclass(slots=True)
class TexteCollecte:
"""Un texte observé sur une source officielle.
Volontairement plus pauvre qu'un `Texte` : un collecteur rapporte ce qu'il
voit, il ne classe pas. La classification et la cotation Guadeloupe sont
faites ensuite, et signalées comme automatiques.
"""
titre: str
source_url: str
collecteur: str
numero_officiel: str | None = None
type: TypeTexte | None = None
statut: Statut | None = None
date_promulgation: date | None = None
date_adoption: date | None = None
date_publication_jo: date | None = None
identifiant_externe: str | None = None # cid Légifrance, dossier AN…
resume: str | None = None
evenements: list[Evenement] = field(default_factory=list)
decisions_cc: list[DecisionCC] = field(default_factory=list)
sources: list[Source] = field(default_factory=list)
@dataclass(slots=True)
class ResultatCollecte:
"""Ce qu'un collecteur rapporte d'un passage."""
collecteur: str
textes: list[TexteCollecte] = field(default_factory=list)
decisions_cc: list[DecisionCC] = field(default_factory=list)
erreurs: list[str] = field(default_factory=list)
duree_s: float = 0.0
ignore: bool = False
motif_ignore: str | None = None
@property
def a_reussi(self) -> bool:
return not self.erreurs and not self.ignore
class Collecteur(ABC):
"""Un collecteur de source officielle.
Les implémentations ne lèvent jamais : `collecter()` capture ses propres
erreurs et les range dans le résultat. C'est ce qui permet au run complet de
se poursuivre quand une source est indisponible.
"""
nom: str = "inconnu"
def est_disponible(self) -> tuple[bool, str | None]:
"""Indique si le collecteur peut travailler, et sinon pourquoi.
Sert à documenter les dégradations : un collecteur privé de clé d'API
doit le dire clairement plutôt que d'échouer silencieusement.
"""
return True, None
@abstractmethod
def _collecter(self) -> ResultatCollecte:
"""Collecte effective, susceptible de lever."""
def collecter(self) -> ResultatCollecte:
depart = time.monotonic()
disponible, motif = self.est_disponible()
if not disponible:
log.info("collecteur ignoré", collecteur=self.nom, motif=motif)
return ResultatCollecte(
collecteur=self.nom, ignore=True, motif_ignore=motif,
duree_s=time.monotonic() - depart,
)
try:
resultat = self._collecter()
except Exception as erreur: # noqa: BLE001 — un collecteur ne fait pas tomber le run
log.warning("collecteur en échec", collecteur=self.nom, erreur=str(erreur))
resultat = ResultatCollecte(collecteur=self.nom, erreurs=[str(erreur)])
resultat.duree_s = time.monotonic() - depart
log.info(
"collecte terminée",
collecteur=self.nom,
textes=len(resultat.textes),
decisions=len(resultat.decisions_cc),
erreurs=len(resultat.erreurs),
duree_s=round(resultat.duree_s, 2),
)
return resultat