@usa-open-data-connectors/usa-sources 0.1.0

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.
Files changed (66) hide show
  1. package/README.md +339 -0
  2. package/dist/blsSeries.d.ts +73 -0
  3. package/dist/blsSeries.d.ts.map +1 -0
  4. package/dist/blsSeries.js +173 -0
  5. package/dist/blsSeries.js.map +1 -0
  6. package/dist/cdcCountyObesity.d.ts +116 -0
  7. package/dist/cdcCountyObesity.d.ts.map +1 -0
  8. package/dist/cdcCountyObesity.js +180 -0
  9. package/dist/cdcCountyObesity.js.map +1 -0
  10. package/dist/cfpbConsumerComplaints.d.ts +152 -0
  11. package/dist/cfpbConsumerComplaints.d.ts.map +1 -0
  12. package/dist/cfpbConsumerComplaints.js +273 -0
  13. package/dist/cfpbConsumerComplaints.js.map +1 -0
  14. package/dist/cpscProductRecalls.d.ts +146 -0
  15. package/dist/cpscProductRecalls.d.ts.map +1 -0
  16. package/dist/cpscProductRecalls.js +298 -0
  17. package/dist/cpscProductRecalls.js.map +1 -0
  18. package/dist/errors.d.ts +15 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +26 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/femaDisasterDeclarations.d.ts +123 -0
  23. package/dist/femaDisasterDeclarations.d.ts.map +1 -0
  24. package/dist/femaDisasterDeclarations.js +214 -0
  25. package/dist/femaDisasterDeclarations.js.map +1 -0
  26. package/dist/fixtures.d.ts +5 -0
  27. package/dist/fixtures.d.ts.map +1 -0
  28. package/dist/fixtures.js +11 -0
  29. package/dist/fixtures.js.map +1 -0
  30. package/dist/index.d.ts +51 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +27 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/nceiAnnualTemperature.d.ts +92 -0
  35. package/dist/nceiAnnualTemperature.d.ts.map +1 -0
  36. package/dist/nceiAnnualTemperature.js +158 -0
  37. package/dist/nceiAnnualTemperature.js.map +1 -0
  38. package/dist/noaaSeaLevel.d.ts +144 -0
  39. package/dist/noaaSeaLevel.d.ts.map +1 -0
  40. package/dist/noaaSeaLevel.js +252 -0
  41. package/dist/noaaSeaLevel.js.map +1 -0
  42. package/dist/openFdaFoodRecalls.d.ts +154 -0
  43. package/dist/openFdaFoodRecalls.d.ts.map +1 -0
  44. package/dist/openFdaFoodRecalls.js +293 -0
  45. package/dist/openFdaFoodRecalls.js.map +1 -0
  46. package/dist/registry.d.ts +28 -0
  47. package/dist/registry.d.ts.map +1 -0
  48. package/dist/registry.js +79 -0
  49. package/dist/registry.js.map +1 -0
  50. package/dist/treasuryAvgInterestRate.d.ts +107 -0
  51. package/dist/treasuryAvgInterestRate.d.ts.map +1 -0
  52. package/dist/treasuryAvgInterestRate.js +191 -0
  53. package/dist/treasuryAvgInterestRate.js.map +1 -0
  54. package/dist/types.d.ts +30 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/usgsEarthquakes.d.ts +108 -0
  59. package/dist/usgsEarthquakes.d.ts.map +1 -0
  60. package/dist/usgsEarthquakes.js +199 -0
  61. package/dist/usgsEarthquakes.js.map +1 -0
  62. package/dist/usgsPeakStreamflow.d.ts +116 -0
  63. package/dist/usgsPeakStreamflow.d.ts.map +1 -0
  64. package/dist/usgsPeakStreamflow.js +211 -0
  65. package/dist/usgsPeakStreamflow.js.map +1 -0
  66. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # @usa-open-data-connectors/usa-sources
