130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""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 ""
|