From b86c1087540fa5600f1d1765c1a1905ab2b9349a Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sun, 13 Sep 2026 17:35:35 -0300 Subject: [PATCH] =?UTF-8?q?feat(C155):=20fallback=20de=20listagem=20desce?= =?UTF-8?q?=20ao=20m=C3=AAs=20quando=20o=20ano=20inteiro=20falha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/import-camara-speeches.mjs | 24 +++++++++++++------ scripts/lib/camaraSpeeches.mjs | 33 ++++++++++++++++---------- tests/unit/camaraSpeeches.unit.spec.ts | 11 +++++++++ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/scripts/import-camara-speeches.mjs b/scripts/import-camara-speeches.mjs index 7864a492..d59cb3f9 100644 --- a/scripts/import-camara-speeches.mjs +++ b/scripts/import-camara-speeches.mjs @@ -245,9 +245,10 @@ async function fetchSpeechesRange(deputyId, from, to) { /** * Lista a faixa inteira; se a API recusar uma página funda (500/hang - * persistente, visto na 55ª em 2026-09-13), cai para a listagem por ano — cada - * chunk é raso e um ano quebrado não derruba os demais. Os chunks que falharem - * são reportados via `onWarning` (o run os registra como falha de `list`). + * persistente, visto na 55ª em 2026-09-13), cai para a listagem por ano e, + * se um ano inteiro falhar, desce ao mês — cada chunk é raso e um mês + * quebrado não derruba os demais. Os chunks que falharem são reportados via + * `onWarning` (o run os registra como falha de `list`). */ async function fetchSpeeches(deputyId, from, to, onWarning = () => {}) { try { @@ -256,12 +257,21 @@ async function fetchSpeeches(deputyId, from, to, onWarning = () => {}) { onWarning(`listagem ${from}..${to} falhou (${error?.message}); tentando por ano`) const speeches = [] let listedChunks = 0 - for (const [chunkFrom, chunkTo] of dateRangeChunks(from, to)) { + for (const [yearFrom, yearTo] of dateRangeChunks(from, to)) { try { - speeches.push(...(await fetchSpeechesRange(deputyId, chunkFrom, chunkTo))) + speeches.push(...(await fetchSpeechesRange(deputyId, yearFrom, yearTo))) listedChunks += 1 - } catch (chunkError) { - onWarning(`listagem ${chunkFrom}..${chunkTo} falhou: ${chunkError?.message}`) + continue + } catch (yearError) { + onWarning(`listagem ${yearFrom}..${yearTo} falhou: ${yearError?.message}; tentando por mês`) + } + for (const [monthFrom, monthTo] of dateRangeChunks(yearFrom, yearTo, 'month')) { + try { + speeches.push(...(await fetchSpeechesRange(deputyId, monthFrom, monthTo))) + listedChunks += 1 + } catch (monthError) { + onWarning(`listagem ${monthFrom}..${monthTo} falhou: ${monthError?.message}`) + } } } if (listedChunks === 0) throw error diff --git a/scripts/lib/camaraSpeeches.mjs b/scripts/lib/camaraSpeeches.mjs index 01698860..7605d9a6 100644 --- a/scripts/lib/camaraSpeeches.mjs +++ b/scripts/lib/camaraSpeeches.mjs @@ -399,27 +399,36 @@ export const normalizeTranscription = (json) => { } } +const isIsoDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(String(value)) + /** - * Splits an inclusive `YYYY-MM-DD` range into calendar-year chunks (no - * overlap; first/last partial). The backfill fallback uses it when the API - * refuses a deep page: a shallow per-year listing still recovers the years - * the broken offset does not cover. + * Splits an inclusive `YYYY-MM-DD` range into calendar chunks (no overlap; + * first/last partial) at `unit` granularity. The backfill fallback uses it + * when the API refuses a deep page: a shallow per-year listing still recovers + * the years the broken offset does not cover, and a broken year is isolated + * down to the month. * * @param {string} from * @param {string} to + * @param {'year' | 'month'} unit * @returns {Array<[string, string]>} */ -export const dateRangeChunks = (from, to) => { +export const dateRangeChunks = (from, to, unit = 'year') => { const chunks = [] - if (!/^\d{4}-\d{2}-\d{2}$/.test(String(from)) || !/^\d{4}-\d{2}-\d{2}$/.test(String(to))) { - return chunks - } + if (!isIsoDate(from) || !isIsoDate(to)) return chunks let start = String(from) while (start <= String(to)) { - const yearEnd = `${start.slice(0, 4)}-12-31` - const end = yearEnd < String(to) ? yearEnd : String(to) - chunks.push([start, end]) - const next = new Date(`${end}T00:00:00Z`) + const end = + unit === 'month' + ? `${start.slice(0, 7)}-${String( + new Date( + Date.UTC(Number(start.slice(0, 4)), Number(start.slice(5, 7)), 0), + ).getUTCDate(), + ).padStart(2, '0')}` + : `${start.slice(0, 4)}-12-31` + const boundedEnd = end < String(to) ? end : String(to) + chunks.push([start, boundedEnd]) + const next = new Date(`${boundedEnd}T00:00:00Z`) next.setUTCDate(next.getUTCDate() + 1) start = next.toISOString().slice(0, 10) } diff --git a/tests/unit/camaraSpeeches.unit.spec.ts b/tests/unit/camaraSpeeches.unit.spec.ts index b878d3c7..1f04c7e6 100644 --- a/tests/unit/camaraSpeeches.unit.spec.ts +++ b/tests/unit/camaraSpeeches.unit.spec.ts @@ -656,4 +656,15 @@ describe('dateRangeChunks', () => { expect(dateRangeChunks('2023-1-1', '2023-12-31')).toEqual([]) expect(dateRangeChunks('', '2023-12-31')).toEqual([]) }) + + it('splits by month when asked, with the right month length', () => { + expect(dateRangeChunks('2018-01-01', '2018-03-15', 'month')).toEqual([ + ['2018-01-01', '2018-01-31'], + ['2018-02-01', '2018-02-28'], + ['2018-03-01', '2018-03-15'], + ]) + expect(dateRangeChunks('2024-02-01', '2024-02-29', 'month')).toEqual([ + ['2024-02-01', '2024-02-29'], + ]) + }) })