TODO: petite présentation du projet.
Nous utilisons l'API des tricoteuses pour récupérer les données. Voici le lien vers la documentation.
L'Assemblée nationale travaille à partir de dossiers législatifs. Un dossier législatif permet de suivre l'évolution d'un texte législatif : en commission, au Sénat, amendements... Deux types de textes législatifs nous intéressent : proposition de loi proposée par un député, projet de loi proposé par le gouvernement.
Voici les différents endpoint:
- dossiers =>
https://parlement.tricoteuses.fr/dossiers - texte de loi =>
https://parlement.tricoteuses.fr/documents - amendements =>
https://parlement.tricoteuses.fr/amendements - députés =>
https://parlement.tricoteuses.fr/acteurs - votes =>
https://parlement.tricoteuses.fr/scrutins
Information sur le chemin d'une loi Les documents parlementaires
Documentation technique de l'API des tricoteuses
- Installer les dépendances :
uv sync- Créer le fichier
.envà partir de l'exemple, puis l'adapter si besoin :
cp .env.example .env- Démarrer PostgreSQL (instance locale définie dans
docker-compose.yml, sur le port5432) :
docker compose up -d dbLes valeurs par défaut de .env.example correspondent à ce conteneur (postgres/postgres, base ipolitics).
Il y a deux façons d'exécuter des commandes:
- uv run main.py [-h] [-d] [-r] [-e] [-a]
- just
Just permet de facilement exécuter des commandes. Si vous ne voulez pas l'installer, vous pouvez toujours utiliser uv run main.py
La commande suivante va télécharger sur l'API des tricoteuses et créer des fichiers JSON dans le répertoire ./data/. Pour rajouter un endpoint, il suffit de modifier la variable APIS dans ./etl/download.py
uv run main.py -d
# ou
just downloaduv run main.py -r
# ou
just db-rebuilduv run main.py -e
# ou
just etluv run main.py -a
# ou
just allAprès avoir exécuté just download, les données de l'API des tricoteuses sont sauvegardées dans le dossier ./data/ sous la forme de fichiers JSON.
L'ETL permet d'extraire des données de ces fichiers pour les sauvegarder dans la base de données. La base de données est représentée à l'aide de
modèles créés via SqlAlchemy.
Pour charger un fichier en db, il faut rajouter un modèle qui porte le même nom que le fichier sans l'extension. Pour rajouter un champ du json dans la table, il faut rajouter une colonne dans le modèle du même type que le json et du même nom.
L'ETL est capable d'inférer à partir des modèles les fichiers à ouvrir et les champs à charger.
Voici une partie du fichier ./data/dossiers.json
{
"uid": "DLR5L17N54464",
"dataset": 17,
"chambre": "AN",
"numero": "4464",
...
}Je veux rajouter le champ chambre dans la DB et faire en sorte que l'ETL l'ajoute de lui-même.
- Rajouter le champ dans le modèle
class Dossier(Base):
__tablename__ = "dossiers"
uid: Mapped[str] = mapped_column(primary_key=True)
titre: Mapped[str] = mapped_column(String(1000))
dataset: Mapped[int]
chambre: Mapped[str] = mapped_column(String(5)) # <-------- nouvelle colonne qui porte le même nom que le champ du fichier jsonLe champ doit porter le même nom sinon l'ETL ne sera pas capable de le trouver.
- Exécuter
just all
| Table | Endpoint | Contenu | Volume (législature 17) |
|---|---|---|---|
acteurs |
/acteurs |
Députés / sénateurs (référentiel trans-législature) | ~3 100 |
organes |
/organes |
Groupes politiques, commissions, assemblées… | ~5 400 |
mandats |
/mandats |
Jointure acteur ↔ organe (appartenance + dates) | ~25 600 |
scrutins |
/scrutins |
Scrutins publics et résultat agrégé (pour/contre/abstentions) | ~8 200 |
groupesVotants |
/groupesVotants |
Résultat d'un scrutin ventilé par groupe politique | ~121 500 * |
documents |
/documents |
Textes (projets/propositions de loi, rapports…) | ~4 500 |
auteursDocument |
/auteursDocument |
Auteur(s) d'un document | ~20 000 |
coSignatairesDocument |
/coSignatairesDocument |
Co-signataires d'un document | ~116 000 |
* groupesVotants n'expose pas de filtre legislature : la table couvre toutes les législatures
(~98 300 lignes se rattachent à un scrutin de la L17, soit 12 groupes pour chacun des 8 192
scrutins concernés). Se scoper par jointure sur scrutins.
Les jointures se font par les colonnes …RefUid (références molles, nullable, indexées). Schéma
entité-relation ci-dessous — les boîtes ne montrent que les colonnes clés (PK + FK + quelques
champs parlants) ; la liste complète est dans les modèles models/.
erDiagram
dossiers {
string uid PK
string titre
string libelleProcedure
string statut
}
documents {
string uid PK
string dossierRefUid FK
string auteurPrincipalUid FK
text titrePrincipal
string classeLibelle
bool texteLoi
string dateDepot
}
amendements {
string uid PK
string acteurRefUid FK
string groupePolitiqueRefUid FK
string dossierRefUid FK
string documentRefUid FK
string scrutinRefUid FK
string numeroLong
string divisionArticleDesignation
text exposeSommaire
string sortAmendement
string dateDepot
}
acteurs {
string uid PK
string groupeParlementaireUid FK
string nom
string prenom
string chambre
bool actif
}
organes {
string uid PK
string codeType
string libelleAbrev
string positionPolitique
}
mandats {
string uid PK
string acteurRefUid FK
string organeRefUid FK
string typeOrgane
string libQualite
string dateDebut
string dateFin
}
scrutins {
string uid PK
string dossierRefUid FK
string documentRefUid FK
string amendementRefUid FK
string dateScrutin
text objet
string code
int pour
int contre
int abstentions
}
groupesVotants {
string uid PK
string scrutinRefUid FK
string organeRefUid FK
string positionMajoritaire
int pour
int contre
int abstentions
}
auteursDocument {
string uid PK
string documentRefUid FK
string acteurRefUid FK
string qualite
}
coSignatairesDocument {
string uid PK
string documentRefUid FK
string acteurRefUid FK
string dateCosignature
}
dossiers ||--o{ documents : "dossierRefUid"
dossiers ||--o{ amendements : "dossierRefUid"
dossiers ||--o{ scrutins : "dossierRefUid"
documents ||--o{ amendements : "documentRefUid"
documents ||--o{ scrutins : "documentRefUid"
documents ||--o{ auteursDocument : "documentRefUid"
documents ||--o{ coSignatairesDocument : "documentRefUid"
acteurs ||--o{ documents : "auteurPrincipalUid"
acteurs ||--o{ amendements : "acteurRefUid"
acteurs ||--o{ mandats : "acteurRefUid"
acteurs ||--o{ auteursDocument : "acteurRefUid"
acteurs ||--o{ coSignatairesDocument : "acteurRefUid"
organes ||--o{ acteurs : "groupeParlementaireUid"
organes ||--o{ amendements : "groupePolitiqueRefUid"
organes ||--o{ mandats : "organeRefUid"
organes ||--o{ groupesVotants : "organeRefUid"
amendements ||--o{ scrutins : "amendementRefUid"
scrutins ||--o{ amendements : "scrutinRefUid"
scrutins ||--o{ groupesVotants : "scrutinRefUid"
Le lien amendement ↔ scrutin est natif, et dans les deux sens : scrutins.amendementRefUid
pointe vers l'amendement tranché par le scrutin, et amendements.scrutinRefUid vers le scrutin
qui a tranché l'amendement. Le second est le plus large (~11 600 amendements contre ~6 800),
un même scrutin pouvant trancher plusieurs amendements identiques. La jointure reste clairsemée :
la plupart des amendements sont tranchés à main levée, sans scrutin public.
Objectif : repérer les amendements dont l'exposé sommaire déclare une collaboration ou une inspiration avec une entité externe (lobby, syndicat, association, entreprise, fédération professionnelle, ONG…) — formulations du type « travaillé avec… », « en concertation avec… », « inspiré de… ».
Le détecteur (analysis/detect_mentions_regex.py) fonctionne par expressions régulières :
déterministe, instantané et sans coût, il matche des familles de formulations calibrées sur
le corpus réel, avec des exclusions contextuelles (acteurs publics ou parlementaires, référents
textuels type « proposé par le texte ») pour limiter les faux positifs.
La base doit être alimentée au préalable (table amendements, voir les sections ETL ci-dessus).
# Tout le corpus, résultats en JSONL uniquement
just detect-mentions-regex
# + écriture des mentions dans la table amendement_mentions
just detect-mentions-regex --persist
# Sur un sous-ensemble
just detect-mentions-regex --limit 100
# Équivalent sans just :
uv run python -m analysis.detect_mentions_regex --persist- JSONL brut :
analysis/output/mentions_regex.jsonl(une ligne par amendement, dossier gitignoré), plus un récap console des formulations rencontrées et de leur fréquence. - Base (avec
--persist) : tableamendement_mentions, une ligne par mention détectée (amendementUid,citation,formulation,modele='regex:v1',createdAt). L'écriture est idempotente par amendement et scopée au tagmodele='regex:v1': les lignes produites par d'autres détecteurs (ex. un LLM) ne sont jamais touchées. - Le repérage regex ne remplit ni
entite, nitypeEntite, niexterne: identifier et qualifier l'entité demande une analyse sémantique (prévue dans une itération ultérieure).
amendement_mentions est une table d'analyse : elle n'est pas listée dans ETL_TABLES
(etl/database.py) et survit donc à un db-rebuild, contrairement à amendements/dossiers
qui sont détruites puis rechargées. Sa colonne amendementUid est une référence molle vers
amendements.uid (pas de ForeignKey), afin qu'aucune contrainte ne bloque le drop de la table
ETL. Pour ajouter une nouvelle analyse, créer un modèle sur ce principe (hors ETL_TABLES).