@tricoteuses/senat 2.20.16 → 2.20.18

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/README.md CHANGED
@@ -29,40 +29,79 @@ docker run --name local-postgres -d -p 5432:5432 -e POSTGRES_PASSWORD=$YOUR_CUST
29
29
 
30
30
  ## Download data
31
31
 
32
+ ### Basic usage
33
+
32
34
  Create a folder where the data will be downloaded and run the following command to download the data and convert it into JSON files.
33
35
 
34
36
  ```bash
35
37
  mkdir ../senat-data/
36
38
 
37
- # Available options for optional `categories` parameter : All, Ameli, Debats, DosLeg, Questions, Sens
38
- npm run data:download ../senat-data -- [--categories All]
39
+ npm run data:download ../senat-data
39
40
  ```
40
41
 
41
- Data from other sources is also available :
42
+ ### Available Commands
43
+
44
+ - `npm run data:download <dir>`: Download, convert data to JSON
45
+ - `npm run data:retrieve_documents <dir>`: Retrieval of textes and rapports from Sénat's website
46
+ - `npm run data:parse_textes_lois <dir>`: Parse textes (requires xml files)
47
+ - `npm run data:retrieve_agenda <dir>`: Retrieval of agenda from Sénat's website
48
+ - `npm run data:retrieve_cr_seance <dir>`: Retrieval of comptes-rendus de séance from Sénat's data
49
+ - `npm run data:retrieve_cr_commission <dir>`: Retrieval of comptes-rendus de commissions from Sénat's website
50
+ - `npm run data:retrieve_senateurs_photos <dir>`: Retrieval of sénateurs' pictures from Sénat's website
51
+
52
+ ### Filtering Options
53
+
54
+ Downloading all the data is long and takes up a lot of disk space. It is possible to choose the type of data that you want to retrieve to reduce the load.
55
+
56
+ Examples:
42
57
 
43
58
  ```bash
44
- # Retrieval of textes and rapports from Sénat's website
45
- # Available options for optional `formats` parameter : xml, html, pdf
46
- # Available options for optional `types` parameter : textes, rapports
47
- npm run data:retrieve_documents ../senat-data -- --fromSession 2022 [--formats xml pdf] [--types textes]
59
+ # Only download amendments
60
+ npm run data:download ../senat-data -- -k Ameli
48
61
 
49
- # Retrieval & parsing (textes in xml format only for now)
50
- npm run data:retrieve_documents ../senat-data -- --fromSession 2022 --parseDocuments
62
+ # Only process data from session 2023 onwards
63
+ npm run data:download ../senat-data -- --fromSession 2023
64
+ ```
51
65
 
52
- # Parsing only
53
- npm run data:parse_textes_lois ../senat-data
66
+ ### Common Options
54
67
 
