@tricoteuses/senat 3.1.20 → 3.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -32
- package/README.md +326 -326
- package/lib/src/other_types/collaborateurs.d.ts +5 -0
- package/lib/src/other_types/manifest.d.ts +11 -0
- package/lib/src/parsers/collaborateurs.d.ts +44 -0
- package/lib/src/parsers/collaborateurs.js +157 -0
- package/lib/src/scripts/retrieve_agenda.js +38 -4
- package/lib/src/scripts/retrieve_collaborateurs.js +166 -0
- package/lib/src/scripts/shared/incremental_import_sql.js +859 -859
- package/lib/src/scripts/shared/schema_version.js +84 -84
- package/lib/src/scripts/shared/staging_metadata_sql.js +214 -214
- package/lib/src/scripts/validate_prefixed_tables.js +12 -12
- package/lib/src/server/ameli.js +11 -11
- package/lib/src/server/conversion_textes.js +106 -106
- package/lib/src/server/debats.js +10 -10
- package/lib/src/server/documents.js +22 -22
- package/lib/src/server/dosleg.js +33 -33
- package/lib/src/server/questions.js +10 -10
- package/lib/src/server/scrutins.js +3 -3
- package/lib/src/server/sens.js +19 -19
- package/lib/src/utils/manifest.d.ts +6 -0
- package/lib/src/utils/manifest.js +37 -0
- package/lib/src/utils/reunion_deletion.d.ts +2 -0
- package/lib/src/utils/reunion_deletion.js +27 -0
- package/lib/src/videos/match.js +4 -4
- package/lib/tests/collaborateurs.test.js +115 -0
- package/package.json +117 -117
- package/lib/src/config.d.ts +0 -21
- package/lib/src/config.js +0 -27
- package/lib/src/conversion_textes.d.ts +0 -11
- package/lib/src/conversion_textes.js +0 -316
- package/lib/src/databases.d.ts +0 -2
- package/lib/src/databases.js +0 -26
- package/lib/src/datasets.d.ts +0 -34
- package/lib/src/datasets.js +0 -233
- package/lib/src/git.d.ts +0 -26
- package/lib/src/git.js +0 -217
- package/lib/src/loaders.d.ts +0 -52
- package/lib/src/loaders.js +0 -254
- package/lib/src/model/agenda.d.ts +0 -6
- package/lib/src/model/agenda.js +0 -148
- package/lib/src/model/ameli.d.ts +0 -55
- package/lib/src/model/ameli.js +0 -148
- package/lib/src/model/commission.d.ts +0 -18
- package/lib/src/model/commission.js +0 -269
- package/lib/src/model/debats.d.ts +0 -67
- package/lib/src/model/debats.js +0 -95
- package/lib/src/model/documents.d.ts +0 -12
- package/lib/src/model/documents.js +0 -141
- package/lib/src/model/dosleg.d.ts +0 -7
- package/lib/src/model/dosleg.js +0 -326
- package/lib/src/model/index.d.ts +0 -7
- package/lib/src/model/index.js +0 -7
- package/lib/src/model/questions.d.ts +0 -45
- package/lib/src/model/questions.js +0 -89
- package/lib/src/model/scrutins.d.ts +0 -13
- package/lib/src/model/scrutins.js +0 -114
- package/lib/src/model/seance.d.ts +0 -3
- package/lib/src/model/seance.js +0 -267
- package/lib/src/model/sens.d.ts +0 -182
- package/lib/src/model/sens.js +0 -485
- package/lib/src/model/util.d.ts +0 -9
- package/lib/src/model/util.js +0 -38
- package/lib/src/raw_types/ameli.d.ts +0 -914
- package/lib/src/raw_types/ameli.js +0 -5
- package/lib/src/raw_types/debats.d.ts +0 -207
- package/lib/src/raw_types/debats.js +0 -5
- package/lib/src/raw_types/dosleg.d.ts +0 -1619
- package/lib/src/raw_types/dosleg.js +0 -5
- package/lib/src/raw_types/questions.d.ts +0 -423
- package/lib/src/raw_types/questions.js +0 -5
- package/lib/src/raw_types/senat.d.ts +0 -11372
- package/lib/src/raw_types/senat.js +0 -5
- package/lib/src/raw_types/sens.d.ts +0 -8248
- package/lib/src/raw_types/sens.js +0 -5
- package/lib/src/raw_types_schemats/ameli.d.ts +0 -539
- package/lib/src/raw_types_schemats/ameli.js +0 -2
- package/lib/src/raw_types_schemats/debats.d.ts +0 -127
- package/lib/src/raw_types_schemats/debats.js +0 -2
- package/lib/src/raw_types_schemats/dosleg.d.ts +0 -977
- package/lib/src/raw_types_schemats/dosleg.js +0 -2
- package/lib/src/raw_types_schemats/questions.d.ts +0 -237
- package/lib/src/raw_types_schemats/questions.js +0 -2
- package/lib/src/raw_types_schemats/sens.d.ts +0 -6915
- package/lib/src/raw_types_schemats/sens.js +0 -2
- package/lib/src/scripts/test_iter_load.js +0 -12
- package/lib/src/types/agenda.d.ts +0 -45
- package/lib/src/types/ameli.d.ts +0 -5
- package/lib/src/types/compte_rendu.d.ts +0 -83
- package/lib/src/types/debats.d.ts +0 -2
- package/lib/src/types/debats.js +0 -1
- package/lib/src/types/dosleg.d.ts +0 -70
- package/lib/src/types/dosleg.js +0 -1
- package/lib/src/types/questions.d.ts +0 -2
- package/lib/src/types/questions.js +0 -1
- package/lib/src/types/sens.d.ts +0 -10
- package/lib/src/types/sens.js +0 -1
- package/lib/src/types/sessions.d.ts +0 -6
- package/lib/src/types/sessions.js +0 -19
- package/lib/src/types/texte.d.ts +0 -72
- package/lib/src/types/texte.js +0 -15
- package/lib/src/validators/config.d.ts +0 -9
- package/lib/src/validators/config.js +0 -10
- /package/lib/src/{scripts/test_iter_load.d.ts → other_types/collaborateurs.js} +0 -0
- /package/lib/src/{types/agenda.js → other_types/manifest.js} +0 -0
- /package/lib/src/{types/ameli.js → scripts/retrieve_collaborateurs.d.ts} +0 -0
- /package/lib/{src/types/compte_rendu.js → tests/collaborateurs.test.d.ts} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fonctions pures de parsing du PDF DRH des collaborateurs de sénateurs
|
|
3
|
+
* (https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf).
|
|
4
|
+
*
|
|
5
|
+
* Le PDF est un tableau à deux colonnes par page :
|
|
6
|
+
* - "Employeur" (le sénateur) à gauche, ex. "Mme AESCHLIMANN Marie-Do" ;
|
|
7
|
+
* - "Nom Collaborateur" à droite, ex. "M. CHAREF Dahmane".
|
|
8
|
+
* Une ligne sans employeur prolonge le sénateur précédent.
|
|
9
|
+
*
|
|
10
|
+
* Pièges traités ici : aucun matricule dans le PDF, prénoms de sénateurs tronqués
|
|
11
|
+
* ("Marie-Do"), noms composés ("DI FOLCO", "MAZENOT CHAPPUY"), accents.
|
|
12
|
+
*/
|
|
13
|
+
/** Élément de texte positionné extrait du PDF (x croissant vers la droite, y croissant vers le haut). */
|
|
14
|
+
export type PdfTextItem = {
|
|
15
|
+
str: string;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
page: number;
|
|
19
|
+
};
|
|
20
|
+
export type PersonneNom = {
|
|
21
|
+
civilite: string;
|
|
22
|
+
nom: string;
|
|
23
|
+
prenom: string;
|
|
24
|
+
};
|
|
25
|
+
export type LigneCollaborateur = {
|
|
26
|
+
senateur: PersonneNom;
|
|
27
|
+
collaborateur: PersonneNom;
|
|
28
|
+
};
|
|
29
|
+
/** Normalise un texte pour la comparaison : majuscules, sans accents, espaces compactés. */
|
|
30
|
+
export declare function normaliserPourComparaison(valeur: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Sépare une cellule "Civilité NOM(S) Prénom" en {civilite, nom, prenom}.
|
|
33
|
+
* Les jetons en majuscules de tête forment le nom (gère les noms composés), le reste le prénom.
|
|
34
|
+
* Renvoie null si la cellule n'est pas un nom de personne exploitable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseCellulePersonne(cellule: string): PersonneNom | null;
|
|
37
|
+
/** Extrait la date d'édition imprimée en pied de page ("Edition du JJ/MM/AAAA"). */
|
|
38
|
+
export declare function extraireDateEdition(strings: string[]): Date | null;
|
|
39
|
+
/**
|
|
40
|
+
* Reconstruit la liste {sénateur, collaborateur} à partir des éléments de texte positionnés.
|
|
41
|
+
* Parcourt les pages puis les lignes de haut en bas ; mémorise le sénateur courant et lui
|
|
42
|
+
* rattache les collaborateurs des lignes suivantes tant qu'aucun nouvel employeur n'apparaît.
|
|
43
|
+
*/
|
|
44
|
+
export declare function construireLignesCollaborateurs(items: PdfTextItem[]): LigneCollaborateur[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fonctions pures de parsing du PDF DRH des collaborateurs de sénateurs
|
|
3
|
+
* (https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf).
|
|
4
|
+
*
|
|
5
|
+
* Le PDF est un tableau à deux colonnes par page :
|
|
6
|
+
* - "Employeur" (le sénateur) à gauche, ex. "Mme AESCHLIMANN Marie-Do" ;
|
|
7
|
+
* - "Nom Collaborateur" à droite, ex. "M. CHAREF Dahmane".
|
|
8
|
+
* Une ligne sans employeur prolonge le sénateur précédent.
|
|
9
|
+
*
|
|
10
|
+
* Pièges traités ici : aucun matricule dans le PDF, prénoms de sénateurs tronqués
|
|
11
|
+
* ("Marie-Do"), noms composés ("DI FOLCO", "MAZENOT CHAPPUY"), accents.
|
|
12
|
+
*/
|
|
13
|
+
// Bandes horizontales des deux colonnes (mesurées sur le PDF : employeur ≈ 147, collaborateur ≈ 285).
|
|
14
|
+
const COLONNE_EMPLOYEUR = { min: 130, max: 225 };
|
|
15
|
+
const COLONNE_COLLABORATEUR = { min: 255, max: 360 };
|
|
16
|
+
const CIVILITES = ["Mme", "M.", "Mlle", "M"];
|
|
17
|
+
// Particules nobiliaires/patronymiques en minuscules, qui font partie du nom quand elles le précèdent
|
|
18
|
+
// (ex. "de CIDRAC", "de LA GONTRIE", "de LEGGE"). Le référentiel Sénat les conserve dans Nom_usuel.
|
|
19
|
+
const PARTICULES = new Set([
|
|
20
|
+
"de",
|
|
21
|
+
"du",
|
|
22
|
+
"des",
|
|
23
|
+
"d'",
|
|
24
|
+
"le",
|
|
25
|
+
"la",
|
|
26
|
+
"les",
|
|
27
|
+
"von",
|
|
28
|
+
"van",
|
|
29
|
+
"der",
|
|
30
|
+
"den",
|
|
31
|
+
"da",
|
|
32
|
+
"di",
|
|
33
|
+
"del",
|
|
34
|
+
"dos",
|
|
35
|
+
]);
|
|
36
|
+
// Lignes d'en-tête / pied de page à ignorer.
|
|
37
|
+
const LIGNES_IGNOREES = [
|
|
38
|
+
/liste des collaborateurs/i,
|
|
39
|
+
/^employeur$/i,
|
|
40
|
+
/^nom collaborateur$/i,
|
|
41
|
+
/^a\.?g\.?a\.?s\.?/i,
|
|
42
|
+
/edition du/i,
|
|
43
|
+
/congé non rémunéré/i,
|
|
44
|
+
/^-\s*\d+\s*-$/,
|
|
45
|
+
];
|
|
46
|
+
/** Normalise un texte pour la comparaison : majuscules, sans accents, espaces compactés. */
|
|
47
|
+
export function normaliserPourComparaison(valeur) {
|
|
48
|
+
return valeur
|
|
49
|
+
.normalize("NFD")
|
|
50
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
51
|
+
.toUpperCase()
|
|
52
|
+
.replace(/['']/g, "'")
|
|
53
|
+
.replace(/\s+/g, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
function estIgnoree(str) {
|
|
57
|
+
const t = str.trim();
|
|
58
|
+
if (!t)
|
|
59
|
+
return true;
|
|
60
|
+
return LIGNES_IGNOREES.some((re) => re.test(t));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sépare une cellule "Civilité NOM(S) Prénom" en {civilite, nom, prenom}.
|
|
64
|
+
* Les jetons en majuscules de tête forment le nom (gère les noms composés), le reste le prénom.
|
|
65
|
+
* Renvoie null si la cellule n'est pas un nom de personne exploitable.
|
|
66
|
+
*/
|
|
67
|
+
export function parseCellulePersonne(cellule) {
|
|
68
|
+
const brut = cellule
|
|
69
|
+
.replace(/\(\*\)/g, "")
|
|
70
|
+
.replace(/\s+/g, " ")
|
|
71
|
+
.trim();
|
|
72
|
+
if (!brut)
|
|
73
|
+
return null;
|
|
74
|
+
let civilite = "";
|
|
75
|
+
let reste = brut;
|
|
76
|
+
for (const civ of CIVILITES) {
|
|
77
|
+
if (brut === civ)
|
|
78
|
+
return null;
|
|
79
|
+
if (brut.startsWith(`${civ} `)) {
|
|
80
|
+
civilite = civ;
|
|
81
|
+
reste = brut.slice(civ.length + 1).trim();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!reste)
|
|
86
|
+
return null;
|
|
87
|
+
const jetons = reste.split(" ");
|
|
88
|
+
const estMajuscule = (jeton) => {
|
|
89
|
+
const lettres = jeton.replace(/[^A-Za-zÀ-ÿ]/g, "");
|
|
90
|
+
return lettres.length > 0 && lettres === lettres.toUpperCase();
|
|
91
|
+
};
|
|
92
|
+
const jetonsNom = [];
|
|
93
|
+
let i = 0;
|
|
94
|
+
// Particules de tête en minuscules (ex. "de", "de la") faisant partie du nom.
|
|
95
|
+
while (i < jetons.length && PARTICULES.has(jetons[i].toLowerCase())) {
|
|
96
|
+
jetonsNom.push(jetons[i]);
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
// Puis les jetons du nom en majuscules.
|
|
100
|
+
while (i < jetons.length && estMajuscule(jetons[i])) {
|
|
101
|
+
jetonsNom.push(jetons[i]);
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
// Une particule seule sans nom en majuscules n'est pas un nom valide : on rétablit.
|
|
105
|
+
if (jetonsNom.length > 0 && jetonsNom.every((j) => PARTICULES.has(j.toLowerCase()))) {
|
|
106
|
+
jetonsNom.length = 0;
|
|
107
|
+
i = 0;
|
|
108
|
+
}
|
|
109
|
+
// Si aucun jeton majuscule (cas inattendu), on prend le premier comme nom.
|
|
110
|
+
if (jetonsNom.length === 0 && jetons.length > 0) {
|
|
111
|
+
jetonsNom.push(jetons[0]);
|
|
112
|
+
i = 1;
|
|
113
|
+
}
|
|
114
|
+
const nom = jetonsNom.join(" ");
|
|
115
|
+
const prenom = jetons.slice(i).join(" ");
|
|
116
|
+
if (!nom)
|
|
117
|
+
return null;
|
|
118
|
+
return { civilite, nom, prenom };
|
|
119
|
+
}
|
|
120
|
+
/** Extrait la date d'édition imprimée en pied de page ("Edition du JJ/MM/AAAA"). */
|
|
121
|
+
export function extraireDateEdition(strings) {
|
|
122
|
+
for (const s of strings) {
|
|
123
|
+
const m = s.match(/Edition du\s+(\d{2})\/(\d{2})\/(\d{4})/i);
|
|
124
|
+
if (m) {
|
|
125
|
+
const [, jj, mm, aaaa] = m;
|
|
126
|
+
return new Date(Date.UTC(Number(aaaa), Number(mm) - 1, Number(jj)));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Reconstruit la liste {sénateur, collaborateur} à partir des éléments de texte positionnés.
|
|
133
|
+
* Parcourt les pages puis les lignes de haut en bas ; mémorise le sénateur courant et lui
|
|
134
|
+
* rattache les collaborateurs des lignes suivantes tant qu'aucun nouvel employeur n'apparaît.
|
|
135
|
+
*/
|
|
136
|
+
export function construireLignesCollaborateurs(items) {
|
|
137
|
+
const utiles = items.filter((it) => !estIgnoree(it.str));
|
|
138
|
+
// Tri lecture : page croissante, puis y décroissant (haut → bas), puis x croissant (gauche → droite).
|
|
139
|
+
const tries = [...utiles].sort((a, b) => a.page - b.page || b.y - a.y || a.x - b.x);
|
|
140
|
+
const lignes = [];
|
|
141
|
+
let senateurCourant = null;
|
|
142
|
+
for (const it of tries) {
|
|
143
|
+
const dansEmployeur = it.x >= COLONNE_EMPLOYEUR.min && it.x <= COLONNE_EMPLOYEUR.max;
|
|
144
|
+
const dansCollaborateur = it.x >= COLONNE_COLLABORATEUR.min && it.x <= COLONNE_COLLABORATEUR.max;
|
|
145
|
+
if (dansEmployeur) {
|
|
146
|
+
const personne = parseCellulePersonne(it.str);
|
|
147
|
+
if (personne)
|
|
148
|
+
senateurCourant = personne;
|
|
149
|
+
}
|
|
150
|
+
else if (dansCollaborateur && senateurCourant) {
|
|
151
|
+
const collaborateur = parseCellulePersonne(it.str);
|
|
152
|
+
if (collaborateur)
|
|
153
|
+
lignes.push({ senateur: senateurCourant, collaborateur });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return lignes;
|
|
157
|
+
}
|
|
@@ -11,6 +11,8 @@ import { assertExistingDirectory, commonOptions } from "./shared/cli_helpers.js"
|
|
|
11
11
|
import { fetchWithRetry } from "./shared/util.js";
|
|
12
12
|
import { buildReunionsByBucket } from "../utils/reunion_parsing.js";
|
|
13
13
|
import { buildSenatDossierIndex, buildDoslegReunionIndex } from "../utils/reunion_odj_building.js";
|
|
14
|
+
import { loadPreviousManifest, saveManifest, detectChanges } from "../utils/manifest.js";
|
|
15
|
+
import { deleteReunionFiles } from "../utils/reunion_deletion.js";
|
|
14
16
|
const optionsDefinitions = [
|
|
15
17
|
...commonOptions,
|
|
16
18
|
{
|
|
@@ -52,11 +54,9 @@ async function retrieveAgendas(options, sessions) {
|
|
|
52
54
|
for (const session of sessions) {
|
|
53
55
|
const originalAgendaSessionDir = path.join(originalAgendaDir, `${session}`);
|
|
54
56
|
fs.ensureDirSync(originalAgendaSessionDir);
|
|
55
|
-
fs.emptyDirSync(originalAgendaSessionDir);
|
|
56
57
|
const transformedAgendaSessionDir = path.join(transformedAgendaDir, `${session}`);
|
|
57
58
|
if (options["parseAgenda"]) {
|
|
58
59
|
fs.ensureDirSync(transformedAgendaSessionDir);
|
|
59
|
-
fs.emptyDirSync(transformedAgendaSessionDir);
|
|
60
60
|
}
|
|
61
61
|
const fifteenDaysFromNow = new Date();
|
|
62
62
|
// Don't download agendas more than 15 days in the future.
|
|
@@ -68,7 +68,8 @@ async function retrieveAgendas(options, sessions) {
|
|
|
68
68
|
try {
|
|
69
69
|
await downloadAgenda(agendaName, agendaPath);
|
|
70
70
|
if (options["parseAgenda"]) {
|
|
71
|
-
await parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPath, dossierIndex, doslegReunionIndex);
|
|
71
|
+
const createdUids = await parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPath, dossierIndex, doslegReunionIndex);
|
|
72
|
+
await processManifestAndDeletions(transformedAgendaSessionDir, agendaFileName, session, createdUids);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
catch (error) {
|
|
@@ -109,21 +110,54 @@ async function parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPa
|
|
|
109
110
|
console.log(`Parsing Agenda ${agendaPath}…`);
|
|
110
111
|
const parsedAgendaEvents = await parseAgendaFromFile(agendaPath);
|
|
111
112
|
if (!parsedAgendaEvents?.length)
|
|
112
|
-
return;
|
|
113
|
+
return [];
|
|
113
114
|
const flatPath = path.join(transformedAgendaSessionDir, `${agendaFileName}.json`);
|
|
114
115
|
fs.writeJSONSync(flatPath, parsedAgendaEvents, { spaces: 2 });
|
|
115
116
|
const byBucket = buildReunionsByBucket(parsedAgendaEvents, dossierBySenatUrl, doslegReunionIndex);
|
|
117
|
+
// Collecter tous les UIDs créés
|
|
118
|
+
const createdUids = [];
|
|
116
119
|
// SP
|
|
117
120
|
if (byBucket.IDS.length > 0) {
|
|
118
121
|
writeGroupsAsFiles(transformedAgendaSessionDir, byBucket.IDS);
|
|
122
|
+
createdUids.push(...byBucket.IDS.map((r) => r.uid));
|
|
119
123
|
}
|
|
120
124
|
// NON-SP
|
|
121
125
|
for (const suffix of ["IDC", "IDM", "IDO", "IDI"]) {
|
|
122
126
|
const groups = byBucket[suffix];
|
|
123
127
|
if (groups.length > 0) {
|
|
124
128
|
writeGroupsAsFiles(transformedAgendaSessionDir, groups);
|
|
129
|
+
createdUids.push(...groups.map((r) => r.uid));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return createdUids;
|
|
133
|
+
}
|
|
134
|
+
async function processManifestAndDeletions(transformedAgendaSessionDir, agendaFileName, session, currentUids) {
|
|
135
|
+
const date = agendaFileName;
|
|
136
|
+
const previousManifest = loadPreviousManifest(transformedAgendaSessionDir, date);
|
|
137
|
+
if (previousManifest) {
|
|
138
|
+
const changes = detectChanges(previousManifest.reunionUids, currentUids);
|
|
139
|
+
if (changes.new.length > 0) {
|
|
140
|
+
console.log(` [${date}] ${changes.new.length} nouvelle(s) réunion(s)`);
|
|
125
141
|
}
|
|
142
|
+
if (changes.deleted.length > 0) {
|
|
143
|
+
console.log(` [${date}] ${changes.deleted.length} réunion(s) supprimée(s) détectée(s)`);
|
|
144
|
+
const deleted = deleteReunionFiles(transformedAgendaSessionDir, changes.deleted);
|
|
145
|
+
console.log(` [${date}] ${deleted} fichier(s) supprimé(s)`);
|
|
146
|
+
}
|
|
147
|
+
if (changes.kept.length > 0 && !options["silent"]) {
|
|
148
|
+
console.log(` [${date}] ${changes.kept.length} réunion(s) conservée(s)`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (!options["silent"]) {
|
|
152
|
+
console.log(` [${date}] Premier téléchargement (${currentUids.length} réunions)`);
|
|
126
153
|
}
|
|
154
|
+
const newManifest = {
|
|
155
|
+
date,
|
|
156
|
+
timestamp: DateTime.now().toISO() ?? "",
|
|
157
|
+
reunionUids: currentUids,
|
|
158
|
+
session,
|
|
159
|
+
};
|
|
160
|
+
saveManifest(transformedAgendaSessionDir, newManifest);
|
|
127
161
|
}
|
|
128
162
|
async function main() {
|
|
129
163
|
const dataDir = assertExistingDirectory(options["dataDir"], "data directory");
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getDocumentProxy } from "unpdf";
|
|
4
|
+
import { construireLignesCollaborateurs, extraireDateEdition, normaliserPourComparaison, } from "../parsers/collaborateurs.js";
|
|
5
|
+
import { assertExistingDirectory } from "./shared/cli_helpers.js";
|
|
6
|
+
import { iterFilePaths } from "../server/loaders.js";
|
|
7
|
+
const URL_PDF_DEFAUT = "https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf";
|
|
8
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36";
|
|
9
|
+
async function telechargerPdf(url) {
|
|
10
|
+
const cookies = new Map();
|
|
11
|
+
let courant = url;
|
|
12
|
+
for (let i = 0; i < 8; i += 1) {
|
|
13
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
14
|
+
if (cookies.size > 0) {
|
|
15
|
+
headers["Cookie"] = [...cookies].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
16
|
+
}
|
|
17
|
+
const reponse = await fetch(courant, { redirect: "manual", headers });
|
|
18
|
+
for (const brut of reponse.headers.getSetCookie?.() ?? []) {
|
|
19
|
+
const [paire] = brut.split(";");
|
|
20
|
+
const idx = paire.indexOf("=");
|
|
21
|
+
if (idx > 0)
|
|
22
|
+
cookies.set(paire.slice(0, idx).trim(), paire.slice(idx + 1).trim());
|
|
23
|
+
}
|
|
24
|
+
if (reponse.status >= 300 && reponse.status < 400) {
|
|
25
|
+
const location = reponse.headers.get("location");
|
|
26
|
+
if (!location)
|
|
27
|
+
throw new Error(`Redirection sans en-tête Location (HTTP ${reponse.status})`);
|
|
28
|
+
courant = new URL(location, courant).toString();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!reponse.ok)
|
|
32
|
+
throw new Error(`Téléchargement du PDF échoué : HTTP ${reponse.status}`);
|
|
33
|
+
const buffer = Buffer.from(await reponse.arrayBuffer());
|
|
34
|
+
if (buffer.subarray(0, 5).toString("latin1") !== "%PDF-") {
|
|
35
|
+
throw new Error("Le contenu téléchargé n'est pas un PDF");
|
|
36
|
+
}
|
|
37
|
+
return buffer;
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Trop de redirections lors du téléchargement du PDF DRH");
|
|
40
|
+
}
|
|
41
|
+
async function extraireItemsPdf(buffer) {
|
|
42
|
+
const pdf = await getDocumentProxy(new Uint8Array(buffer));
|
|
43
|
+
const items = [];
|
|
44
|
+
const strings = [];
|
|
45
|
+
for (let p = 1; p <= pdf.numPages; p += 1) {
|
|
46
|
+
const page = await pdf.getPage(p);
|
|
47
|
+
const contenu = await page.getTextContent();
|
|
48
|
+
for (const item of contenu.items) {
|
|
49
|
+
if (typeof item.str !== "string" || !item.str.trim() || !item.transform)
|
|
50
|
+
continue;
|
|
51
|
+
items.push({ str: item.str, x: item.transform[4], y: item.transform[5], page: p });
|
|
52
|
+
strings.push(item.str);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { items, strings };
|
|
56
|
+
}
|
|
57
|
+
function chargerSenateurs(dataDir) {
|
|
58
|
+
const senateursDir = path.join(dataDir, "sens", "senateurs");
|
|
59
|
+
const files = new Map();
|
|
60
|
+
const indexParNom = new Map();
|
|
61
|
+
if (!fs.existsSync(senateursDir))
|
|
62
|
+
return { files, indexParNom };
|
|
63
|
+
for (const filePath of iterFilePaths(senateursDir)) {
|
|
64
|
+
const senator = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
65
|
+
files.set(senator.matricule, senator);
|
|
66
|
+
const cle = normaliserPourComparaison(senator.nom_usuel);
|
|
67
|
+
const liste = indexParNom.get(cle);
|
|
68
|
+
if (liste)
|
|
69
|
+
liste.push(senator);
|
|
70
|
+
else
|
|
71
|
+
indexParNom.set(cle, [senator]);
|
|
72
|
+
}
|
|
73
|
+
return { files, indexParNom };
|
|
74
|
+
}
|
|
75
|
+
async function main() {
|
|
76
|
+
const dataDir = process.argv[2];
|
|
77
|
+
assertExistingDirectory(dataDir, "dataDir");
|
|
78
|
+
// 1. Téléchargement + parsing du PDF
|
|
79
|
+
console.log(`Téléchargement du PDF DRH : ${URL_PDF_DEFAUT}`);
|
|
80
|
+
const buffer = await telechargerPdf(URL_PDF_DEFAUT);
|
|
81
|
+
const { items, strings } = await extraireItemsPdf(buffer);
|
|
82
|
+
const lignes = construireLignesCollaborateurs(items);
|
|
83
|
+
const dateEdition = extraireDateEdition(strings);
|
|
84
|
+
console.log(`PDF lu : ${lignes.length} lignes collaborateur, édition ${dateEdition?.toISOString().slice(0, 10) ?? "inconnue"}`);
|
|
85
|
+
if (lignes.length === 0) {
|
|
86
|
+
throw new Error("Aucune ligne collaborateur extraite du PDF : format inattendu, abandon sans modification.");
|
|
87
|
+
}
|
|
88
|
+
// 2. Charger les fichiers sénateurs existants
|
|
89
|
+
const { indexParNom } = chargerSenateurs(dataDir);
|
|
90
|
+
if (indexParNom.size === 0) {
|
|
91
|
+
throw new Error("Aucun fichier sénateur trouvé. Lancez d'abord data:download pour générer les fichiers.");
|
|
92
|
+
}
|
|
93
|
+
// 3. Résoudre les sénateurs du PDF → fichiers
|
|
94
|
+
let nbEnrichis = 0;
|
|
95
|
+
let nbNonResolus = 0;
|
|
96
|
+
// Grouper les collaborateurs par matricule de sénateur
|
|
97
|
+
const collabsParMatricule = new Map();
|
|
98
|
+
const nomsNonResolus = new Set();
|
|
99
|
+
for (const ligne of lignes) {
|
|
100
|
+
const cleNom = normaliserPourComparaison(ligne.senateur.nom);
|
|
101
|
+
const candidats = indexParNom.get(cleNom);
|
|
102
|
+
if (!candidats || candidats.length === 0) {
|
|
103
|
+
nomsNonResolus.add(`${ligne.senateur.civilite} ${ligne.senateur.nom} ${ligne.senateur.prenom}`.trim());
|
|
104
|
+
nbNonResolus += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Désambiguïsation par préfixe de prénom
|
|
108
|
+
const prenomCible = normaliserPourComparaison(ligne.senateur.prenom);
|
|
109
|
+
const parPrenom = candidats.filter((c) => normaliserPourComparaison(c.prenom_usuel).startsWith(prenomCible));
|
|
110
|
+
let senator = null;
|
|
111
|
+
if (candidats.length === 1) {
|
|
112
|
+
senator = candidats[0];
|
|
113
|
+
}
|
|
114
|
+
else if (parPrenom.length === 1) {
|
|
115
|
+
senator = parPrenom[0];
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
nomsNonResolus.add(`${ligne.senateur.civilite} ${ligne.senateur.nom} ${ligne.senateur.prenom}`.trim());
|
|
119
|
+
nbNonResolus += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let collabs = collabsParMatricule.get(senator.matricule);
|
|
123
|
+
if (!collabs) {
|
|
124
|
+
collabs = [];
|
|
125
|
+
collabsParMatricule.set(senator.matricule, collabs);
|
|
126
|
+
}
|
|
127
|
+
// Dédoublonnage
|
|
128
|
+
const nNom = normaliserPourComparaison(ligne.collaborateur.nom);
|
|
129
|
+
const nPrenom = normaliserPourComparaison(ligne.collaborateur.prenom);
|
|
130
|
+
if (!collabs.some((c) => normaliserPourComparaison(c.nom) === nNom && normaliserPourComparaison(c.prenom) === nPrenom)) {
|
|
131
|
+
collabs.push({
|
|
132
|
+
civilite: ligne.collaborateur.civilite,
|
|
133
|
+
nom: ligne.collaborateur.nom,
|
|
134
|
+
prenom: ligne.collaborateur.prenom,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 4. Écrire les collaborateurs dans les fichiers sénateurs
|
|
139
|
+
for (const [matricule, collaborateurs] of collabsParMatricule) {
|
|
140
|
+
const cheminFichier = path.join(dataDir, "sens", "senateurs", `${matricule}.json`);
|
|
141
|
+
if (!fs.existsSync(cheminFichier))
|
|
142
|
+
continue;
|
|
143
|
+
const senator = JSON.parse(fs.readFileSync(cheminFichier, "utf-8"));
|
|
144
|
+
senator.collaborateurs = collaborateurs;
|
|
145
|
+
fs.writeFileSync(cheminFichier, JSON.stringify(senator, null, 2) + "\n");
|
|
146
|
+
nbEnrichis += 1;
|
|
147
|
+
}
|
|
148
|
+
// 5. Nettoyer les collaborateurs des sénateurs absents du PDF
|
|
149
|
+
const matriculesEnrichis = new Set(collabsParMatricule.keys());
|
|
150
|
+
const { files: tousLesFichiers } = chargerSenateurs(dataDir);
|
|
151
|
+
for (const [matricule, senator] of tousLesFichiers) {
|
|
152
|
+
if (!matriculesEnrichis.has(matricule) && senator.collaborateurs) {
|
|
153
|
+
delete senator.collaborateurs;
|
|
154
|
+
const cheminFichier = path.join(dataDir, "sens", "senateurs", `${matricule}.json`);
|
|
155
|
+
fs.writeFileSync(cheminFichier, JSON.stringify(senator, null, 2) + "\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.log(`${nbEnrichis} sénateur(s) enrichi(s) avec leurs collaborateurs`);
|
|
159
|
+
if (nomsNonResolus.size > 0) {
|
|
160
|
+
console.warn(`${nbNonResolus} sénateur(s) du PDF non rattaché(s) : ${[...nomsNonResolus].slice(0, 10).join(", ")}${nomsNonResolus.size > 10 ? "…" : ""}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
main().catch((error) => {
|
|
164
|
+
console.error(error);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|