@fin.cx/opendata 4.3.1 → 4.5.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.
@@ -0,0 +1,292 @@
1
+ import { CpiError } from './classes.cpierror.js';
2
+ import {
3
+ bundesbankCpiBaseUrl,
4
+ bundesbankVpiAnnualKey,
5
+ bundesbankVpiFirstMonth,
6
+ bundesbankVpiFirstYear,
7
+ bundesbankVpiMonthlyKey,
8
+ destatisLicence,
9
+ } from './constants.js';
10
+ import { parseDecimal } from './decimal.js';
11
+ import { frequencyOf } from './indexmath.js';
12
+ import type {
13
+ ICpiAnnualInput,
14
+ ICpiClientOptions,
15
+ ICpiMonthlyInput,
16
+ ICpiReading,
17
+ ICpiSeries,
18
+ ICpiSourceNote,
19
+ ICpiValue,
20
+ TCpiFrequency,
21
+ TCpiPeriod,
22
+ } from './interfaces.js';
23
+ import { getText, timeoutOf } from './request.js';
24
+
25
+ /**
26
+ * The columns of the API's SDMX-CSV answer this client reads. The API leaves out a column that is empty
27
+ * in every row: `OBS_STATUS` appears only where a value carries a status.
28
+ */
29
+ const COLUMNS = ['BBK_STD_FREQ', 'TIME_PERIOD', 'OBS_VALUE', 'BBK_DECIMALS', 'BBK_ID', 'BBK_UNIT', 'BBK_TITLE'] as const;
30
+ const STATUS_COLUMN = 'OBS_STATUS';
31
+
32
+ /** The key of the overall index for Germany on one base, as the answer names it. */
33
+ const SERIES_KEY = /^BBDP1\.([MA])\.DE\.N\.VPI\.C\.A00000\.I(\d{2})\.A$/u;
34
+
35
+ const UNIT = /^(\d{4})=100$/u;
36
+
37
+ const FREQUENCY_CODES: Readonly<Record<TCpiFrequency, string>> = { monthly: 'M', annual: 'A' };
38
+
39
+ /** What the API's `OBS_STATUS` says about a value; any other flag is not read. */
40
+ const STATUSES: Readonly<Record<string, ICpiValue['status']>> = { '': 'final', E: 'estimated' };
41
+
42
+ interface IRange {
43
+ frequency: TCpiFrequency;
44
+ from: TCpiPeriod;
45
+ to?: TCpiPeriod;
46
+ }
47
+
48
+ interface IReadSeries {
49
+ values: ICpiValue[];
50
+ estimates: ICpiValue[];
51
+ base: string;
52
+ baseYear: number;
53
+ key: string;
54
+ title: string;
55
+ }
56
+
57
+ /** One line of semicolon-separated values; a field in double quotes may hold separators and doubled quotes. */
58
+ const splitLine = (lineArg: string): string[] | undefined => {
59
+ const fields: string[] = [];
60
+ let field = '';
61
+ let quoted = false;
62
+ for (let position = 0; position < lineArg.length; position++) {
63
+ const character = lineArg[position];
64
+ if (quoted) {
65
+ if (character === '"' && lineArg[position + 1] === '"') {
66
+ field += '"';
67
+ position++;
68
+ } else if (character === '"') {
69
+ quoted = false;
70
+ } else {
71
+ field += character;
72
+ }
73
+ } else if (character === '"' && field === '') {
74
+ quoted = true;
75
+ } else if (character === ';') {
76
+ fields.push(field);
77
+ field = '';
78
+ } else {
79
+ field += character;
80
+ }
81
+ }
82
+ if (quoted) {
83
+ return undefined;
84
+ }
85
+ fields.push(field);
86
+ return fields;
87
+ };
88
+
89
+ /**
90
+ * The Verbraucherpreisindex für Deutschland (overall index, unadjusted) of the Statistisches Bundesamt as
91
+ * the Deutsche Bundesbank republishes it in its time series API (`bundesbankCpiBaseUrl`), monthly and as
92
+ * annual averages. Each answer names its base, and the values the Bundesbank marks as its own estimate
93
+ * (the latest month at times) are kept apart from the published ones. Nothing is kept: each answer goes
94
+ * to the caller with the source note to show and the answer as it arrived.
95
+ */
96
+ export class BundesbankCpiClient {
97
+ private readonly baseUrl: string;
98
+
99
+ constructor(private readonly options: ICpiClientOptions = {}) {
100
+ timeoutOf(options);
101
+ this.baseUrl = (options.baseUrl ?? bundesbankCpiBaseUrl).replace(/\/?$/u, '/');
102
+ }
103
+
104
+ /** The months from `from` to `to`, both included, or to the latest one when `to` is left out. */
105
+ public async getMonthly(inputArg: ICpiMonthlyInput): Promise<ICpiSeries> {
106
+ return this.getSeries({ frequency: 'monthly', from: inputArg.from, ...(inputArg.to !== undefined ? { to: inputArg.to } : {}) }, inputArg.signal);
107
+ }
108
+
109
+ /** The annual averages from `from` to `to`, both included, or to the latest one when `to` is left out. */
110
+ public async getAnnual(inputArg: ICpiAnnualInput): Promise<ICpiSeries> {
111
+ return this.getSeries({ frequency: 'annual', from: inputArg.from, ...(inputArg.to !== undefined ? { to: inputArg.to } : {}) }, inputArg.signal);
112
+ }
113
+
114
+ /**
115
+ * The published value of one month. A month with only the Bundesbank's estimate, or with nothing yet,
116
+ * ends in `not_published`, the estimate in the error where there is one.
117
+ */
118
+ public async getMonth(monthArg: string, optionsArg: { signal?: AbortSignal } = {}): Promise<ICpiReading> {
119
+ const series = await this.getSeries({ frequency: 'monthly', from: monthArg, to: monthArg }, optionsArg.signal);
120
+ const value = series.values[0];
121
+ if (!value) {
122
+ const estimate = series.estimates[0];
123
+ const message = estimate
124
+ ? `the index for ${monthArg} is not published yet; the Bundesbank gives only its own estimate (${estimate.value}).`
125
+ : `the index for ${monthArg} is not published yet.`;
126
+ throw new CpiError('not_published', message, {
127
+ service: 'bundesbank',
128
+ httpStatus: series.answer.httpStatus,
129
+ answer: series.answer,
130
+ period: monthArg,
131
+ ...(estimate ? { estimate } : {}),
132
+ });
133
+ }
134
+ return { ...value, source: series.source, answer: series.answer };
135
+ }
136
+
137
+ private async getSeries(rangeArg: IRange, signalArg: AbortSignal | undefined): Promise<ICpiSeries> {
138
+ checkRange(rangeArg);
139
+ const key = rangeArg.frequency === 'monthly' ? bundesbankVpiMonthlyKey : bundesbankVpiAnnualKey;
140
+ const uri = `${this.baseUrl}${key}?startPeriod=${rangeArg.from}${rangeArg.to !== undefined ? `&endPeriod=${rangeArg.to}` : ''}`;
141
+ const answer = await getText(this.options, uri, signalArg);
142
+ const refused = (codeArg: 'series_withdrawn' | 'busy' | 'unavailable' | 'unexpected_answer', messageArg: string): CpiError =>
143
+ new CpiError(codeArg, messageArg, { service: 'bundesbank', httpStatus: answer.httpStatus, answer });
144
+ if (answer.httpStatus === 404) {
145
+ throw refused('series_withdrawn', `the Bundesbank has no series ${key}: it answered 404.`);
146
+ }
147
+ if (answer.httpStatus === 429) {
148
+ throw refused('busy', 'the Bundesbank refused the request as one too many (429).');
149
+ }
150
+ if (answer.httpStatus >= 500 && answer.httpStatus <= 599) {
151
+ throw refused('unavailable', `the Bundesbank cannot answer now (${answer.httpStatus}).`);
152
+ }
153
+ if (answer.httpStatus !== 200) {
154
+ throw refused('unexpected_answer', `the Bundesbank answered ${answer.httpStatus}.`);
155
+ }
156
+ const read = readAnswer(answer.text, rangeArg, (messageArg) => refused('unexpected_answer', messageArg));
157
+ if (!read) {
158
+ throw new CpiError('not_published', `the Bundesbank has no value from ${rangeArg.from}${rangeArg.to !== undefined ? ` to ${rangeArg.to}` : ' on'} yet.`, {
159
+ service: 'bundesbank',
160
+ httpStatus: answer.httpStatus,
161
+ answer,
162
+ period: rangeArg.from,
163
+ });
164
+ }
165
+ return {
166
+ index: 'de-vpi',
167
+ frequency: rangeArg.frequency,
168
+ base: read.base,
169
+ baseYear: read.baseYear,
170
+ values: read.values,
171
+ estimates: read.estimates,
172
+ source: sourceNoteOf(read, uri),
173
+ answer,
174
+ };
175
+ }
176
+ }
177
+
178
+ /** A range of the series' periods, or the reason it is none. */
179
+ const checkRange = (rangeArg: IRange): void => {
180
+ const written = rangeArg.frequency === 'monthly' ? 'YYYY-MM' : 'YYYY';
181
+ for (const period of [rangeArg.from, rangeArg.to]) {
182
+ if (period !== undefined && frequencyOf(period) !== rangeArg.frequency) {
183
+ throw new CpiError('invalid_input', `${JSON.stringify(period)} is no period written ${written}.`);
184
+ }
185
+ }
186
+ if (rangeArg.to !== undefined && rangeArg.to < rangeArg.from) {
187
+ throw new CpiError('invalid_input', `the range ends (${rangeArg.to}) before it begins (${rangeArg.from}).`);
188
+ }
189
+ const first = rangeArg.frequency === 'monthly' ? bundesbankVpiFirstMonth : bundesbankVpiFirstYear;
190
+ if (rangeArg.from < first) {
191
+ throw new CpiError('before_first_period', `${rangeArg.from} lies before ${first}, the first period of the series.`, { period: rangeArg.from });
192
+ }
193
+ };
194
+
195
+ /**
196
+ * The values of an SDMX-CSV answer, or `undefined` where it holds none. An answer is read only where every
197
+ * row is of the series and the frequency asked for, of one key and one base, inside the range asked for,
198
+ * once per period, with a value of the digits the answer states and a status this client knows.
199
+ */
200
+ const readAnswer = (textArg: string, rangeArg: IRange, unreadableArg: (messageArg: string) => CpiError): IReadSeries | undefined => {
201
+ const lines = textArg.replace(/^/u, '').split(/\r?\n/u).filter((lineArg) => lineArg !== '');
202
+ const header = lines[0] === undefined ? undefined : splitLine(lines[0]);
203
+ if (!header || header[0] !== 'DATAFLOW') {
204
+ throw unreadableArg('the answer is no SDMX-CSV of the Bundesbank.');
205
+ }
206
+ const rows = lines.slice(1);
207
+ if (rows.length === 0) {
208
+ return undefined;
209
+ }
210
+ const missing = COLUMNS.filter((columnArg) => !header.includes(columnArg));
211
+ if (missing.length > 0) {
212
+ throw unreadableArg(`the answer has no column ${missing.join(', ')}.`);
213
+ }
214
+ const at = (fieldsArg: string[], columnArg: (typeof COLUMNS)[number] | typeof STATUS_COLUMN): string => {
215
+ const position = header.indexOf(columnArg);
216
+ return position < 0 ? '' : fieldsArg[position] ?? '';
217
+ };
218
+ const values: ICpiValue[] = [];
219
+ const estimates: ICpiValue[] = [];
220
+ const periods = new Set<string>();
221
+ let key: string | undefined;
222
+ let title = '';
223
+ let base = '';
224
+ let baseYear = 0;
225
+ for (const row of rows) {
226
+ const fields = splitLine(row);
227
+ if (!fields || fields.length !== header.length) {
228
+ throw unreadableArg(`a row is not of the answer's columns: ${row}`);
229
+ }
230
+ const rowKey = at(fields, 'BBK_ID');
231
+ const keyMatch = SERIES_KEY.exec(rowKey);
232
+ const unitMatch = UNIT.exec(at(fields, 'BBK_UNIT'));
233
+ const frequencyCode = FREQUENCY_CODES[rangeArg.frequency];
234
+ if (!keyMatch || keyMatch[1] !== frequencyCode || at(fields, 'BBK_STD_FREQ') !== frequencyCode) {
235
+ throw unreadableArg(`${JSON.stringify(rowKey)} is not the ${rangeArg.frequency} series of the overall index for Germany.`);
236
+ }
237
+ if (!unitMatch || unitMatch[1].slice(2) !== keyMatch[2]) {
238
+ throw unreadableArg(`${JSON.stringify(at(fields, 'BBK_UNIT'))} is no base of the series ${rowKey}.`);
239
+ }
240
+ if (key !== undefined && key !== rowKey) {
241
+ throw unreadableArg(`the answer holds more than one series or base: ${key} and ${rowKey}.`);
242
+ }
243
+ key = rowKey;
244
+ title = at(fields, 'BBK_TITLE');
245
+ base = unitMatch[0];
246
+ baseYear = Number(unitMatch[1]);
247
+ const period = at(fields, 'TIME_PERIOD');
248
+ if (frequencyOf(period) !== rangeArg.frequency || period < rangeArg.from || (rangeArg.to !== undefined && period > rangeArg.to)) {
249
+ throw unreadableArg(`${JSON.stringify(period)} is not a period asked for.`);
250
+ }
251
+ if (periods.has(period)) {
252
+ throw unreadableArg(`${period} is answered twice.`);
253
+ }
254
+ periods.add(period);
255
+ const flag = at(fields, STATUS_COLUMN);
256
+ if (!Object.hasOwn(STATUSES, flag)) {
257
+ throw unreadableArg(`${JSON.stringify(flag)} is no status this client knows, for ${period}.`);
258
+ }
259
+ const status = STATUSES[flag];
260
+ const written = at(fields, 'OBS_VALUE');
261
+ if (written === '' || written === 'NaN') {
262
+ // a period without a value has no observation
263
+ continue;
264
+ }
265
+ const decimals = Number(at(fields, 'BBK_DECIMALS'));
266
+ const decimal = parseDecimal(written);
267
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 6 || !decimal || decimal.units <= 0n || decimal.scale > decimals) {
268
+ throw unreadableArg(`${JSON.stringify(written)} is no index value of ${at(fields, 'BBK_DECIMALS')} decimals, for ${period}.`);
269
+ }
270
+ const value = decimal.scale === decimals ? written : `${written}${decimal.scale === 0 ? '.' : ''}${'0'.repeat(decimals - decimal.scale)}`;
271
+ (status === 'final' ? values : estimates).push({ index: 'de-vpi', period, value, base, status });
272
+ }
273
+ if (key === undefined || values.length + estimates.length === 0) {
274
+ return undefined;
275
+ }
276
+ const byPeriod = (leftArg: ICpiValue, rightArg: ICpiValue): number => leftArg.period.localeCompare(rightArg.period);
277
+ return { values: values.sort(byPeriod), estimates: estimates.sort(byPeriod), base, baseYear, key, title };
278
+ };
279
+
280
+ const sourceNoteOf = (readArg: IReadSeries, uriArg: string): ICpiSourceNote => ({
281
+ source: 'bundesbank',
282
+ provider: 'Deutsche Bundesbank',
283
+ dataOrigin: 'Statistisches Bundesamt (Destatis)',
284
+ title: readArg.title,
285
+ key: readArg.key,
286
+ uri: uriArg,
287
+ licence: destatisLicence.name,
288
+ licenceUri: destatisLicence.uri,
289
+ attribution:
290
+ `Quelle: Deutsche Bundesbank, Zeitreihe ${readArg.key}; Daten: Statistisches Bundesamt (Destatis), ` +
291
+ `Verbraucherpreisindex für Deutschland, ${readArg.base}; ${destatisLicence.name} (${destatisLicence.uri}); ${uriArg}`,
292
+ });
@@ -0,0 +1,76 @@
1
+ import type { ICpiServiceAnswer, ICpiValue, TCpiPeriod } from './interfaces.js';
2
+
3
+ /**
4
+ * Why no index value or no result was given:
5
+ * - `invalid_input`: a period, a value, a percentage or an amount that is not one, or a range that ends
6
+ * before it begins;
7
+ * - `before_first_period`: a period before the first one the series holds;
8
+ * - `not_published`: no published value for the period yet: nothing at all, or only an estimate
9
+ * (`estimate`); a calculation given an estimate refuses it the same way;
10
+ * - `base_mismatch`: values of different bases, indices or frequencies, which are never compared;
11
+ * - `series_withdrawn`: the source no longer has the series asked for (HTTP 404), as happens to a
12
+ * series key of an old base after the base year changed;
13
+ * - `busy`: the source refuses for too many requests (HTTP 429);
14
+ * - `unavailable`: the source cannot answer now (HTTP 5xx);
15
+ * - `unreachable`: no answer arrived: the name, the connection or TLS failed;
16
+ * - `timeout`: the answer did not arrive within the client's time;
17
+ * - `aborted`: the caller's signal ended the request;
18
+ * - `unexpected_answer`: an answer this client cannot read: another status, a missing column, a value or
19
+ * a flag it does not know, a period it did not ask for, or more than one series or base.
20
+ */
21
+ export type TCpiErrorCode =
22
+ | 'invalid_input'
23
+ | 'before_first_period'
24
+ | 'not_published'
25
+ | 'base_mismatch'
26
+ | 'series_withdrawn'
27
+ | 'busy'
28
+ | 'unavailable'
29
+ | 'unreachable'
30
+ | 'timeout'
31
+ | 'aborted'
32
+ | 'unexpected_answer';
33
+
34
+ export interface ICpiErrorDetails {
35
+ /** the source asked, where a request was sent */
36
+ service?: 'bundesbank';
37
+ /** the HTTP status of the answer, where one arrived */
38
+ httpStatus?: number;
39
+ /** the answer as it arrived, where one did */
40
+ answer?: ICpiServiceAnswer;
41
+ /** the period the refusal is about */
42
+ period?: TCpiPeriod;
43
+ /** the estimate the source gave for that period, where it gave one (`not_published`) */
44
+ estimate?: ICpiValue;
45
+ }
46
+
47
+ /** The typed error of `@fin.cx/opendata/cpi`. */
48
+ export class CpiError extends Error {
49
+ public readonly code: TCpiErrorCode;
50
+ public readonly service?: ICpiErrorDetails['service'];
51
+ public readonly httpStatus?: number;
52
+ public readonly answer?: ICpiServiceAnswer;
53
+ public readonly period?: TCpiPeriod;
54
+ public readonly estimate?: ICpiValue;
55
+
56
+ constructor(code: TCpiErrorCode, message: string, detailsArg: ICpiErrorDetails = {}, options?: { cause?: unknown }) {
57
+ super(message, options);
58
+ this.name = 'CpiError';
59
+ this.code = code;
60
+ if (detailsArg.service !== undefined) {
61
+ this.service = detailsArg.service;
62
+ }
63
+ if (detailsArg.httpStatus !== undefined) {
64
+ this.httpStatus = detailsArg.httpStatus;
65
+ }
66
+ if (detailsArg.answer !== undefined) {
67
+ this.answer = detailsArg.answer;
68
+ }
69
+ if (detailsArg.period !== undefined) {
70
+ this.period = detailsArg.period;
71
+ }
72
+ if (detailsArg.estimate !== undefined) {
73
+ this.estimate = detailsArg.estimate;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The Deutsche Bundesbank's time series API (SDMX REST), dataflow `BBDP1`, where the Bundesbank
3
+ * republishes the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt ("Quelle:
4
+ * Statistisches Bundesamt, Wiesbaden."). Read on 2026-10-05; it answers without registration.
5
+ */
6
+ export const bundesbankCpiBaseUrl = 'https://api.statistiken.bundesbank.de/rest/data/BBDP1/';
7
+
8
+ /**
9
+ * The series keys of the overall index for Germany, monthly and annual average, unadjusted. The base
10
+ * dimension is left open (`..A`): the answer names the current base (`BBDP1.M.DE.N.VPI.C.A00000.I20.A`,
11
+ * 2020=100), and when the Statistisches Bundesamt changes the base year the Bundesbank withdraws the
12
+ * key of the old one (the `I15` key of 2015=100 answers 404).
13
+ */
14
+ export const bundesbankVpiMonthlyKey = 'M.DE.N.VPI.C.A00000..A';
15
+ export const bundesbankVpiAnnualKey = 'A.DE.N.VPI.C.A00000..A';
16
+
17
+ /** The first month and year of the series (January 1991, the first one the API answers). */
18
+ export const bundesbankVpiFirstMonth = '1991-01';
19
+ export const bundesbankVpiFirstYear = '1991';
20
+
21
+ /** The licence under which the Statistisches Bundesamt publishes its figures. */
22
+ export const destatisLicence = {
23
+ name: 'Datenlizenz Deutschland – Namensnennung – Version 2.0',
24
+ uri: 'https://www.govdata.de/dl-de/by-2-0',
25
+ } as const;
26
+
27
+ /** How long one request may take by default, the answer included. */
28
+ export const defaultCpiTimeoutMs = 30_000;
@@ -0,0 +1,45 @@
1
+ /** A non-negative decimal as integer units of 10^-scale: `'113.5'` is 1135 at scale 1. */
2
+ export interface IScaledDecimal {
3
+ units: bigint;
4
+ scale: number;
5
+ }
6
+
7
+ /** A decimal written with digits and at most one point between them; no sign, no exponent, no comma, no leading zero. */
8
+ const DECIMAL = /^(0|[1-9]\d*)(?:\.(\d+))?$/u;
9
+
10
+ const TEN = 10n;
11
+
12
+ export const powerOfTen = (exponentArg: number): bigint => TEN ** BigInt(exponentArg);
13
+
14
+ export const parseDecimal = (textArg: unknown): IScaledDecimal | undefined => {
15
+ const match = typeof textArg === 'string' ? DECIMAL.exec(textArg) : null;
16
+ if (!match) {
17
+ return undefined;
18
+ }
19
+ const fraction = match[2] ?? '';
20
+ return { units: BigInt(`${match[1]}${fraction}`), scale: fraction.length };
21
+ };
22
+
23
+ /** The units of a decimal at a scale at least its own. */
24
+ export const atScale = (decimalArg: IScaledDecimal, scaleArg: number): bigint =>
25
+ decimalArg.units * powerOfTen(scaleArg - decimalArg.scale);
26
+
27
+ /** numerator / denominator, rounded half away from zero ("kaufmännisch"); the denominator is positive. */
28
+ export const divideRounded = (numeratorArg: bigint, denominatorArg: bigint): bigint => {
29
+ const negative = numeratorArg < 0n;
30
+ const magnitude = negative ? -numeratorArg : numeratorArg;
31
+ let quotient = magnitude / denominatorArg;
32
+ if ((magnitude % denominatorArg) * 2n >= denominatorArg) {
33
+ quotient += 1n;
34
+ }
35
+ return negative ? -quotient : quotient;
36
+ };
37
+
38
+ /** Units of 10^-scale written as a decimal with exactly `scaleArg` digits after the point: `-22` at 1 is `'-2.2'`. */
39
+ export const formatUnits = (unitsArg: bigint, scaleArg: number): string => {
40
+ const negative = unitsArg < 0n;
41
+ const digits = (negative ? -unitsArg : unitsArg).toString().padStart(scaleArg + 1, '0');
42
+ const whole = digits.slice(0, digits.length - scaleArg);
43
+ const fraction = digits.slice(digits.length - scaleArg);
44
+ return `${negative ? '-' : ''}${whole}${scaleArg > 0 ? `.${fraction}` : ''}`;
45
+ };
@@ -0,0 +1,16 @@
1
+ // This entry point stands alone: it imports nothing from the rest of the package and has no
2
+ // dependencies. It keeps nothing: every answer goes to the caller, with the source's answer as it
3
+ // arrived, which the caller keeps as the evidence of an index adjustment.
4
+ export * from './interfaces.js';
5
+ export { CpiError, type ICpiErrorDetails, type TCpiErrorCode } from './classes.cpierror.js';
6
+ export {
7
+ bundesbankCpiBaseUrl,
8
+ bundesbankVpiAnnualKey,
9
+ bundesbankVpiFirstMonth,
10
+ bundesbankVpiFirstYear,
11
+ bundesbankVpiMonthlyKey,
12
+ defaultCpiTimeoutMs,
13
+ destatisLicence,
14
+ } from './constants.js';
15
+ export { cpiChangePercent, cpiChangePoints, cpiFirstReaching, cpiFirstReachingPoints, cpiScaleAmount, cpiThresholds } from './indexmath.js';
16
+ export { BundesbankCpiClient } from './classes.bundesbankcpiclient.js';