55
- # Retrieval (& parsing) of agenda from Sénat's website
56
- npm run data:retrieve_agenda ../senat-data -- --fromSession 2022 [--parseAgenda]
68
+ - `--categories` or `-k <name>`: Filter by dataset categories (Available options: `All`, `Ameli`, `Debats`, `DosLeg`, `Questions`, `Sens`)
69
+ - `--fromSession <year>`: Specify the session year to retrieve data from (default: 2022)
70
+ - `--dataDir <path>` (Mandatory): Path to the working directory where all data is stored (required)
71
+ - `--silent` or `-s`: Disable logging
72
+ - `--verbose` or `-v`: Enable verbose logging
73
+ - `--commit` or `-c`: Automatically commit converted data
74
+ - `--pull` or `-p`: Pull repositories before starting
75
+ - `--clone` or `-C <url>`: Clone Git repositories from a remote group or organization
76
+ - `--remote` or `-r <name>`: Push commits to specified Git remote(s)
77
+ - `--keepDir`: Keep directories when cleaning data
78
+ - `--only-recent <days>`: Retrieve only documents created within the last N days
79
+
80
+ ### Options for Retrieving Documents
81
+
82
+ - `--formats <format>`: Specify document formats to retrieve (options: `xml`, `html`, `pdf`)
83
+ - `--types <type>`: Specify document types to retrieve (options: `textes`, `rapports`)
84
+ - `--parseDocuments`: Parse documents after retrieval
85
+ - `--parseAgenda`: Parse agenda after retrieval
86
+ - `--parseDebats`: Parse comptes-rendus after retrieval
87
+
88
+ #### Examples
89
+
90
+ ```bash
91
+ # Retrieval of textes and rapports in specific formats
92
+ npm run data:retrieve_documents ../senat-data -- --fromSession 2022 --formats xml pdf --types textes
93
+
94
+ # Retrieval & parsing (textes in xml format only for now)
95
+ npm run data:retrieve_documents ../senat-data -- --fromSession 2022 --parseDocuments
57
96
 
58
- # Retrieval (& parsing) of comptes-rendus de séance from Sénat's data
59
- npm run data:retrieve_cr_seance ../senat-data -- [--parseDebats] [--keepDir]
97
+ # Retrieval & parsing of agenda
98
+ npm run data:retrieve_agenda ../senat-data -- --fromSession 2022 --parseAgenda
60
99
 
61
- # Retrieval (& parsing) of comptes-rendus de commissions from Sénat's website
62
- npm run data:retrieve_cr_commission ../senat-data -- [--parseDebats] [--keepDir]
100
+ # Retrieval & parsing of comptes-rendus de séance
101
+ npm run data:retrieve_cr_seance ../senat-data -- --parseDebats --keepDir
63
102
 
64
- # Retrieval of sénateurs' pictures from Sénat's website
65
- npm run data:retrieve_senateurs_photos ../senat-data
103
+ # Retrieval & parsing of comptes-rendus de commissions
104
+ npm run data:retrieve_cr_commission ../senat-data -- --parseDebats --keepDir
66
105
  ```
67
106
 
68
107
  ## Data download using Docker
