Files
veye-lalwa/pipeline/impacts.py
T

130 lines
4.5 KiB
Python
Raw Normal View History

"""Ventilation des impacts sur les libertés par public.
Le rapport ne dresse pas de tableau « texte × public ». En revanche il consacre
un chapitre à chacun des trois publics :
=========== ===============================================
Chapitre Public
=========== ===============================================
`sec04` Libertés individuelles : analyse croisée
`sec05` Libertés associatives : le chantier latent
`sec06` Entreprises : obligations nouvelles et ouvertures
=========== ===============================================
Un texte développé dans le chapitre 5 est donc, par construction du rapport, un
texte qui touche les libertés associatives. C'est ce placement — un choix
éditorial explicite, pas une inférence — qui sert de base à la ventilation.
Le **sens** de l'impact vient d'abord de la cotation « statut libertés » de
l'annexe, seule donnée cotée à la main. À défaut — les textes non promulgués ne
sont pas cotés — il est déduit du vocabulaire de la phrase, et le texte est
alors marqué « à vérifier ».
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pipeline.modeles import Impact, Public, RegimeLibertes, SensImpact
from pipeline.parseurs.chapitres import LectureChapitres
from pipeline.parseurs.dates_fr import sans_accents
CHAPITRE_PAR_PUBLIC: dict[str, Public] = {
"sec04": Public.INDIVIDUS,
"sec05": Public.ASSOCIATIONS,
"sec06": Public.ENTREPRISES,
}
SENS_PAR_REGIME: dict[RegimeLibertes, SensImpact] = {
RegimeLibertes.DROITS_PLUS: SensImpact.POSITIF,
RegimeLibertes.CONTROLE_PLUS: SensImpact.NEGATIF,
RegimeLibertes.MIXTE: SensImpact.MIXTE,
RegimeLibertes.NEUTRE: SensImpact.NEUTRE,
}
# Vocabulaire de restriction et d'extension de droits, tel qu'employé par le
# rapport. Les listes sont volontairement courtes : un mot ambigu ferait plus
# de dégâts qu'un impact manquant.
_RESTRICTION = (
"restriction", "restreint", "sanction", "amende", "interdiction", "interdit",
"peine", "penal", "controle prefectoral", "surveillance", "fichage",
"dissolution", "retrait", "suspension", "obligation declarative", "censure",
"expropriation", "retention", "fouille", "reduit", "durci", "aggrave",
)
_EXTENSION = (
"droit opposable", "nouveau droit", "garantie", "protection", "reparation",
"extension des droits", "acces", "recours", "assistance", "indemnisation",
"simplification", "allegement", "ouverture", "facilite", "reserve aux pme",
)
@dataclass(slots=True)
class Ventilation:
"""Impacts déduits pour un texte, et ce qui les a motivés."""
impacts: dict[Public, Impact]
deduit: bool # vrai si le sens n'a pas pu venir de la cotation manuelle
MOTIF_VERIFICATION = (
"Sens de l'impact déduit du vocabulaire du rapport : l'annexe ne cote le "
"régime de libertés que pour les lois promulguées."
)
def ventiler(
texte_id: str,
lecture: LectureChapitres,
*,
regime: RegimeLibertes | None,
) -> Ventilation:
"""Répartit l'effet d'un texte entre individus, associations et entreprises."""
impacts: dict[Public, Impact] = {}
deduit = False
for mention in lecture.mentions.get(texte_id, []):
public = CHAPITRE_PAR_PUBLIC.get(mention.chapitre)
if public is None or public in impacts:
continue
phrases = " ".join(mention.phrases)
if regime is not None:
sens = SENS_PAR_REGIME[regime]
else:
sens = _sens_du_vocabulaire(phrases)
deduit = True
impacts[public] = Impact(sens=sens, note=_note(mention.phrases))
return Ventilation(impacts=impacts, deduit=deduit and bool(impacts))
def _sens_du_vocabulaire(fragment: str) -> SensImpact:
plat = sans_accents(fragment).lower()
restrictions = sum(1 for mot in _RESTRICTION if _present(mot, plat))
extensions = sum(1 for mot in _EXTENSION if _present(mot, plat))
if restrictions and extensions:
return SensImpact.MIXTE
if restrictions:
return SensImpact.NEGATIF
if extensions:
return SensImpact.POSITIF
return SensImpact.NEUTRE
def _present(expression: str, plat: str) -> bool:
return re.search(rf"(?<!\w){re.escape(expression)}\w{{0,3}}(?!\w)", plat) is not None
def _note(phrases: list[str]) -> str:
"""Première phrase substantielle du chapitre, comme justification affichable."""
from pipeline.parseurs.markdown import nettoyer
for phrase in phrases:
propre = nettoyer(phrase)
if len(propre) > 60:
return propre[:400]
return ""