2
+
3
+ Uniform TypeScript adapters for US public data sources. Keyless first, with a
4
+ strict parser and a committed fixture behind every adapter so a build never
5
+ depends on a live API.
6
+
7
+ The HTTP API and the CLI read this package's registry (`US_DATA_SOURCES`), so a
8
+ new adapter is exposed on `/api/sources`, `/api/sources/{id}/probe`, and
9
+ `/api/sources/{id}/data` as soon as it is registered.
10
+
11
+ ## Sources
12
+
13
+ | id | publisher | auth | what it reads |
14
+ | ---------------------------- | --------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------- |
15
+ | `bls-unemployment-rate` | Bureau of Labor Statistics | none | The national unemployment rate, monthly, seasonally adjusted |
16
+ | `usgs-hawaii-earthquakes` | US Geological Survey | none | Earthquakes of magnitude 2.5 and above near the Hawaiian islands |
17
+ | `cdc-county-obesity` | Centers for Disease Control (CDC) | none | The share of adults with obesity in each US county, from CDC PLACES |
18
+ | `ncei-annual-temperature` | NOAA National Centers for Environmental Information | none | Calendar-year average temperature for the contiguous United States, since 1895 |
19
+ | `noaa-sea-level` | NOAA Center for Operational Oceanographic Products and Services | none | Monthly mean sea level at a tide gauge, folded into calendar-year averages, since 1856 at The Battery |
20
+ | `treasury-avg-interest-rate` | US Department of the Treasury | none | Average interest rate on the interest-bearing federal debt outstanding, monthly since 2001 |
21
+ | `fema-disaster-declarations` | Federal Emergency Management Agency | none | Every disaster declaration FEMA has published, one row per declaration, from 1953 to the newest one |
22
+ | `openfda-food-recalls` | US Food and Drug Administration | none | Every food recall FDA has published as an enforcement report, from June 2012 to the newest publication date |
23
+ | `usgs-peak-streamflow` | US Geological Survey | none | The annual peak-flow record at a USGS stream gauge, one row per water year, since 1844 at the Mississippi River at St. Louis |
24
+ | `cpsc-product-recalls` | US Consumer Product Safety Commission | none | Every consumer product recall CPSC has published, one row per recall, from 2014 to the newest one |
25
+ | `cfpb-consumer-complaints` | Consumer Financial Protection Bureau | none | Every consumer complaint sent to a company since December 2011, counted by year, with the products and companies named most |
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { fetchBlsSeries, US_UNEMPLOYMENT_SERIES_ID } from '@usa-open-data-connectors/usa-sources';
31
+
32
+ const rate = await fetchBlsSeries(US_UNEMPLOYMENT_SERIES_ID, {
33
+ startYear: 2006,
34
+ endYear: 2025,
35
+ });
36
+ console.log(rate.latest.year, rate.latest.period, rate.latest.value);
37
+ ```
38
+
39
+ ```ts
40
+ import {
41
+ buildUsgsEarthquakeCatalogue,
42
+ fetchUsgsEarthquakes,
43
+ USGS_HAWAII_BOUNDS,
44
+ USGS_HAWAII_MIN_MAGNITUDE,
45
+ } from '@usa-open-data-connectors/usa-sources';
46
+
47
+ const earthquakes = await fetchUsgsEarthquakes({
48
+ startDate: '2025-01-01',
49
+ endDate: '2026-01-01',
50
+ minMagnitude: USGS_HAWAII_MIN_MAGNITUDE,
51
+ bounds: USGS_HAWAII_BOUNDS,
52
+ });
53
+ const catalogue = buildUsgsEarthquakeCatalogue(earthquakes);
54
+ console.log(catalogue.count, catalogue.strongest.place, catalogue.strongest.magnitude);
55
+ ```
56
+
57
+ ```ts
58
+ import {
59
+ buildCdcCountyObesitySet,
60
+ fetchCdcCountyObesity,
61
+ } from '@usa-open-data-connectors/usa-sources';
62
+
63
+ const set = buildCdcCountyObesitySet(await fetchCdcCountyObesity());
64
+ console.log(set.countyCount, set.lowest.countyName, set.highest.percent, set.national.percent);
65
+ ```
66
+
67
+ ```ts
68
+ import {
69
+ buildNceiAnnualTemperatureSeries,
70
+ fetchNceiAnnualTemperature,
71
+ } from '@usa-open-data-connectors/usa-sources';
72
+
73
+ const series = await fetchNceiAnnualTemperature({
74
+ startYear: 1895,
75
+ endYear: new Date().getFullYear(),
76
+ });
77
+ console.log(series.yearCount, series.warmest.year, series.coldest.valueFahrenheit);
78
+ ```
79
+
80
+ ```ts
81
+ import { buildNoaaSeaLevelSeries, fetchNoaaSeaLevel } from '@usa-open-data-connectors/usa-sources';
82
+
83
+ const seaLevel = buildNoaaSeaLevelSeries(await fetchNoaaSeaLevel());
84
+ console.log(
85
+ seaLevel.station.name,
86
+ seaLevel.yearCount,
87
+ seaLevel.highest.year,
88
+ seaLevel.trendMillimetresPerYear
89
+ );
90
+ ```
91
+
92
+ ```ts
93
+ import { fetchTreasuryAvgInterestRates } from '@usa-open-data-connectors/usa-sources';
94
+
95
+ const rates = await fetchTreasuryAvgInterestRates();
96
+ console.log(rates.monthCount, rates.lastMonth.averageInterestRatePercent, rates.lowest.recordDate);
97
+ ```
98
+
99
+ ```ts
100
+ import { fetchFemaDeclarations } from '@usa-open-data-connectors/usa-sources';
101
+
102
+ const declarations = await fetchFemaDeclarations();
103
+ console.log(
104
+ declarations.declarationCount,
105
+ declarations.busiestYear.year,
106
+ declarations.incidentTypes[0]?.name
107
+ );
108
+ ```
109
+
110
+ ```ts
111
+ import {
112
+ buildUsgsPeakStreamflowSeries,
113
+ fetchUsgsPeakStreamflow,
114
+ } from '@usa-open-data-connectors/usa-sources';
115
+
116
+ const record = await fetchUsgsPeakStreamflow();
117
+ console.log(
118
+ record.yearCount,
119
+ record.highest.waterYear,
120
+ record.highest.peakDischargeCubicFeetPerSecond,
121
+ record.highestToLowestRatio
122
+ );
123
+ ```
124
+
125
+ ```ts
126
+ import { fetchCpscProductRecalls } from '@usa-open-data-connectors/usa-sources';
127
+
128
+ const recalls = await fetchCpscProductRecalls();
129
+ console.log(
130
+ recalls.totalRecalls,
131
+ recalls.busiestCompleteYear.year,
132
+ recalls.remedyOptions[0]?.option,
133
+ recalls.manufacturerCountries[0]?.country
134
+ );
135
+ ```
136
+
137
+ ```ts
138
+ import { fetchCfpConsumerComplaints } from '@usa-open-data-connectors/usa-sources';
139
+
140
+ const complaints = await fetchCfpConsumerComplaints();
141
+ console.log(
142
+ complaints.totalComplaints,
143
+ complaints.busiestCompleteYear.year,
144
+ complaints.newestReceivedDate,
145
+ complaints.topCompanies[0]?.company
146
+ );
147
+ ```
148
+
149
+ ## Notes on the BLS API
150
+
151
+ - The public API refuses a request spanning more than ten years, so
152
+ `fetchBlsSeries` splits a longer range into windows: two decades cost three
153
+ calls once the range runs into the current year.
154
+ - Keyless access allows 25 series queries a day. A registered key raises that
155
+ to 500, and the adapter takes `apiKey` for the day that matters.
156
+ - The agency uses `-` for a month it could not publish, and `M13` for an
157
+ annual average. Both are dropped, so one year holds twelve monthly values.
158
+
159
+ ## Notes on the USGS earthquake catalogue
160
+
161
+ - The event query answers a window of any length, one request per window. The
162
+ box, the magnitude floor, and the dates come from the caller.
163
+ - The feed mixes tectonic earthquakes with quarry blasts, explosions, and ice
164
+ quakes, and a row can arrive without a magnitude. The parser keeps only
165
+ tectonic earthquakes that carry a magnitude and a depth.
166
+ - The catalogue answers newest first. `parseUsgsEarthquakes` returns oldest
167
+ first, so a caller reading index 0 gets the start of the window.
168
+
169
+ ## Notes on the CDC PLACES county release
170
+
171
+ - The resource id is per release. `cdc-county-obesity` reads the 2025
172
+ release, whose estimates come from the 2023 Behavioral Risk Factor
173
+ Surveillance System (BRFSS) and the Census Bureau's 2023 county population
174
+ estimates. A new release gets a new id, so a caller pinned to this one
175
+ keeps reading the numbers it was written against.
176
+ - Every row is a model-based estimate, not a count. The release publishes
177
+ each measure twice, as crude and as age-adjusted prevalence; the adapter
178
+ reads the crude rows, because age-adjusted ones answer a different question.
179
+ - A county the model could not estimate arrives with no value, and the
180
+ release carries no obesity rows at all for Kentucky and Pennsylvania. Rows
181
+ without a value are dropped, and the national row is kept apart from the
182
+ counties rather than counted as one.
183
+
184
+ ## Notes on the Climate at a Glance download
185
+
186
+ - The download is a CSV, not JSON: two comment lines, a header, then one row
187
+ per year as `YYYYMM,value` in degrees Fahrenheit. `parseNceiAnnualTemperatureCsv`
188
+ skips the comments and the header, and stops on any other line shape rather
189
+ than dropping it.
190
+ - The URL carries a window length and a window end month. Twelve months ending
191
+ in December is the calendar year, which is the number a story about a year
192
+ can compare. A shorter window ending in December would be that month alone,
193
+ and the parser refuses a file with more than one row per year.
194
+ - The current year joins the file only once December has closed it, so the
195
+ newest row is the last complete calendar year. The 2026 download is dated
196
+ 2026-09-26 and ends at 2025.
197
+
198
+ ## Notes on the CO-OPS sea level record
199
+
200
+ - One request covers the whole window: the `monthly_mean` product answers one
201
+ row per month rather than one row per reading, so 170 years cost one call
202
+ and about 460 KB. The endpoint is keyless.
203
+ - Values are metres against the station's MSL datum, the average of hourly
204
+ heights over the 1983-2001 National Tidal Datum Epoch. Reading the whole
205
+ record against one fixed epoch is what makes two years comparable.
206
+ - The API answers a row for every month in the window and leaves the value
207
+ blank when it has none, which is how the early gaps arrive (1920 holds
208
+ seven months, 1861 and 1879-1892 hold none). Blank rows are dropped, and a
209
+ year's average is taken over the months it does have, so `monthCount` says
210
+ how much of the year a value covers.
211
+ - Errors come back in the body with HTTP 200, as
212
+ `{"error":{"message":"..."}}`, so the parser checks for that before reading
213
+ the data. A wrong product name answers with plain text instead, which fails
214
+ the shape check.
215
+
216
+ ## Notes on the Treasury average interest rate
217
+
218
+ - One keyless request covers the whole run. The dataset answers a row per
219
+ month per security type, and the filter pins the response to
220
+ `security_type_desc:eq:Interest-bearing Debt`, which is the portfolio total
221
+ the bureau's own monthly statement is built from. The other rows, the rates
222
+ on bills, notes, bonds, and non-marketable debt, never reach the parser.
223
+ - The rate is the average across everything outstanding, not the rate on
224
+ anything issued today. That is why it moves slowly: a new bill reprices a
225
+ fraction of the portfolio, and a bond issued in 2019 still pays its own
226
+ coupon.
227
+ - A page holds at most 10,000 rows and the run is around 300, so the adapter
228
+ asks for the whole series in one request and does not page. A run longer
229
+ than the cap would be a change worth making here rather than a silent
230
+ truncation, so the row limit is a named constant.
231
+ - Each row is dated to the last day of its month. A month the agency could
232
+ not compute would arrive without a value, and those rows are dropped.
233
+ - A request the API rejects answers with an HTTP error and a body of
234
+ `{"error":"Invalid Query Param","message":"..."}`, which the parser reports
235
+ as an API error rather than a shape error.
236
+
237
+ ## Notes on the USGS peak-flow record
238
+
239
+ - The adapter reads the `peaks` collection on the USGS Water Data OGC API
240
+ (`https://api.waterdata.usgs.gov/ogcapi/v0`), not the older
241
+ `waterservices.usgs.gov` endpoints. The older statistics service answers
242
+ `503 Service unavailable` to roughly half of scripted requests, which is
243
+ enough to fail a build; the OGC API answered every request in the same
244
+ window. The daily-values service on the older host still works and is worth
245
+ using when a daily series is what a story needs.
246
+ - Every number in a row arrives as a string, including the peak itself, so
247
+ the parser converts once and keeps the number. `value` blank means the
248
+ agency filed no peak for that year, and the row is dropped rather than
249
+ read as zero.
250
+ - The rows are dated by **water year**, which starts on 1 October. A peak on
251
+ 1955-10-08 is filed under water year 1956, so `waterYear` and
252
+ `calendarYear` are separate fields and are not always the same number. The
253
+ record is sorted and charted by water year, because that is the unit the
254
+ agency files and the unit a flood season belongs to.
255
+ - The record has holes. At the default gauge there is no row for the 17
256
+ water years from 1845 to 1861, and the series lists them in
257
+ `missingWaterYears` rather than drawing them as zero, because a year with
258
+ no filing is not a year the river ran dry.
259
+ - A gauge with several peaks in one water year is filed once, with the
260
+ largest. The adapter does not de-duplicate, so a response carrying two rows
261
+ for one water year would be a change in the source worth seeing.
262
+ - Rows carry the agency's qualifiers, for example `UNKNOWNREGULATION` where
263
+ the agency could not tell whether works upstream changed the flow, or
264
+ `MAXDAILYMEAN` where the peak is a daily mean rather than an instantaneous
265
+ reading. They are kept on each year rather than dropped, so the copy can be
266
+ honest about them.
267
+ - One keyless request covers the whole record. The default gauge answers 165
268
+ water years in one page of about 126 KB.
269
+
270
+ ## Notes on the CPSC recall service
271
+
272
+ - The adapter reads
273
+ `https://www.saferproducts.gov/RestWebServices/Recall`, the same service
274
+ behind the public SaferProducts.gov search. It is keyless.
275
+ - One request covers one calendar year. A range wider than a year fails: the
276
+ service answers HTTP 200 with a single row whose title reads
277
+ `Error retrieving Recalls: The underlying provider failed on Open.` The
278
+ adapter reads a year at a time for that reason, and the parser throws on
279
+ that row rather than counting it as a recall.
280
+ - A year of rows is heavy as JSON. 2025 is 420 recalls in about 1.3 MB, so the
281
+ default window of 2014 to the current year costs about 11 MB across 13
282
+ requests. A year with no recall keeps a zero row in `years` rather than
283
+ disappearing from the chart.
284
+ - The newest year in the window is the year the agency is still filling, so
285
+ `busiestCompleteYear` and `quietestCompleteYear` are picked from the years
286
+ before it. `completeYearCount` says how many those were.
287
+ - `ManufacturerCountries` names every country a recall touches, so a recall
288
+ can list several and the country counts add to more than the recall count.
289
+ In the 2014 to 2026 window China appears on 2,312 of 3,986 recalls, the
290
+ United States on 753, and 122 countries appear at least once.
291
+ - `RemedyOptions` is nearly an enum (Refund, Repair, Replace, Dispose, New
292
+ Instructions, Label, Inspect) with two junk rows in it: one carries `R` and
293
+ one carries a whole paragraph of consumer instructions. The adapter keeps
294
+ whatever the agency sends and sorts the counts, so the tail of the list
295
+ shows the mess rather than hiding it.
296
+ - The service holds recalls from the 1970s. The adapter starts at 2014, which
297
+ keeps the window to 13 requests and to the years a reading of the current
298
+ trend needs.
299
+ - The committed snapshot (`cpsc-product-recalls-2026-10-02.json`) holds the
300
+ folded counts, not the raw years, because a year of raw rows is about a
301
+ megabyte and the counts are a few kilobytes. The second fixture
302
+ (`cpsc-recall-rows-2014-2026-sampled-2026-10-02.json`) holds the first three
303
+ recalls of every year in the window, which is what the registry test answers
304
+ each per-year request with.
305
+
306
+ ## Notes on the Consumer Complaint Database
307
+
308
+ - The adapter reads
309
+ `https://www.consumerfinance.gov/data-research/consumer-complaints/search/api/v1/`,
310
+ the search endpoint behind the public Consumer Complaint Database. It is
311
+ keyless. The bureau answers a scripted request with 403 and a request with a
312
+ declared user agent with 200.
313
+ - The search API computes its term aggregations on every request, which takes
314
+ the body from about fifteen kilobytes to about four hundred. Adding
315
+ `no_aggs=true` drops them, so the adapter asks for `size=0&no_aggs=true` and
316
+ reads `hits.total.value` when it only needs a count, and makes one extra
317
+ request without `no_aggs` for the product and company tallies.
318
+ - The API answers one question per request and has no date sort, so the window
319
+ costs one request per year plus one for the tallies plus one or two to find
320
+ the newest day. `findNewestCfpComplaintDate` walks back from today and stops
321
+ at the first day with a complaint.
322
+ - The first year in the window, 2011, holds only December: the bureau
323
+ published its first complaints on 1 December 2011. It is kept rather than
324
+ dropped, and the page's data note says the first bar covers one month.
325
+ - The product tally splits credit reporting across more than one label because
326
+ the bureau changed its product taxonomy over the years. The adapter keeps
327
+ whatever the API sends rather than merging labels, so a reader of
328
+ `topProducts` should not assume the labels are stable.
329
+ - The committed snapshot (`cfpb-consumer-complaints-2026-10-03.json`) holds
330
+ the folded counts, not the eighteen million raw rows. The registry test
331
+ answers each per-year request, the newest-day probe, and the tally request
332
+ from that one fixture.
333
+
334
+ ## Checks
335
+
336
+ ```sh
337
+ npm run test --workspace @usa-open-data-connectors/usa-sources
338
+ npm run test:smoke --workspace @usa-open-data-connectors/usa-sources # hits the live API
339
+ ```
@@ -0,0 +1,73 @@
1
+ import type { UsDataAdapter } from './types.js';
2
+ /** One month of a BLS time series. */
3
+ export interface BlsObservation {
4
+ seriesId: string;
5
+ year: number;
6
+ /** BLS period code: M01 to M12 for months, M13 for an annual average. */
7
+ period: string;
8
+ periodName: string;
9
+ value: number;
10
+ }
11
+ /** Rolled-up facts about one BLS series. */
12
+ export interface BlsSeries {
13
+ seriesId: string;
14
+ points: BlsObservation[];
15
+ first: BlsObservation;
16
+ latest: BlsObservation;
17
+ peak: BlsObservation;
18
+ lowest: BlsObservation;
19
+ changeFromPeak: number;
20
+ changeFromFirst: number;
21
+ }
22
+ /**
23
+ * The national unemployment rate, seasonally adjusted. Published monthly by
24
+ * the Bureau of Labor Statistics, the agency that runs the Current Population
25
+ * Survey.
26
+ */
27
+ export declare const US_UNEMPLOYMENT_SERIES_ID = "LNS14000000";
28
+ /** Human-readable name for the unemployment series. */
29
+ export declare const US_UNEMPLOYMENT_SERIES_LABEL = "US unemployment rate, seasonally adjusted";
30
+ /** Base URL for the BLS public data API. */
31
+ export declare const BLS_API_BASE = "https://api.bls.gov/publicAPI/v2/timeseries/data";
32
+ /**
33
+ * The BLS public API refuses a request spanning more than ten years, so a
34
+ * longer range has to be assembled from several windows.
35
+ */
36
+ export declare const BLS_MAX_YEARS_PER_REQUEST = 10;
37
+ /**
38
+ * Parses a BLS timeseries payload into flat observations.
39
+ *
40
+ * The annual-average period (M13) is dropped, because mixing it with the
41
+ * twelve monthly points would put two values on the same year.
42
+ *
43
+ * @param payload - the JSON body from the BLS timeseries endpoint
44
+ * @returns one observation per series per month
45
+ */
46
+ export declare function parseBlsObservations(payload: unknown): BlsObservation[];
47
+ /**
48
+ * Folds observations into one series with its high, low, and end points.
49
+ *
50
+ * @param observations - monthly observations, in any order
51
+ * @param seriesId - the series to keep; rows from other series are ignored
52
+ * @returns the ordered series with its headline points
53
+ */
54
+ export declare function buildBlsSeries(observations: BlsObservation[], seriesId: string): BlsSeries;
55
+ /**
56
+ * Reads a BLS series across a range of years, splitting the range into
57
+ * windows the API will accept.
58
+ *
59
+ * Keyless. The public API allows 25 series queries a day without a key and
60
+ * 500 with one, so a long range costs more than one call.
61
+ *
62
+ * @param seriesId - the BLS series id, such as LNS14000000
63
+ * @param options - the year range to read, and an optional fetch stub
64
+ * @returns every monthly observation in the range, oldest first
65
+ */
66
+ export declare function fetchBlsSeries(seriesId: string, options: {
67
+ startYear: number;
68
+ endYear: number;
69
+ fetchImpl?: typeof globalThis.fetch;
70
+ }): Promise<BlsSeries>;
71
+ /** National unemployment rate adapter, keyless. */
72
+ export declare const blsUnemploymentAdapter: UsDataAdapter<BlsSeries>;
73
+ //# sourceMappingURL=blsSeries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blsSeries.d.ts","sourceRoot":"","sources":["../src/blsSeries.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,4CAA4C;AAC5C,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AAEvD,uDAAuD;AACvD,eAAO,MAAM,4BAA4B,8CAA8C,CAAC;AAExF,4CAA4C;AAC5C,eAAO,MAAM,YAAY,qDAAqD,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAwB5C;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,EAAE,CAiCvE;AAkBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+B1F;AAiBD;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAE,GACnF,OAAO,CAAC,SAAS,CAAC,CAepB;AAED,mDAAmD;AACnD,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,SAAS,CAmB3D,CAAC"}
@@ -0,0 +1,173 @@
1
+ import { z } from 'zod';
2
+ import { UsSourceApiError, UsSourceParseError } from './errors.js';
3
+ import { readFixtureJson } from './fixtures.js';
4
+ /**
5
+ * The national unemployment rate, seasonally adjusted. Published monthly by
6
+ * the Bureau of Labor Statistics, the agency that runs the Current Population
7
+ * Survey.
8
+ */
9
+ export const US_UNEMPLOYMENT_SERIES_ID = 'LNS14000000';
10
+ /** Human-readable name for the unemployment series. */
11
+ export const US_UNEMPLOYMENT_SERIES_LABEL = 'US unemployment rate, seasonally adjusted';
12
+ /** Base URL for the BLS public data API. */
13
+ export const BLS_API_BASE = 'https://api.bls.gov/publicAPI/v2/timeseries/data';
14
+ /**
15
+ * The BLS public API refuses a request spanning more than ten years, so a
16
+ * longer range has to be assembled from several windows.
17
+ */
18
+ export const BLS_MAX_YEARS_PER_REQUEST = 10;
19
+ const BLS_PERIOD_CODE = new RegExp('^M(0[1-9]|1[0-2])$');
20
+ const BLS_OBSERVATION_SCHEMA = z.object({
21
+ year: z.string(),
22
+ period: z.string(),
23
+ periodName: z.string(),
24
+ value: z.string(),
25
+ });
26
+ const BLS_SERIES_SCHEMA = z.object({
27
+ seriesID: z.string(),
28
+ data: z.array(BLS_OBSERVATION_SCHEMA),
29
+ });
30
+ const BLS_RESPONSE_SCHEMA = z.object({
31
+ status: z.string(),
32
+ message: z.array(z.string()).optional(),
33
+ Results: z.object({
34
+ series: z.array(BLS_SERIES_SCHEMA),
35
+ }),
36
+ });
37
+ /**
38
+ * Parses a BLS timeseries payload into flat observations.
39
+ *
40
+ * The annual-average period (M13) is dropped, because mixing it with the
41
+ * twelve monthly points would put two values on the same year.
42
+ *
43
+ * @param payload - the JSON body from the BLS timeseries endpoint
44
+ * @returns one observation per series per month
45
+ */
46
+ export function parseBlsObservations(payload) {
47
+ const parsed = BLS_RESPONSE_SCHEMA.safeParse(payload);
48
+ if (!parsed.success) {
49
+ throw new UsSourceParseError('bls', parsed.error.message);
50
+ }
51
+ if (parsed.data.status !== 'REQUEST_SUCCEEDED') {
52
+ throw new UsSourceApiError('bls', `${parsed.data.status}: ${(parsed.data.message ?? []).join('; ')}`);
53
+ }
54
+ const observations = [];
55
+ for (const series of parsed.data.Results.series) {
56
+ for (const row of series.data) {
57
+ if (!BLS_PERIOD_CODE.test(row.period)) {
58
+ continue;
59
+ }
60
+ const year = Number(row.year);
61
+ const value = Number(row.value);
62
+ if (!Number.isInteger(year) || !Number.isFinite(value)) {
63
+ continue;
64
+ }
65
+ observations.push({
66
+ seriesId: series.seriesID,
67
+ year,
68
+ period: row.period,
69
+ periodName: row.periodName,
70
+ value,
71
+ });
72
+ }
73
+ }
74
+ return sortObservations(observations);
75
+ }
76
+ /**
77
+ * Sorts an observation list oldest first. The agency answers newest first,
78
+ * and a caller reading `points[0]` should get the start of the series.
79
+ *
80
+ * @param observations - observations in any order
81
+ * @returns the same observations, oldest first
82
+ */
83
+ function sortObservations(observations) {
84
+ return [...observations].sort((left, right) => {
85
+ if (left.year !== right.year) {
86
+ return left.year - right.year;
87
+ }
88
+ return left.period.localeCompare(right.period);
89
+ });
90
+ }
91
+ /**
92
+ * Folds observations into one series with its high, low, and end points.
93
+ *
94
+ * @param observations - monthly observations, in any order
95
+ * @param seriesId - the series to keep; rows from other series are ignored
96
+ * @returns the ordered series with its headline points
97
+ */
98
+ export function buildBlsSeries(observations, seriesId) {
99
+ const points = sortObservations(observations.filter((observation) => observation.seriesId === seriesId));
100
+ const first = points[0];
101
+ const latest = points[points.length - 1];
102
+ if (first === undefined || latest === undefined) {
103
+ throw new UsSourceParseError('bls', `No observations for series ${seriesId}`);
104
+ }
105
+ let peak = first;
106
+ let lowest = first;
107
+ for (const point of points) {
108
+ if (point.value > peak.value) {
109
+ peak = point;
110
+ }
111
+ if (point.value < lowest.value) {
112
+ lowest = point;
113
+ }
114
+ }
115
+ return {
116
+ seriesId,
117
+ points,
118
+ first,
119
+ latest,
120
+ peak,
121
+ lowest,
122
+ changeFromPeak: latest.value - peak.value,
123
+ changeFromFirst: latest.value - first.value,
124
+ };
125
+ }
126
+ /** Reads one ten-year-or-shorter window from the BLS API. */
127
+ async function fetchBlsWindow(seriesId, startYear, endYear, fetchImpl) {
128
+ const url = `${BLS_API_BASE}/${seriesId}?startyear=${startYear}&endyear=${endYear}`;
129
+ const response = await fetchImpl(url);
130
+ if (!response.ok) {
131
+ throw new UsSourceApiError('bls', `HTTP ${response.status} reading series ${seriesId}`);
132
+ }
133
+ return parseBlsObservations(await response.json());
134
+ }
135
+ /**
136
+ * Reads a BLS series across a range of years, splitting the range into
137
+ * windows the API will accept.
138
+ *
139
+ * Keyless. The public API allows 25 series queries a day without a key and
140
+ * 500 with one, so a long range costs more than one call.
141
+ *
142
+ * @param seriesId - the BLS series id, such as LNS14000000
143
+ * @param options - the year range to read, and an optional fetch stub
144
+ * @returns every monthly observation in the range, oldest first
145
+ */
146
+ export async function fetchBlsSeries(seriesId, options) {
147
+ const { startYear, endYear } = options;
148
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
149
+ const observations = [];
150
+ for (let windowStart = startYear; windowStart <= endYear; windowStart += BLS_MAX_YEARS_PER_REQUEST) {
151
+ const windowEnd = Math.min(windowStart + BLS_MAX_YEARS_PER_REQUEST - 1, endYear);
152
+ observations.push(...(await fetchBlsWindow(seriesId, windowStart, windowEnd, fetchImpl)));
153
+ }
154
+ return buildBlsSeries(observations, seriesId);
155
+ }
156
+ /** National unemployment rate adapter, keyless. */
157
+ export const blsUnemploymentAdapter = {
158
+ id: 'bls-unemployment-rate',
159
+ name: US_UNEMPLOYMENT_SERIES_LABEL,
160
+ auth: 'none',
161
+ description: 'Monthly national unemployment rate from the Current Population Survey.',
162
+ fetchLive: async (options) => {
163
+ const endYear = new Date().getUTCFullYear();
164
+ return fetchBlsSeries(US_UNEMPLOYMENT_SERIES_ID, {
165
+ startYear: endYear - 19,
166
+ endYear,
167
+ ...(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
168
+ });
169
+ },
170
+ parse: (payload) => buildBlsSeries(parseBlsObservations(payload), US_UNEMPLOYMENT_SERIES_ID),
171
+ loadFixture: () => buildBlsSeries(parseBlsObservations(readFixtureJson('bls-unemployment-rate.json')), US_UNEMPLOYMENT_SERIES_ID),
172
+ };
173
+ //# sourceMappingURL=blsSeries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blsSeries.js","sourceRoot":"","sources":["../src/blsSeries.ts"],"names":[],"mappings":"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"}