package/lib/git.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export declare function initRepo(repositoryDir: string): void;
2
+ export declare function commit(repositoryDir: string, message: string): boolean;
3
+ export declare function commitAndPush(repositoryDir: string, message: string, remotes?: string[]): number;
4
+ export declare function resetAndPull(gitDir: string): boolean;
5
+ export declare function clone(gitGroupUrl: string | undefined, gitName: string, workingDir: string): void;
6
+ export declare function run(repositoryDir: string, args: string, verbose?: boolean): string;
7
+ export declare function test(repositoryDir: string, args: string, verbose?: boolean): boolean;
8
+ /**
9
+ * Information about a changed file in git
10
+ */
11
+ export interface GitChangedFile {
12
+ path: string;
13
+ status: "A" | "M" | "D" | "R" | "C" | "T" | "U";
14
+ }
15
+ /**
16
+ * Get the list of files that have changed since a specific commit in a git repository.
17
+ * @param repositoryDir The directory of the git repository
18
+ * @param sinceCommit The commit hash to compare against (e.g., "HEAD~1", "abc123", etc.)
19
+ * @param options Options for filtering
20
+ * @param options.diffFilter Git diff-filter string (default: "AMR").
21
+ * A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied, T=Type changed, U=Unmerged
22
+ * @returns A Map of file paths to their git status
23
+ */
24
+ export declare function getChangedFilesSinceCommit(repositoryDir: string, sinceCommit: string, options?: {
25
+ diffFilter?: string;
26
+ }): Map<string, GitChangedFile["status"]>;
package/lib/git.js ADDED
@@ -0,0 +1,167 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "fs-extra";
3
+ import path from "node:path";
4
+ const MAXBUFFER = 50 * 1024 * 1024;
5
+ export function initRepo(repositoryDir) {
6
+ if (!fs.existsSync(path.join(repositoryDir, ".git"))) {
7
+ fs.ensureDirSync(repositoryDir);
8
+ execSync("git init", {
9
+ cwd: repositoryDir,
10
+ env: process.env,
11
+ encoding: "utf-8",
12
+ stdio: ["ignore", "ignore", "pipe"],
13
+ });
14
+ }
15
+ }
16
+ export function commit(repositoryDir, message) {
17
+ initRepo(repositoryDir);
18
+ execSync("git add .", {
19
+ cwd: repositoryDir,
20
+ env: process.env,
21
+ encoding: "utf-8",
22
+ stdio: ["ignore", "ignore", "pipe"],
23
+ maxBuffer: MAXBUFFER,
24
+ });
25
+ try {
26
+ execSync(`git commit -m "${message}" --quiet`, {
27
+ cwd: repositoryDir,
28
+ env: process.env,
29
+ encoding: "utf-8",
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ });
32
+ return true;
33
+ }
34
+ catch (childProcess) {
35
+ if (childProcess.stdout === null ||
36
+ !/nothing to commit|rien à valider/.test(childProcess.stdout)) {
37
+ console.error(childProcess.output);
38
+ throw childProcess;
39
+ }
40
+ return false;
41
+ }
42
+ }
43
+ export function commitAndPush(repositoryDir, message, remotes) {
44
+ let exitCode = 0;
45
+ if (commit(repositoryDir, message)) {
46
+ for (const remote of remotes || []) {
47
+ try {
48
+ execSync(`git push ${remote} master`, {
49
+ cwd: repositoryDir,
50
+ env: process.env,
51
+ encoding: "utf-8",
52
+ stdio: ["ignore", "ignore", "pipe"],
53
+ });
54
+ }
55
+ catch (childProcess) {
56
+ // Don't stop when push fails.
57
+ console.error(childProcess.output);
58
+ exitCode = childProcess.status;
59
+ }
60
+ }
61
+ }
62
+ else {
63
+ // There was nothing to commit.
64
+ exitCode = 10;
65
+ }
66
+ return exitCode;
67
+ }
68
+ export function resetAndPull(gitDir) {
69
+ execSync("git reset --hard origin/master", {
70
+ cwd: gitDir,
71
+ env: process.env,
72
+ encoding: "utf-8",
73
+ stdio: ["ignore", "ignore", "pipe"],
74
+ });
75
+ execSync("git pull --rebase", {
76
+ cwd: gitDir,
77
+ env: process.env,
78
+ encoding: "utf-8",
79
+ stdio: ["ignore", "ignore", "pipe"],
80
+ });
81
+ return true;
82
+ }
83
+ export function clone(gitGroupUrl, gitName, workingDir) {
84
+ if (gitGroupUrl !== undefined) {
85
+ execSync(`git clone ${gitGroupUrl}/${gitName}.git`, {
86
+ cwd: workingDir,
87
+ env: process.env,
88
+ encoding: "utf-8",
89
+ stdio: ["ignore", "ignore", "pipe"],
90
+ });
91
+ }
92
+ }
93
+ export function run(repositoryDir, args, verbose) {
94
+ try {
95
+ if (verbose)
96
+ console.log(`git -C ${repositoryDir} ${args}`);
97
+ const output = execSync(`git ${args}`, {
98
+ cwd: repositoryDir,
99
+ maxBuffer: MAXBUFFER,
100
+ })
101
+ .toString()
102
+ .trim();
103
+ if (verbose)
104
+ console.log(output);
105
+ return output;
106
+ }
107
+ catch (childProcess) {
108
+ for (const output of ["stdout", "stderr"])
109
+ console.error(`${output}: ${childProcess[output]}`);
110
+ throw childProcess;
111
+ }
112
+ }
113
+ export function test(repositoryDir, args, verbose) {
114
+ try {
115
+ if (verbose)
116
+ console.log(`git -C ${repositoryDir} ${args}`);
117
+ const output = execSync(`git ${args}`, {
118
+ cwd: repositoryDir,
119
+ stdio: ["ignore", "pipe", "pipe"],
120
+ maxBuffer: MAXBUFFER,
121
+ })
122
+ .toString()
123
+ .trim();
124
+ if (verbose)
125
+ console.log(output);
126
+ return true;
127
+ }
128
+ catch (childProcess) {
129
+ if (childProcess.status != 0)
130
+ return false;
131
+ throw childProcess;
132
+ }
133
+ }
134
+ /**
135
+ * Get the list of files that have changed since a specific commit in a git repository.
136
+ * @param repositoryDir The directory of the git repository
137
+ * @param sinceCommit The commit hash to compare against (e.g., "HEAD~1", "abc123", etc.)
138
+ * @param options Options for filtering
139
+ * @param options.diffFilter Git diff-filter string (default: "AMR").
140
+ * A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied, T=Type changed, U=Unmerged
141
+ * @returns A Map of file paths to their git status
142
+ */
143
+ export function getChangedFilesSinceCommit(repositoryDir, sinceCommit, options = {}) {
144
+ const { diffFilter } = options;
145
+ try {
146
+ // Using diff-filter: A = Added, M = Modified, R = Renamed, D = Deleted, etc.
147
+ // Default to AMR (excludes deleted files to prevent loading errors)
148
+ const filter = diffFilter ?? "AMR";
149
+ const output = run(repositoryDir, `diff --name-status --diff-filter=${filter} ${sinceCommit}`, false);
150
+ const changedFiles = new Map();
151
+ for (const line of output.split("\n")) {
152
+ if (line.trim().length === 0)
153
+ continue;
154
+ const parts = line.split("\t");
155
+ if (parts.length >= 2) {
156
+ const status = parts[0].charAt(0);
157
+ const path = parts[1];
158
+ changedFiles.set(path, status);
159
+ }
160
+ }
161
+ return changedFiles;
162
+ }
163
+ catch (error) {
164
+ console.error(`Error getting changed files since commit ${sinceCommit}:`, error);
165
+ return new Map();
166
+ }
167
+ }
package/lib/loaders.d.ts CHANGED
@@ -49,6 +49,7 @@ export interface DossierLegislatifDocumentResult {
49
49
  type_lecture: string;
50
50
  libelle_lecture: string;
51
51
  libelle_organisme: string | null;
52
+ code_organisme: string | null;
52
53
  numero: number | null;
53
54
  id: string | null;
54
55
  url: string;
@@ -3,6 +3,7 @@ import commandLineArgs from "command-line-args";
3
3
  import fs from "fs-extra";
4
4
  import path from "path";
5
5
  import pLimit from "p-limit";
6
+ import * as git from "../git";
6
7
  import { datasets, EnabledDatasets, getEnabledDatasets } from "../datasets";
7
8
  import { DATA_ORIGINAL_FOLDER, DOCUMENT_METADATA_FILE, DOSLEG_DOSSIERS_FOLDER, SCRUTINS_FOLDER, RAPPORT_FOLDER, SENS_CIRCONSCRIPTIONS_FOLDER, SENS_ORGANISMES_FOLDER, SENS_SENATEURS_FOLDER, TEXTE_FOLDER, } from "../loaders";
8
9
  import { findAllAmendements, findAllCirconscriptions, findAllDebats, findAllDossiers, findAllScrutins, findAllOrganismes, findAllQuestions, findAllSens, findSenatRapportUrls, findSenatTexteUrls, } from "../model";
@@ -17,14 +18,26 @@ const SENAT_TEXTE_XML_BASE_URL = "https://www.senat.fr/akomantoso/";
17
18
  const SENAT_TEXTE_BASE_URL = "https://www.senat.fr/leg/";
18
19
  const SENAT_EXPOSE_DES_MOTIFS_BASE_URL = "https://www.senat.fr/leg/exposes-des-motifs/";
19
20
  const SENAT_RAPPORT_BASE_URL = "https://www.senat.fr/rap/";
21
+ function commitGit(datasetDir, options, exitCode) {
22
+ if (options.commit) {
23
+ const errorCode = git.commitAndPush(datasetDir, "Nouvelle moisson", options.remote);
24
+ if ((exitCode === 10 && errorCode !== 10) || (exitCode === 0 && errorCode !== 0 && errorCode !== 10)) {
25
+ exitCode = errorCode;
26
+ }
27
+ }
28
+ return exitCode;
29
+ }
20
30
  async function convertData() {
21
31
  const dataDir = options["dataDir"];
22
32
  assert(dataDir, "Missing argument: data directory");
23
33
  const enabledDatasets = getEnabledDatasets(options["categories"]);
24
34
  console.time("data transformation time");
35
+ let exitCode = 0;
25
36
  if (enabledDatasets & EnabledDatasets.Ameli) {
26
37
  try {
27
38
  await convertDatasetAmeli(dataDir, options);
39
+ const ameliDir = path.join(dataDir, datasets.ameli.database);
40
+ exitCode = commitGit(ameliDir, options, exitCode);
28
41
  }
29
42
  catch (error) {
30
43
  console.error(`Error converting Ameli dataset:`, error);
@@ -33,6 +46,8 @@ async function convertData() {
33
46
  if (enabledDatasets & EnabledDatasets.Debats) {
34
47
  try {
35
48
  await convertDatasetDebats(dataDir, options);
49
+ const debatsDir = path.join(dataDir, datasets.debats.database);
50
+ exitCode = commitGit(debatsDir, options, exitCode);
36
51
  }
37
52
  catch (error) {
38
53
  console.error(`Error converting Debats dataset:`, error);
@@ -41,12 +56,16 @@ async function convertData() {
41
56
  if (enabledDatasets & EnabledDatasets.DosLeg) {
42
57
  try {
43
58
  await convertDatasetDosLeg(dataDir, options);
59
+ const doslegDir = path.join(dataDir, datasets.dosleg.database);
60
+ exitCode = commitGit(doslegDir, options, exitCode);
44
61
  }
45
62
  catch (error) {
46
63
  console.error(`Error converting DosLeg dataset:`, error);
47
64
  }
48
65
  try {
49
66
  await convertDatasetScrutins(dataDir, options);
67
+ const scrutinsDir = path.join(dataDir, SCRUTINS_FOLDER);
68
+ exitCode = commitGit(scrutinsDir, options, exitCode);
50
69
  }
51
70
  catch (error) {
52
71
  console.error(`Error converting Scrutins dataset:`, error);
@@ -55,6 +74,8 @@ async function convertData() {
55
74
  if (enabledDatasets & EnabledDatasets.Questions) {
56
75
  try {
57
76
  await convertDatasetQuestions(dataDir);
77
+ const questionsDir = path.join(dataDir, datasets.questions.database);
78
+ exitCode = commitGit(questionsDir, options, exitCode);
58
79
  }
59
80
  catch (error) {
60
81
  console.error(`Error converting Questions dataset:`, error);
@@ -63,6 +84,8 @@ async function convertData() {
63
84
  if (enabledDatasets & EnabledDatasets.Sens) {
64
85
  try {
65
86
  await convertDatasetSens(dataDir);
87
+ const sensDir = path.join(dataDir, datasets.sens.database);
88
+ exitCode = commitGit(sensDir, options, exitCode);
66
89
  }
67
90
  catch (error) {
68
91
  console.error(`Error converting Sens dataset:`, error);
@@ -71,6 +94,7 @@ async function convertData() {
71
94
  if (!options["silent"]) {
72
95
  console.timeEnd("data transformation time");
73
96
  }
97
+ return exitCode;
74
98
  }
75
99
  async function convertDatasetAmeli(dataDir, options) {
76
100
  const dataset = datasets.ameli;
@@ -284,7 +308,7 @@ async function convertDatasetSens(dataDir) {
284
308
  }
285
309
  }
286
310
  convertData()
287
- .then(() => process.exit(0))
311
+ .then((exitCode) => process.exit(exitCode || 0))
288
312
  .catch((error) => {
289
313
  console.log(error);
290
314
  process.exit(1);
@@ -40,14 +40,31 @@ export declare const keepDirOption: {
40
40
  name: string;
41
41
  type: BooleanConstructor;
42
42
  };
43
- export declare const commonOptions: ({
43
+ export declare const cloneOption: {
44
+ alias: string;
45
+ help: string;
46
+ name: string;
47
+ type: StringConstructor;
48
+ };
49
+ export declare const commitOption: {
50
+ help: string;
51
+ name: string;
52
+ type: BooleanConstructor;
53
+ };
54
+ export declare const remoteOption: {
44
55
  alias: string;
45
- defaultValue: string[];
46
56
  help: string;
47
57
  multiple: boolean;
48
58
  name: string;
49
59
  type: StringConstructor;
50
- } | {
60
+ };
61
+ export declare const pullOption: {
62
+ alias: string;
63
+ help: string;
64
+ name: string;
65
+ type: BooleanConstructor;
66
+ };
67
+ export declare const commonOptions: ({
51
68
  defaultOption: boolean;
52
69
  help: string;
53
70
  name: string;
@@ -60,4 +77,9 @@ export declare const commonOptions: ({
60
77
  help: string;
61
78
  name: string;
62
79
  type: BooleanConstructor;
80
+ } | {
81
+ alias: string;
82
+ help: string;
83
+ name: string;
84
+ type: StringConstructor;
63
85
  })[];
@@ -40,6 +40,30 @@ export const keepDirOption = {
40
40
  name: "keepDir",
41
41
  type: Boolean,
42
42
  };
43
+ export const cloneOption = {
44
+ alias: "C",
45
+ help: "clone repositories from given group (or organization) git URL",
46
+ name: "clone",
47
+ type: String,
48
+ };
49
+ export const commitOption = {
50
+ help: "commit clean files",
51
+ name: "commit",
52
+ type: Boolean,
53
+ };
54
+ export const remoteOption = {
55
+ alias: "r",
56
+ help: "push commit to given remote",
57
+ multiple: true,
58
+ name: "remote",
59
+ type: String,
60
+ };
61
+ export const pullOption = {
62
+ alias: "p",
63
+ help: "pull repositories before proceeding",
64
+ name: "pull",
65
+ type: Boolean,
66
+ };
43
67
  export const commonOptions = [
44
68
  categoriesOption,
45
69
  dataDirDefaultOption,
@@ -48,4 +72,8 @@ export const commonOptions = [
48
72
  verboseOption,
49
73
  onlyRecentOption,
50
74
  keepDirOption,
75
+ cloneOption,
76
+ commitOption,
77
+ remoteOption,
78
+ pullOption,
51
79
  ];
@@ -3,40 +3,6 @@ const xmlParser = new XMLParser({
3
3
  ignoreAttributes: false,
4
4
  attributeNamePrefix: "@_",
5
5
  });
6
- function getFirstInterventionChapterId(dataNvs) {
7
- const xml = xmlParser.parse(dataNvs);
8
- const rootChapters = xml?.data?.chapters?.chapter;
9
- if (!rootChapters)
10
- return null;
11
- const chaptersArray = Array.isArray(rootChapters) ? rootChapters : [rootChapters];
12
- let foundId = null;
13
- function dfsChapter(chapter) {
14
- if (foundId)
15
- return;
16
- const metas = chapter.metadata ? (Array.isArray(chapter.metadata) ? chapter.metadata : [chapter.metadata]) : [];
17
- const isIntervention = metas.some((m) => m?.["@_name"] === "type" && (m?.["@_value"] === "IN" || m?.["@_label"] === "Intervention"));
18
- const hasSpeaker = !!chapter.speaker;
19
- if (isIntervention && hasSpeaker && chapter["@_id"]) {
20
- foundId = String(chapter["@_id"]);
21
- return;
22
- }
23
- const children = chapter.chapter;
24
- if (!children)
25
- return;
26
- const childArray = Array.isArray(children) ? children : [children];
27
- for (const child of childArray) {
28
- dfsChapter(child);
29
- if (foundId)
30
- return;
31
- }
32
- }
33
- for (const ch of chaptersArray) {
34
- dfsChapter(ch);
35
- if (foundId)
36
- break;
37
- }
38
- return foundId;
39
- }
40
6
  function getTimecodeForChapterId(finalPlayerNvs, chapterId) {
41
7
  const xml = xmlParser.parse(finalPlayerNvs);
42
8
  const synchros = xml?.player?.synchro;
@@ -55,8 +21,19 @@ function getTimecodeForChapterId(finalPlayerNvs, chapterId) {
55
21
  return Math.floor(ms / 1000);
56
22
  }
57
23
  export function getFirstInterventionStartTimecode(dataNvs, finalPlayerNvs) {
58
- const firstChapterId = getFirstInterventionChapterId(dataNvs);
24
+ const firstChapterId = getFirstChapterId(dataNvs);
59
25
  if (!firstChapterId)
60
26
  return null;
61
27
  return getTimecodeForChapterId(finalPlayerNvs, firstChapterId);
62
28
  }
29
+ function getFirstChapterId(dataNvs) {
30
+ const xml = xmlParser.parse(dataNvs);
31
+ const rootChapters = xml?.data?.chapters?.chapter;
32
+ if (!rootChapters)
33
+ return null;
34
+ const chaptersArray = Array.isArray(rootChapters) ? rootChapters : [rootChapters];
35
+ const firstChapter = chaptersArray[0];
36
+ if (!firstChapter || !firstChapter["@_id"])
37
+ return null;
38
+ return String(firstChapter["@_id"]);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/senat",
3
- "version": "2.20.16",
3
+ "version": "2.20.18",
4
4
  "description": "Handle French Sénat's open data",
5
5
  "keywords": [
6
6
  "France",