🇬🇧 English · 🇪🇸 Español
Latin America Real-Time Connectors, Schema Canaries & .pmz Cartridges
puremacro.fetch.realtime gains four connectors for Latin American central banks and statistical agencies — banxico (Banco de México, SIE), inegi (INEGI, Banco de Indicadores / BIE), bcb (Banco Central do Brasil, SGS) and bcch (Banco Central de Chile, SIETE) — together with three pieces of plumbing they depend on: a schema canary (puremacro.fetch.realtime.canary) that checks each provider's JSON envelope before it is parsed, a snapshot store (the realtime_vintages table of the SQLite cache database, puremacro._cache_db) that turns repeated captures into vintages, and a portable .pmz cartridge (puremacro.fetch.realtime.cartridge, built on puremacro.pocket) that moves a VintagePanel to a machine with no network and no credentials. Credentials are resolved through puremacro.credentials (services banxico, inegi and bcch; bcb is keyless). The connectors register with the provider registry, so the general entry point vintage_panel(...) of real_time_data.md reaches them through providers=.
One fact shapes everything on this page, so it comes first rather than in a footnote: none of these four sources publishes an archive of past editions. puremacro manufactures vintages by snapshotting.
1. Theoretical & Algorithmic Framework
1.1 Vintages by snapshot, not by archive
ALFRED, the Bundesbank real-time database and the OECD STES revisions archive keep every edition a statistical office ever published. Banxico's SIE, INEGI's BIE, the BCB's SGS and the BCCh's SIETE do not: each request returns the current series, overwritten in place, and the payload carries no vintage field. puremacro.fetch.realtime.banxico.MEXICO_VINTAGE_NOTE records that determination for Mexico, and the same holds for Brazil and Chile.
The four connectors therefore build vintages the only way they can. Every successful fetch is parsed into a tidy [date, vintage, value] frame whose vintage column is the snapshot date, the day this machine captured the series — pd.Timestamp.now().normalize(), i.e. local midnight of today, unless you pass vintage_date= — and the rows are written into the realtime_vintages table of the local SQLite cache, keyed by (provider, country, series_id, observation_date, vintage_date). Writing \(y_t^{(v)}\) for the value of reference period \(t\) carried by the snapshot captured on day \(v\), one fetch contributes the set
and the local archive after captures on days \(v_1 < v_2 < \dots < v_K\) is \(\bigcup_k S_{v_k}\). The consequences are immediate and the rest of the page keeps coming back to them:
- A single fetch yields exactly one edition.
coverage()reportsn_vintages == 1and no revision test is possible. - Revision history accumulates only across captures on different days. A re-fetch on the same day hits the same primary key and overwrites it (
INSERT OR REPLACE). - The vintage date is when you captured, not when the institution released.
VINTAGE_SEMANTICS["banxico"]and its three siblings say exactly that, andVintagePanel.vintage_semantics()returns the text for every provider present. - For the historical editions of Mexican quarterly GDP use provider
oecd_stes, which archives INEGI's series with 329 monthly editions from 1999-02 (see real_time_data.md).
1.2 From snapshots to revisions
Given the archive, the objects the rest of puremacro.fetch.realtime works with are the standard real-time data-set constructs of Croushore and Stark (2001). The revision triangle places reference dates on rows and capture dates on columns, \(T[t, v] = y_t^{(v)}\), with NaN wherever period \(t\) was not yet in the payload on day \(v\). The as-of cross-section on day \(v^*\) keeps, for every \(t\), the last capture not later than \(v^*\):
For a series catalogued in levels or as an index, the transform applied within each vintage column before any test is the log-difference in percent, \(x_t^{(v)} = 100\,[\ln y_t^{(v)} - \ln y_{t-1}^{(v)}]\); series catalogued as a rate or a growth rate are used as they stand (UNITS_TRANSFORM). Numerators and denominators are never mixed across vintages. The preliminary estimate of \(t\) is its first capture, the final is its latest, and the revision is \(r_t = x_t^{f} - x_t^{p}\) — provided the first capture can be observed at all. revision_frame drops every reference period that ended before the earliest vintage column (\(t < v_1\)), because the edition captured on \(v_1\) already carries a revised number for it (require_observable_first=True, which VintagePanel.revisions and news_or_noise do not expose; call puremacro.vintages.revision_frame(panel.long(...), require_observable_first=False) for the naive reading). With snapshot vintages this means the revision sample starts at the first capture date, not at the start of the series: everything captured on day one is history that arrived already revised.
The Mankiw and Shapiro (1986) test pair is the two regressions
Under news the preliminary estimate is an efficient forecast and the revision is orthogonal to it, \(\beta_p = 0\); under noise the preliminary estimate is the truth plus measurement error, \(x_t^p = x_t^f + u_t\), so \(r_t = -u_t\) is orthogonal to the final, \(\beta_f = 0\), and negatively related to the preliminary. The reported noise_share is \(\max(0, -\hat\beta_p)\), the share of the preliminary estimate's variance attributable to \(u_t\) under the pure noise model. Standard errors are heteroskedasticity-robust by default (hac_lags=0) and Newey-West HAC with hac_lags="auto" or an integer bandwidth. With snapshot vintages this machinery is only as informative as the calendar of captures behind it: \(K\) captures on \(K\) distinct days give at most \(K\) columns.
1.3 The snapshot store
CREATE TABLE IF NOT EXISTS realtime_vintages (
provider TEXT NOT NULL,
country TEXT NOT NULL,
series_id TEXT NOT NULL,
observation_date TEXT NOT NULL, -- ISO YYYY-MM-DD
vintage_date TEXT NOT NULL, -- ISO YYYY-MM-DD, the snapshot date
value REAL,
PRIMARY KEY (provider, country, series_id, observation_date, vintage_date)
);
CREATE INDEX IF NOT EXISTS realtime_vintages_idx
ON realtime_vintages(provider, country, series_id, vintage_date);
The table lives in the same file as the HTTP cache and the ALFRED store, ~/.cache/puremacro/cache.db or $PUREMACRO_HTTP_CACHE_DIR (CACHE_DB.md), and bootstrap_schema creates it next to connector_events (schema_version component realtime_vintages, version 1). Three helpers in puremacro._cache_db wrap it: store_realtime_vintages (insert-or-replace, returns the number of rows written), query_realtime_vintages (optionally restricted to the editions with vintage_date <= ..., the raw material of the as-of cross-section; VintagePanel.as_of then keeps the latest edition per period) and record_connector_event (one row per telemetry event).
The four connectors differ only in how they build the request and parse the body; everything after that — canary policy, cache write, history read-back, fallback, telemetry — lives in one shared helper (puremacro.fetch.realtime._snapshot.fetch_snapshot_vintages), so every fetch_*_vintages call follows the same decision tree:
- Resolve the credential (Banxico, INEGI, BCCh; BCCh needs both halves). If it does not resolve and
use_cache=True, return the stored snapshots when there are any — no request, no warning, no event — and otherwise raiseMissingCredentialError. - Request the live series with
urllib(User-Agent: puremacro (real-time vintage reader),timeout=60.0by default) and run the parser, which runs the canary under theon_driftpolicy of Section 1.4. - With
use_cache=True, store the parsed rows and log the event('success', 'none'). Then, withhistory=True(the default), return every snapshot stored locally for this series — today's included, one vintage per snapshot date — which is what a revision test needs;history=Falsereturns only the snapshot just fetched. - If the payload parsed to an empty frame and the store holds rows, warn
... received empty observations; falling back to cached vintages.and log('fallback', 'sqlite_cache'). - If anything raised — an HTTP error, a timeout, a
SchemaDriftError— and the store holds rows, warn... failed (<exc>); falling back to cached vintages.(SchemaDriftWarningfor drift,UserWarningotherwise) and log('fallback', 'sqlite_cache'). With a cold store the exception propagates.
use_cache=False opts out of the store entirely — it neither reads nor writes realtime_vintages — so the live snapshot comes back exactly as parsed and any failure is raised rather than papered over. A snapshot that cannot be stored is still returned, and a telemetry write that fails never breaks the fetch that emitted it. For the two credentialed providers that carry secrets in the URL, the failure message and the url / filename attributes urllib hangs on its exceptions are scrubbed before they are shown.
The panel entry points fetch_*_panel catch whatever propagates and record it in metadata["failed"] under "<ISO3>:<variable>"; vintage_panel re-keys that as "<ISO3>:<variable>:<provider>" and never raises for a failed series.
1.4 Schema canaries
A REST service that renames a key or changes a date format keeps answering 200 OK. SchemaCanary catches that class of drift before it becomes a silently empty panel. Each validator is a pure function returning (ok, reason); the parsers call them and apply a policy, so drift is handled like any other fetch failure in the tree above. Payloads may be bytes, str or already-decoded JSON. Observation lists longer than 500 elements are sampled: the first 20 and the last 20 elements are inspected.
| Provider | Root | Envelope | Per observation | Date rule |
|---|---|---|---|---|
banxico |
dict | bmx.series[0].datos (a list, possibly empty) |
keys fecha, dato |
DD/MM/YYYY or YYYY-MM-DD, parsed strictly |
inegi |
dict | Series[0].OBSERVATIONS (a list) |
keys TIME_PERIOD, OBS_VALUE |
YYYY/NN with \(1900 \le YYYY \le 2100\) and \(1 \le NN \le 12\), a YYYYQn-style period, or anything pd.to_datetime accepts |
bcb |
list | the list itself | keys data, valor |
exactly DD/MM/YYYY |
bcch |
dict | Codigo absent or 0; the observation list under Series.Obs — the spelling SIETE emits — with the lowercase obs, under Series or at the root, still accepted |
keys indexDateString, value |
DD-MM-YYYY, or anything pd.to_datetime accepts |
The on_drift policy. Every LatAm parser and fetch path takes on_drift, one of DRIFT_POLICIES = ("raise", "warn", "ignore"), and canary.handle_drift(provider, reason, on_drift) applies it:
"raise"(the default): raiseSchemaDriftError, aValueError. The fetch layer turns that into a cache fallback when the store holds anything and re-raises otherwise."warn": emitSchemaDriftWarning(aUserWarning) and let the parser try anyway — its own date fallbacks, an ISO date whereDD/MM/YYYYwas expected for instance, often still recover the payload."ignore": parse silently.
Whatever the policy, a ('schema_drift', ...) event is recorded in connector_events — with fallback_used 'none', 'warning_emitted' or 'ignored' respectively, and subject to the telemetry kill-switch of Section 7 — so connector_health() sees the drift even when a fallback hid it from the caller. A policy string outside DRIFT_POLICIES raises ValueError on the first call, before the payload is looked at, rather than months later inside a fallback path.
SchemaCanary.check(provider, payload, *, raise_on_drift=False, on_drift=None) and its functional twin validate_payload carry both spellings: on_drift wins, and when it is None the older raise_on_drift flag selects "raise" (True) or "warn" (False). Both return (ok, reason) whenever they return at all, and an unknown provider name passes through as (True, "").
1.5 The .pmz cartridge container
pack_realtime_cartridge and load_realtime_cartridge are thin wrappers over puremacro.pocket, so a real-time cartridge is a pocket cartridge:
- a plain zip (
ZIP_STORED) with two kinds of member:manifest.jsonand oneframes/<name>.npzper frame. A real-time cartridge holdsdata, the tidyVintagePanel.dfwith the eightVINTAGE_COLUMNS, and — when you pack aVintagePanelthat carries metadata — a second framevintage_metadata, a two-column key / JSON-value table thatload_realtime_cartridgeturns back intopanel.metadata, sofailed,missing,freq,provider_usedand friends survive the round trip. Values that are not JSON-native (aDataFrameof dropped editions, aTimestamp) are stored throughstrrather than dropped, so the cartridge still says what was there; - each frame is an npz archive — one NumPy array per column plus a JSON schema recording dtypes, index type and column names (
puremacro.runtime.store). No parquet, no pyarrow and no pickle: object columns holding anything but strings are refused at pack time, andnp.load(..., allow_pickle=False)is used at load time; - the manifest records
format = "puremacro-cartridge",version = 1, aprovenanceblock (createdUTC timestamp,puremacro_version,source,vintage,notes,call,host) and, per frame,name,n_rows,n_cols,columns,index,n_bytesand the SHA-256 of the stored npz payload.
load_realtime_cartridge(path, verify=True) reads the zip through pocket.load, which with verify=True (the default) compares every stored payload's digest with the manifest and raises pocket.CartridgeError on a mismatch. verify=False skips that comparison altogether, so a corrupted frame loads silently: turn it off only on a large cartridge you already trust. It returns a VintagePanel whose metadata carries cartridge_path, provenance_source, provenance_vintage and provenance_notes. The digest depends on the data alone, not on the clock, so pocket.load(path).verify() can re-encode the loaded frames and compare again — which also catches a frame mutated in memory since the load — but load_realtime_cartridge does not call it for you.
Is it safe on a file you did not produce? Against code execution, yes: the format contains no pickle, members are read from memory by name and never extracted to disk, and the manifest is parsed with json. Against tampering, no: the SHA-256 is an unsigned checksum, so anyone can repack a modified frame with a matching digest. The pocket module states it precisely — cartridges are a transport format, not a trust boundary — and a hostile archive can still cost memory to inflate. The relationship to puremacro.pocket is containment: pocket.load(path) opens a real-time cartridge as a generic Cartridge, pocket.inspect_cartridge(path) reads its manifest without decoding any data, and pocket.to_base64 / pocket.from_base64 move it by clipboard. pack_realtime_cartridge only fixes the defaults (source="LatAm Regional Real-Time Ecosystem", vintage= today's UTC date, a standard note) and load_realtime_cartridge only adds the VintagePanel wrapping.
2. Methodological & Provider Options
banxico |
inegi |
bcb |
bcch |
|
|---|---|---|---|---|
| Institution | Banco de México, SIE | INEGI, Banco de Indicadores / BIE | Banco Central do Brasil, SGS | Banco Central de Chile, SIETE |
| Country served | MEX |
MEX |
BRA |
CHL |
| Credential | token | token | none | user + password |
| Where it travels | request header Bmx-Token |
path segment of the request URL | — | query string (user=, pass=), URL-encoded |
| Endpoint constant | BANXICO_SERIES_URL |
INEGI_SERIES_URL |
BCB_SGS_URL |
BCCH_SIETE_URL (built by _build_bcch_url) |
| Value coercion | drops N/E, NaN, null; strips thousands commas |
strips thousands commas | comma is the decimal separator | comma is the decimal separator |
| Vintage semantics | snapshot date | snapshot date | snapshot date | snapshot date |
The catalogue entries BANXICO_SERIES, INEGI_SERIES, BCB_SERIES and BCCH_SERIES in puremacro.fetch.realtime.catalog are registered at import time, so vintage_catalog() lists them and providers_for("MEX", "cpi") returns ['banxico', 'inegi']. The units column decides the default revision transform of Section 1.2 and the freq column is enforced by vintage_panel (below). Entries whose identifier could not be confirmed against the live service carry the marker VERIFY ONLINE in their note; pytest -m network is the audit that clears them.
| Provider | Variable | Series id | Units | Freq | Catalogue note |
|---|---|---|---|---|---|
banxico |
policy_rate |
SF61745 |
rate | D | Tasa objetivo (tasa de interés interbancaria a 1 día), diaria |
banxico |
cpi |
SP1 |
index | M | Índice Nacional de Precios al Consumidor (INPC general), mensual |
banxico |
activity |
SR17631 |
index | M | IGAE índice general, mensual — VERIFY ONLINE |
inegi |
gdp_real |
735848 |
level | Q | PIB trimestral, valores constantes 2018, desestacionalizado — VERIFY ONLINE |
inegi |
cpi |
628197 |
index | M | INPC general, mensual — VERIFY ONLINE |
inegi |
activity |
736184 |
index | M | IGAE índice general, mensual — VERIFY ONLINE |
bcb |
gdp_real |
22099 |
index | Q | PIB trimestral - dados dessazonalizados - índice encadeado (média 1995 = 100) |
bcb |
cpi |
433 |
growth_mom | M | IPCA - variação % mensal |
bcb |
policy_rate |
432 |
rate | D | Taxa de juros - Meta Selic definida pelo Copom (% a.a.), diária |
bcb |
activity |
24363 |
index | M | IBC-Br, dessazonalizado, mensal — VERIFY ONLINE |
bcch |
gdp_real |
F032.PIB.FLU.R.CLP.EP18.Z.Z.0.T |
level | Q | PIB volumen a precios del año anterior encadenado, ref. 2018, trimestral — VERIFY ONLINE |
bcch |
cpi |
F074.IPC.IND.Z.Z.C.M |
index | M | IPC general, índice base 2023 = 100, mensual — VERIFY ONLINE |
bcch |
policy_rate |
F022.TPM.TPO.D001.NO.Z.D |
rate | D | Tasa de Política Monetaria (TPM), diaria |
bcch |
activity |
F032.IMC.IND.Z.Z.EP18.Z.Z.0.M |
index | M | IMACEC Índice Mensual de Actividad Económica, mensual |
Two of these entries are worth reading twice. BCB 433 is the IPCA monthly percentage change, not an index level, so its units are growth_mom and no revision helper will log-difference an already-differenced series. BCB 22099 is the IBGE quarterly seasonally adjusted chained-volume index (22109 is the unadjusted one); 4380, which looks like a GDP series, is "PIB mensal - valores correntes", monthly and nominal. On the Chilean side the SIETE codes end in the Spanish frequency letter (D, M, T, A) — none ends in .Q — and the IPC lives under chapter F074, not F073, which is exchange rates.
policy_rate and activity are the two canonical variables this page adds. policy_rate ("Central bank policy rate / target interest rate") carries the aliases tpm, selic, tasa_objetivo, interest_rate, mp_rate, central_bank_rate, target_rate and overnight_rate; activity ("Monthly economic activity index (IGAE / IMACEC / IBC-Br), SA") carries economic_activity, igae, imacec and ibc_br. So variables=["selic"] and variables=["policy_rate"] are the same request.
Through the registry, the live calls are vintage_panel(["MEX"], providers="banxico", variables=["policy_rate"], freq="D"), vintage_panel(["BRA"], providers="bcb", variables=["gdp_real"], freq="Q") and vintage_panel(["CHL"], providers="bcch", variables=["activity"], freq="M"); Section 4.4 shows what the first one does offline. Things to know about that route:
providers="auto"never reaches these connectors:DEFAULT_PROVIDER_ORDERisoecd_stes, alfred, bundesbank, ons, statcan, ecb_rtd. Name them explicitly, as a string or in a list.freqis enforced, one frequency per call.SUPPORTED_FREQUENCIESis{"Q", "M", "D"}and anything else raisesValueError. A catalogue entry that declares a different frequency is not served: it is reported inmetadata["missing_pairs"]/metadata["failed"]with the reason, e.g.catalogue series SF61745 is daily (freq='D'); requested freq='Q'. Entries that declare no frequency — every quarterly vintage archive, which predates the field — are served at"Q"only. Ask for each series at its own frequency, and pass the matchingperiods_per_year(4, 12, 252…) totriangle()/revisions()/news_or_noise(); keep daily policy rates out of revision tests altogether.- The default
series="gdp_real"resolves to nothing forbanxico(its catalogue has no GDP), sovintage_panel(["MEX"], providers="banxico")reports(MEX, gdp_real)as missing rather than failing. catalog=overrides are consulted for servability only; the four connectors look series up in their own tables. For an identifier that is not catalogued, callfetch_*_vintages(series_id)directly and route the frame throughnormalize_vintage_frame.fetch_*_panelexpects canonical variable names (vintage_panelcanonicalises before dispatch) and silently skips countries and variables outside its table.store=andrefresh=are accepted and ignored.- Combining providers, e.g.
providers=["banxico", "bcb", "bcch"], triggers the usual mixed-provider warning. It is harmless here, since all four share the same snapshot-date semantics, andwarn_on_mixed_providers=Falsesilences it.
3. Credentials & Configuration
Resolution follows puremacro.credentials (CREDENTIALS.md): explicit kwarg, then the environment variables in registry order, then the TOML file ($PUREMACRO_CREDENTIALS_FILE if set, else $XDG_CONFIG_HOME/puremacro/credentials.toml if XDG_CONFIG_HOME is set, else ~/.puremacro/credentials.toml), else nothing. credentials.get_credential and credentials.require_credential are aliases of get and require; credentials.status() shows which services are configured without printing any value.
| Service | Environment variables (in order) | TOML keys | Sign-up |
|---|---|---|---|
banxico |
BANXICO_API_KEY, BMX_TOKEN, PUREMACRO_BANXICO_API_KEY |
[banxico] api_key |
https://www.banxico.org.mx/SieAPIRest/service/v1/token_req.html |
inegi |
INEGI_API_KEY, PUREMACRO_INEGI_API_KEY |
[inegi] api_key |
https://www.inegi.org.mx/app/api/indicadores/desarrolladores/jsonxml/ |
bcch (user) |
BCCH_API_USER, PUREMACRO_BCCH_API_USER |
[bcch] user (or api_key) |
https://si3.bcentral.cl/estadisticas/principal1/registro/index.html |
bcch (password) |
BCCH_API_PASS, PUREMACRO_BCCH_API_PASS |
[bcch] password |
(same registration) |
BCCh is the odd one out: SIETE authenticates with the registered e-mail and a password, and the registry keeps the two halves apart. PASSWORD_ENV_VARS names the services whose credential is a pair — {"bcch": ("BCCH_API_PASS", "PUREMACRO_BCCH_API_PASS")} — and credentials.get("bcch") resolves only the user, scanning BCCH_API_USER, PUREMACRO_BCCH_API_USER and then [bcch].user / [bcch].api_key, while credentials.get_password("bcch") resolves only the password, from the password variables and then [bcch].password. A lone password can therefore no longer be picked up and sent as the user, and the password is readable from the TOML file. credentials.require("bcch") demands both halves and, when either is missing, raises MissingCredentialError whose message reads "needs a user and a password" and lists both variable groups; credentials.status() flags a service whose user is configured but whose password is not. fetch_bcch_vintages(user=..., password=...) overrides either half explicitly.
Both halves travel in the query string of the GET request (user=, pass=, URL-encoded by _build_bcch_url), so treat the request URL as a secret: do not paste it into an issue, and remember that proxies and server logs see it. The connector scrubs the user and the password — raw and URL-encoded — from the warning text and from the url / filename attributes urllib attaches to its exceptions, so a fallback message is safe to paste; the URL you build yourself is not. Banxico's token goes in the Bmx-Token header; INEGI's is a path segment of the URL.
4. Runnable Worked Examples
Every script below runs offline and prints exactly the output shown; the bodies take a fraction of a second, the puremacro import a few seconds. The scripts that touch the database first point it at a temporary directory (PUREMACRO_HTTP_CACHE_DIR) so that they leave ~/.cache/puremacro/cache.db untouched; drop that line to work against your real cache.
4.1 Fixture to panel to cartridge and back
A payload shaped like the BCB SGS response goes through the canary, the parser, normalize_vintage_frame and a VintagePanel, is packed into a .pmz in a temporary directory and comes back verified:
import json, os, tempfile, zipfile
os.environ["PUREMACRO_HTTP_CACHE_DIR"] = tempfile.mkdtemp() # keep the example out of ~/.cache
from puremacro import pocket
from puremacro.fetch.realtime import (
VintagePanel, normalize_vintage_frame, validate_payload,
pack_realtime_cartridge, load_realtime_cartridge,
)
from puremacro.fetch.realtime.bcb import parse_bcb_json
# A fixture shaped exactly like the SGS endpoint's JSON: a list of {data, valor}
raw = json.dumps([{"data": "01/01/2020", "valor": "4,50"},
{"data": "01/04/2020", "valor": "3,75"}])
print(validate_payload("bcb", raw))
df = parse_bcb_json(raw, series_id="432", vintage_date="2024-01-15")
print(df)
long = normalize_vintage_frame(df, country="BRA", variable="policy_rate",
provider="bcb", series_id="432", units="rate")
panel = VintagePanel(df=long, metadata={"provider": "bcb"})
print(panel.coverage()[["country", "variable", "n_obs", "n_vintages", "n_periods"]])
with tempfile.TemporaryDirectory() as tmp:
path = pack_realtime_cartridge(panel, f"{tmp}/bra.pmz", vintage="2024-01-15")
print(zipfile.ZipFile(path).namelist())
manifest = pocket.inspect_cartridge(path)
print(manifest["format"], manifest["version"], manifest["frames"][0]["name"], manifest["frames"][0]["n_rows"])
back = load_realtime_cartridge(path) # verify=True: SHA-256 per frame
print(back.metadata["provenance_source"], "|", back.metadata["provenance_vintage"])
print(back.df.equals(panel.df))
(True, '')
date vintage value
0 2020-01-01 2024-01-15 4.50
1 2020-04-01 2024-01-15 3.75
country variable n_obs n_vintages n_periods
0 BRA policy_rate 2 1 2
['manifest.json', 'frames/data.npz', 'frames/vintage_metadata.npz']
puremacro-cartridge 1 data 2
LatAm Regional Real-Time Ecosystem | 2024-01-15
True
The comma-decimal "4,50" became 4.50, the archive holds manifest.json, frames/data.npz and the frames/vintage_metadata.npz that carries panel.metadata across, and the data frame survives the round trip unchanged (equals is True, dtypes included). Note n_vintages = 1: one snapshot, one edition.
4.2 The canary contract
import os, tempfile, warnings
os.environ["PUREMACRO_HTTP_CACHE_DIR"] = tempfile.mkdtemp()
from puremacro.fetch.realtime import (
SchemaCanary, SchemaDriftError, SchemaDriftWarning, validate_payload,
)
good = {"bmx": {"series": [{"idSerie": "SF61745",
"datos": [{"fecha": "15/01/2026", "dato": "10.75"}]}]}}
drifted = {"bmx": {"series": [{"idSerie": "SF61745",
"datos": [{"fecha": "15/01/2026", "value": "10.75"}]}]}} # 'dato' renamed
print(SchemaCanary.validate_banxico(good))
print(SchemaCanary.validate_banxico(drifted))
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
print(validate_payload("banxico", drifted)) # warns, returns
print(caught[0].category.__name__, "->", caught[0].message)
try:
validate_payload("banxico", drifted, raise_on_drift=True)
except SchemaDriftError as exc:
print("raised", type(exc).__name__)
print(SchemaCanary.validate_bcb([{"data": "2026-01-01", "valor": "12.0"}])) # ISO date, not DD/MM/YYYY
print(SchemaCanary.validate_bcch({"Codigo": 99, "Descripcion": "Servicio no disponible"}))
print(validate_payload("alfred", {"anything": 1})) # unknown provider: passes through
(True, '')
(False, "datos[0] missing 'fecha' or 'dato' key")
(False, "datos[0] missing 'fecha' or 'dato' key")
SchemaDriftWarning -> Schema drift detected for banxico: datos[0] missing 'fecha' or 'dato' key
raised SchemaDriftError
(False, "element[0] date '2026-01-01' does not match DD/MM/YYYY")
(False, 'BCCh API returned error code 99: Servicio no disponible')
(True, '')
The validate_* statics only report; validate_payload is where the policy lives. Here it defaults to on_drift="warn" (no raise_on_drift), so it emits SchemaDriftWarning, records ('schema_drift', 'warning_emitted') and returns; raise_on_drift=True — or on_drift="raise" — raises SchemaDriftError and records ('schema_drift', 'none'), and on_drift="ignore" returns silently but still records ('schema_drift', 'ignored'). The parsers take the same keyword and default to "raise": parse_banxico_json(drifted, on_drift="warn") warns and returns the rows it could salvage, while the default raises.
4.3 Two captures make a vintage
The same BCB series captured on two different days, the second capture revising 2023Q4 and adding 2024Q1. Both go into the snapshot store; the as-of query and the revision triangle then fall out of query_realtime_vintages and VintagePanel:
import os, tempfile
os.environ["PUREMACRO_HTTP_CACHE_DIR"] = tempfile.mkdtemp()
from puremacro._cache_db import store_realtime_vintages, query_realtime_vintages
from puremacro.fetch.realtime import VintagePanel, normalize_vintage_frame
from puremacro.fetch.realtime.bcb import parse_bcb_json
from puremacro.reports import df_to_markdown
# Two captures of the same series on two different days. The second
# edition revises 2023Q4 and adds 2024Q1 -- that is what a vintage is here.
capture_1 = parse_bcb_json(
[{"data": "01/07/2023", "valor": "100.0"}, {"data": "01/10/2023", "valor": "101.0"}],
series_id="22099", vintage_date="2024-03-01")
capture_2 = parse_bcb_json(
[{"data": "01/07/2023", "valor": "100.0"}, {"data": "01/10/2023", "valor": "101.6"},
{"data": "01/01/2024", "valor": "102.1"}],
series_id="22099", vintage_date="2024-06-01")
for cap in (capture_1, capture_2):
n = store_realtime_vintages(cap.assign(provider="bcb", country="BRA", series_id="22099"))
print("rows stored:", n)
cached = query_realtime_vintages("bcb", "BRA", "22099")
print(cached[["date", "vintage", "value"]])
as_of = query_realtime_vintages("bcb", "BRA", "22099", vintage_date="2024-03-31") # editions up to that day
print(as_of[["date", "vintage", "value"]])
long = normalize_vintage_frame(cached, country="BRA", variable="gdp_real",
provider="bcb", series_id="22099", units="index")
panel = VintagePanel(long)
tri = panel.triangle("BRA", "gdp_real")
print(tri)
print(len(panel.revisions("BRA", "gdp_real"))) # every reference period predates the first capture
tri.index, tri.columns = tri.index.strftime("%Y-%m-%d"), tri.columns.strftime("%Y-%m-%d")
print(df_to_markdown(tri)) # or df_to_latex / df_to_typst
print(panel.vintage_semantics()["bcb"])
rows stored: 2
rows stored: 3
date vintage value
0 2023-07-01 2024-03-01 100.0
1 2023-07-01 2024-06-01 100.0
2 2023-10-01 2024-03-01 101.0
3 2023-10-01 2024-06-01 101.6
4 2024-01-01 2024-06-01 102.1
date vintage value
0 2023-07-01 2024-03-01 100.0
1 2023-10-01 2024-03-01 101.0
vintage 2024-03-01 2024-06-01
date
2023-07-01 100.0 100.0
2023-10-01 101.0 101.6
2024-01-01 NaN 102.1
0
| date | 2024-03-01 | 2024-06-01 |
|------------|------------|------------|
| 2023-07-01 | 100 | 100 |
| 2023-10-01 | 101 | 101.6 |
| 2024-01-01 | | 102.1 |
Banco Central do Brasil SGS API time series. Open public endpoint that overwrites in place, so the vintage date is the local SNAPSHOT date: the day this machine fetched the series and stored it in the SQLite realtime_vintages cache. One vintage per stored snapshot; history begins with the first local fetch. Not a publication date.
Five rows in the store, two of them for 2023Q4 (101.0 on 2024-03-01, 101.6 on 2024-06-01), and the as-of query on 2024-03-31 returns only the March capture. The triangle is \(T[t, v]\) of Section 1.2, NaN where 2024Q1 did not yet exist. Yet revisions() has zero rows: all three reference periods ended before the first capture (2024-03-01), so none has an observable first release and the censoring rule of Section 1.2 drops them all. Two captures, two columns, no usable revision pair: the revision sample of a snapshot archive begins with the reference periods that end after capturing started, and news_or_noise() needs many distinct capture days beyond that before it says anything.
4.4 The registry call, offline
vintage_panel against banxico with no token and a cold store, then with a snapshot in the store, then a keyless provider during a simulated outage (the network call is replaced by a URLError, so nothing leaves the machine). The Banxico policy rate is catalogued as daily, so the call asks for freq="D"; freq="Q" would be refused with catalogue series SF61745 is daily (freq='D'); requested freq='Q' before any request is built.
import os, tempfile, warnings, urllib.error
from unittest.mock import patch
os.environ["PUREMACRO_HTTP_CACHE_DIR"] = tempfile.mkdtemp()
os.environ["PUREMACRO_CREDENTIALS_FILE"] = "/nonexistent/credentials.toml"
for var in ("BANXICO_API_KEY", "BMX_TOKEN", "PUREMACRO_BANXICO_API_KEY"):
os.environ.pop(var, None)
import pandas as pd
from puremacro._cache_db import store_realtime_vintages
from puremacro.fetch.realtime import vintage_panel
from puremacro.fetch.realtime.bcb import fetch_bcb_vintages
from puremacro.narrative.sources._telemetry import connector_health
# 1. No token and a cold store: the failure is recorded, not raised
with warnings.catch_warnings():
warnings.simplefilter("ignore")
panel = vintage_panel(["MEX"], providers="banxico", variables=["policy_rate"], freq="D")
print(panel.is_empty(), "|", panel.metadata["failed"]["MEX:policy_rate:banxico"].split(" needs")[0])
# 2. A snapshot captured on an earlier day makes the same call work offline
store_realtime_vintages(pd.DataFrame({
"provider": "banxico", "country": "MEX", "series_id": "SF61745",
"date": ["2026-01-15", "2026-02-15"], "vintage": "2026-03-01", "value": [10.50, 10.25]}))
panel = vintage_panel(["MEX"], providers="banxico", variables=["policy_rate"], freq="D")
print(panel.coverage()[["country", "variable", "provider", "n_obs", "n_vintages"]])
print(panel.metadata["provider_used"], panel.metadata["failed"])
# 3. A keyless provider during a simulated outage: warm cache -> warning + fallback event
store_realtime_vintages(pd.DataFrame({
"provider": "bcb", "country": "BRA", "series_id": "432",
"date": ["2026-01-01"], "vintage": "2026-03-01", "value": [12.25]}))
with patch("urllib.request.urlopen", side_effect=urllib.error.URLError("simulated outage")):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
df = fetch_bcb_vintages("432")
print(len(df), "|", str(caught[0].message).split("; ")[1])
print(connector_health()[["source", "n_total", "n_success", "n_fallback"]])
True | MissingCredentialError: Banco de México SIE API
country variable provider n_obs n_vintages
0 MEX policy_rate banxico 2 1
{'MEX': 'banxico'} {}
1 | falling back to cached vintages.
source n_total n_success n_fallback
0 bcb 1 0 1
Step 1 returns an empty panel and files the MissingCredentialError under metadata["failed"]["MEX:policy_rate:banxico"] (the suppressed warning is vintage_panel's usual "returned nothing for 1 of 1 requested countries"). Step 2 is the same call served from the store: no token, no request, provider_used == {'MEX': 'banxico'} and nothing failed. Step 3 shows the fallback branch of Section 1.3 for the keyless connector — the stored row comes back with the falling back to cached vintages warning and a ('fallback', 'sqlite_cache') event, which connector_health() reports as one fallback for bcb. Neither of the first two steps logged an event; only a live attempt does.
4.5 What the catalogue and the credentials registry say
from puremacro.fetch.realtime import vintage_catalog, providers_for, VINTAGE_SEMANTICS
from puremacro import credentials
cat = vintage_catalog()
latam = cat[cat["provider"].isin(["banxico", "inegi", "bcb", "bcch"])]
print(latam[["provider", "country", "variable", "series_id", "units", "freq"]].to_string(index=False))
print(providers_for("MEX", "cpi"), providers_for("MEX", "tasa_objetivo"))
for svc in ("banxico", "inegi", "bcch"):
print(svc, credentials.SERVICES[svc].env_vars)
print("bcch password vars:", credentials.PASSWORD_ENV_VARS["bcch"])
print(VINTAGE_SEMANTICS["banxico"])
provider country variable series_id units freq
banxico MEX activity SR17631 index M
banxico MEX cpi SP1 index M
banxico MEX policy_rate SF61745 rate D
bcb BRA activity 24363 index M
bcb BRA cpi 433 growth_mom M
bcb BRA gdp_real 22099 index Q
bcb BRA policy_rate 432 rate D
bcch CHL activity F032.IMC.IND.Z.Z.EP18.Z.Z.0.M index M
bcch CHL cpi F074.IPC.IND.Z.Z.C.M index M
bcch CHL gdp_real F032.PIB.FLU.R.CLP.EP18.Z.Z.0.T level Q
bcch CHL policy_rate F022.TPM.TPO.D001.NO.Z.D rate D
inegi MEX activity 736184 index M
inegi MEX cpi 628197 index M
inegi MEX gdp_real 735848 level Q
['banxico', 'inegi'] ['banxico']
banxico ('BANXICO_API_KEY', 'BMX_TOKEN', 'PUREMACRO_BANXICO_API_KEY')
inegi ('INEGI_API_KEY', 'PUREMACRO_INEGI_API_KEY')
bcch ('BCCH_API_USER', 'PUREMACRO_BCCH_API_USER')
bcch password vars: ('BCCH_API_PASS', 'PUREMACRO_BCCH_API_PASS')
Banco de México SIE API time series. Upstream overwrites series in place, so the vintage date is the local SNAPSHOT date: the day this machine fetched the series and stored it in the SQLite realtime_vintages cache. One vintage per stored snapshot; history begins with the first local fetch. Not a publication date.
Notebook 58 (notebooks/58_latin_america_realtime_macro.py, Spanish edition _es.py) walks the same objects end to end — panel, cartridge, coverage, as-of cross-section, triangle, Mankiw-Shapiro. Its panel is synthetic: it is generated with numpy.random.default_rng(42) around linear trends, so it exercises the machinery but its numbers are not Banxico, BCB or BCCh data.
5. Full API Specification
The four connector modules share one shape; the differences are the credential arguments and the endpoint constant.
puremacro.fetch.realtime.banxico
BANXICO_SERIES_URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/{series_id}/datos"
parse_banxico_json(raw: bytes | str | dict, *, series_id: str = "",
vintage_date: str | pd.Timestamp | None = None,
on_drift: str = "raise") -> pd.DataFrame
fetch_banxico_vintages(series_id: str, *, token: str | None = None, vintage_date: str | None = None,
timeout: float = 60.0, use_cache: bool = True, history: bool = True,
on_drift: str = "raise") -> pd.DataFrame
fetch_banxico_panel(countries, variables, *, token: str | None = None, timeout: float = 60.0,
use_cache: bool = True, history: bool = True, on_drift: str = "raise",
**_ignored) -> VintagePanel
MEXICO_VINTAGE_NOTE, SPAIN_VINTAGE_NOTE, BANXICO_SIE_BASE, INEGI_BIE_BASE # module constants
puremacro.fetch.realtime.inegi
INEGI_SERIES_URL = "https://www.inegi.org.mx/app/api/indicadores/desarrolladores/jsonxml/"
"INDICATOR/{series_id}/es/00/false/BIE/2.0/{token}?type=json"
parse_inegi_json(raw: bytes | str | dict, *, series_id: str = "",
vintage_date: str | pd.Timestamp | None = None, freq: str | None = None,
on_drift: str = "raise") -> pd.DataFrame
fetch_inegi_vintages(series_id: str, *, token: str | None = None, vintage_date: str | None = None,
timeout: float = 60.0, use_cache: bool = True, history: bool = True,
on_drift: str = "raise") -> pd.DataFrame
fetch_inegi_panel(countries, variables, *, token: str | None = None, timeout: float = 60.0,
use_cache: bool = True, history: bool = True, on_drift: str = "raise",
**_ignored) -> VintagePanel
puremacro.fetch.realtime.bcb
BCB_SGS_URL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.{series_id}/dados?formato=json"
parse_bcb_json(raw: bytes | str | list | dict, *, series_id: str = "",
vintage_date: str | pd.Timestamp | None = None,
on_drift: str = "raise") -> pd.DataFrame
fetch_bcb_vintages(series_id: str, *, vintage_date: str | None = None, timeout: float = 60.0,
use_cache: bool = True, history: bool = True,
on_drift: str = "raise") -> pd.DataFrame
fetch_bcb_panel(countries, variables, *, timeout: float = 60.0, use_cache: bool = True,
history: bool = True, on_drift: str = "raise", **_ignored) -> VintagePanel
puremacro.fetch.realtime.bcch
BCCH_SIETE_ENDPOINT = "https://si3.bcentral.cl/SieteRestWS/SieteRestWS.ashx"
BCCH_SIETE_URL = BCCH_SIETE_ENDPOINT + "?user={user}&pass={password}&function=GetSeries"
"×eries={series_id}&firstdate={firstdate}&lastdate={lastdate}"
parse_bcch_json(raw: bytes | str | dict, *, series_id: str = "",
vintage_date: str | pd.Timestamp | None = None,
on_drift: str = "raise") -> pd.DataFrame
fetch_bcch_vintages(series_id: str, *, user: str | None = None, password: str | None = None,
firstdate: str | None = None, lastdate: str | None = None,
vintage_date: str | None = None, timeout: float = 60.0,
use_cache: bool = True, history: bool = True,
on_drift: str = "raise") -> pd.DataFrame
fetch_bcch_panel(countries, variables, *, user: str | None = None, password: str | None = None,
timeout: float = 60.0, use_cache: bool = True, history: bool = True,
on_drift: str = "raise", **_ignored) -> VintagePanel
| Parameter | Meaning |
|---|---|
raw |
The HTTP body (bytes), its text, or the decoded JSON. Empty input returns an empty [date, vintage, value] frame; a body that is not valid JSON raises json.JSONDecodeError, which the fetch layer treats as a failed fetch rather than as an empty series; a payload that fails the canary follows on_drift. |
series_id |
The provider's identifier. Not written into the output frame. |
vintage_date |
The snapshot date to stamp on this fetch. Default: today at local midnight. |
freq (INEGI) |
A hint used only when the payload is too short for the structural rule below to decide. |
firstdate, lastdate (BCCh) |
Optional yyyy-mm-dd window; empty means the whole series. |
token, user, password |
Explicit credentials; when omitted they are resolved as in Section 3. |
timeout |
Seconds passed to urllib.request.urlopen. |
use_cache |
True stores the parsed rows in realtime_vintages and falls back to the stored snapshots on failure. False neither reads nor writes the store: the live snapshot is returned as parsed and any failure is raised. |
history |
True (default) returns every snapshot stored locally for the series, today's included, one vintage per snapshot date; False returns only the snapshot just fetched. |
on_drift |
"raise" (default), "warn" or "ignore"; the canary policy of Section 1.4. Any other value raises ValueError. |
countries, variables |
ISO3 codes and canonical variable names. Entries outside the provider's own country or table are skipped silently. |
INEGI period parsing. TIME_PERIOD arrives as YYYY/NN, which is a month for a monthly indicator and a quarter for a quarterly one, and nothing in the payload says which reliably. parse_inegi_json decides structurally, with no hard-coded series id: a period number above 4 rules out quarters; period numbers that never exceed 4 across two or more years, or across five or more observations, rule out months, because a monthly series is contiguous and five consecutive months always reach May. Only when the payload is too short to tell does the caller's freq decide, then the FREQ field (Spanish words such as "trimestral" / "mensual", or the numeric CL_FREQ codes); failing all of those the series is treated as monthly. Quarterly periods are dated to the first day of the quarter, monthly ones to the first day of the month.
puremacro.fetch.realtime.catalog
BANXICO_SERIES, INEGI_SERIES, BCB_SERIES, BCCH_SERIES : dict[str, SeriesSpec]
SeriesSpec(series_id: str, units: str = "level", source: str = "", note: str = "", freq: str = "")
SeriesSpec.default_transform() -> str
VERIFY_ONLINE = "VERIFY ONLINE"
CANONICAL_VARIABLES["policy_rate"], CANONICAL_VARIABLES["activity"]
VARIABLE_ALIASES: tpm, selic, tasa_objetivo, interest_rate, mp_rate, central_bank_rate,
target_rate, overnight_rate -> "policy_rate"; economic_activity, igae, imacec,
ibc_br -> "activity"
resolve_spec(provider, country, variable, *, catalog=None) -> SeriesSpec | None
resolve_series(provider, country, variable, *, catalog=None) -> str | None
providers_for(country, variable="gdp_real") -> list[str]
vintage_catalog(provider=None) -> pd.DataFrame # + a freq column
puremacro.fetch.realtime.canary
DRIFT_POLICIES = ("raise", "warn", "ignore")
class SchemaDriftError(ValueError)
class SchemaDriftWarning(UserWarning)
handle_drift(provider: str, reason: str, on_drift: str = "raise", *, stacklevel: int = 2) -> None
class SchemaCanary:
validate_banxico(payload: Any) -> tuple[bool, str] # staticmethod
validate_inegi(payload: Any) -> tuple[bool, str] # staticmethod
validate_bcb(payload: Any) -> tuple[bool, str] # staticmethod
validate_bcch(payload: Any) -> tuple[bool, str] # staticmethod
check(provider: str, payload: Any, *, raise_on_drift: bool = False,
on_drift: str | None = None) -> tuple[bool, str] # classmethod
validate_payload(provider: str, payload: Any, *, raise_on_drift: bool = False,
on_drift: str | None = None) -> tuple[bool, str]
puremacro.fetch.realtime.cartridge
pack_realtime_cartridge(panel_or_df: VintagePanel | pd.DataFrame, path: str | Path, *,
source: str = "LatAm Regional Real-Time Ecosystem",
vintage: str | None = None,
notes: str = "Latin America central bank real-time macroeconomic vintage cartridge") -> Path
load_realtime_cartridge(path: str | Path, *, verify: bool = True) -> VintagePanel
puremacro._cache_db
store_realtime_vintages(df, *, conn: sqlite3.Connection | None = None) -> int
query_realtime_vintages(provider: str, country: str, series_id: str, *,
vintage_date: str | None = None,
conn: sqlite3.Connection | None = None) -> pd.DataFrame
record_connector_event(source: str, outcome: str, fallback_used: str, *,
conn: sqlite3.Connection | None = None) -> None
puremacro.credentials
SERVICES["banxico" | "inegi" | "bcch"] : ServiceCredentialSpec(name, env_vars, signup_url, description)
PASSWORD_ENV_VARS = {"bcch": ("BCCH_API_PASS", "PUREMACRO_BCCH_API_PASS")}
get(service: str, *, explicit: str | None = None) -> str | None # alias: get_credential
get_password(service: str, *, explicit: str | None = None) -> str | None
require(service: str, *, explicit: str | None = None) -> str # alias: require_credential
status() -> pd.DataFrame
class MissingCredentialError(RuntimeError)
puremacro.fetch.realtime
SUPPORTED_FREQUENCIES = {"Q", "M", "D"}
vintage_panel(countries=None, *, series="gdp_real", variables=None, freq="Q",
providers="oecd_stes", catalog=None, store=None, refresh=False, use_cache=True,
timeout=None, warn_on_mixed_providers=True, drop_unadjusted=True) -> VintagePanel
store_realtime_vintagesacceptsdateorobservation_dateandvintageorvintage_dateas column names, requiresprovider,country,series_idandvalue, upper-cases the country, storesNaNvalues asNULL, skips rows whose dates do not parse, and returns the number of rows written (0for an empty frame).query_realtime_vintagesreturns[date, vintage, value, provider, country, series_id]withdateandvintageas datetimes, ordered by observation then vintage;vintage_date=keeps editions withvintage_date <= vintage_date.record_connector_eventwrites(ts, source, outcome, fallback_used)withtsthe current Unix time; the values are free strings here, unlike the validated vocabulary ofnarrative.sources._telemetry.log_event.pack_realtime_cartridgepackspanel.df(or the frame you pass) as the framedata, plusvintage_metadatawhen aVintagePanelwith metadata is handed to it;pathis used verbatim, so the.pmzsuffix is a convention.getresolves the token, or the user half of a two-part service;get_passwordresolves the password half and returnsNonefor every service outsidePASSWORD_ENV_VARS.requiredemands both halves of a pair before it returns the user.
6. Result Interface & Manuscript Export
Parser and fetch frames. parse_*_json and fetch_*_vintages return a pd.DataFrame with columns date (datetime), vintage (datetime) and value (float), sorted and deduplicated on (date, vintage); the cache fallback returns the same three columns from query_realtime_vintages.
VintagePanel (puremacro.fetch.realtime._base), a dataclass with two fields:
df: the tidy frame withVINTAGE_COLUMNS = [country, variable, date, vintage, value, provider, series_id, units], one row per(country, variable, date, vintage).metadata: a dict. Fromfetch_*_panel:provider,failed. Fromvintage_panel:requested_countries,variables,providers_tried,provider_used,missing,missing_pairs,failed,freq(andunadjusted_droppedwhen the seasonal screen removed editions). Fromload_realtime_cartridge:cartridge_path,provenance_source,provenance_vintage,provenance_notes.
Its methods are the operations of Section 1.2: countries / variables / providers (properties), len(panel), is_empty(), vintage_semantics(), coverage() (n_obs, n_vintages, n_periods, first and last date and vintage per series), as_of(vintage_date) (a (country, date)-indexed wide frame), long(country, variable), triangle(country, variable, *, transform="level", periods_per_year=4), revisions(country, variable, *, transform=None, periods_per_year=4, release=0) (transform=None picks the transform the catalogued units imply), news_or_noise(...) returning a MankiwShapiroResult, news_or_noise_panel(...) returning one row per (country, variable), filter(...) and VintagePanel.concat(...).
MankiwShapiroResult (puremacro.vintages), a dataclass with n_obs, transform, hac_lags, significance, mean_revision, se_mean_revision, t_mean_revision, p_mean_revision, std_revision, alpha_on_preliminary, beta_on_preliminary, se_beta_on_preliminary, t_beta_on_preliminary, p_beta_on_preliminary, alpha_on_final, beta_on_final, se_beta_on_final, t_beta_on_final, p_beta_on_final, noise_share, rejects_news, rejects_noise and verdict.
Canary and cartridge results. The validators return (ok, reason) tuples; load_realtime_cartridge returns a VintagePanel, and pocket.load(path) returns the underlying Cartridge with frames, provenance (created, puremacro_version, source, vintage, notes, call, host), records, verify() and summary().
Manuscript export. None of these objects carries summary(), plot(), to_frame(), to_markdown(), to_latex() or to_typst() methods of its own; their tables — coverage(), as_of(), triangle(), revisions(), news_or_noise_panel(), vintage_catalog(), credentials.status(), connector_health() — are plain DataFrames, and puremacro.reports.df_to_markdown, df_to_latex and df_to_typst render them, as Section 4.3 does for the triangle. Cartridge.summary() is the one text summary in this stack.
7. Caveats & Limitations
- Snapshot semantics. One fetch is one edition; revisions exist only across captures on distinct days; a same-day re-fetch overwrites; reference periods that ended before the first capture have no observable first release and are dropped from
revisions()(Section 1.2); the vintage stamp is local midnight whilepack_realtime_cartridge's defaultvintageis the UTC date. For archived history of Mexican quarterly GDP useoecd_stes. - No shared HTTP layer. The four connectors call
urllibdirectly and bypasspuremacro._httpandfetch_with_backoff: no HTTP body cache, no SSL fallback, no retry on HTTP 429. A rate limit surfaces as anHTTPError, hence as a store fallback or an exception. use_cache=Falseis a full opt-out. It neither reads nor writesrealtime_vintages, so there is no fallback at all: the live snapshot is returned as parsed and every failure propagates. That is the flag to use when you want to know the request failed; leave itTruewhen you want the panel to survive an outage.- Telemetry. Events use outcomes
success,fallbackandschema_drift, withfallback_usedvaluesnone,sqlite_cache,warning_emittedandignored. No event is written for the no-credential store path or for a cold-store raise. The connectors write through_cache_db.record_connector_event, which honors thePUREMACRO_NARRATIVE_TELEMETRY=0kill-switch of CONNECTOR_HEALTH.md and becomes a no-op when it is set, so with telemetry off no connector event reachesconnector_eventsat all.connector_health()counts every row withfallback_used != 'live'as a fallback, so a successful real-time fetch ('none') is counted in bothn_successandn_fallback; queryconnector_eventsdirectly for these sources. - One frequency per
vintage_panelcall.freqis now enforced against the catalogue'sfreqfield, not against the data: a series that declares another frequency is refused with a reason rather than served at the wrong label, and an entry that declares none is served at"Q"only. Nothing checks that the payload really is at the declared frequency, so a provider that changes a series' frequency upstream will still be labelled by the catalogue. Pass the matchingperiods_per_yearto the revision helpers, and keep the daily policy rates out of revision tests. - BCCh credentials travel in the query string of a plain GET. They are scrubbed from the connector's own warnings and exception attributes, but a URL you build yourself with
BCCH_SIETE_URL.format(...)carries them in clear text. - Catalogue. Entries marked
VERIFY ONLINEin their note were written without a live check of the identifier;pytest -m networkis the audit that clears them.catalog=overrides do not redirect these connectors (Section 2). - Cartridges are a transport format: the SHA-256 is unsigned,
verify=Falseskips the on-read digest comparison entirely (only the defaultverify=Truechecks it), and the archive can only hold what the npz codec accepts (no arbitrary Python objects). As thepocketmodule notes, the fetchers need sockets, so in a Pyodide session these connectors can only serve the cache or a cartridge — the cartridge is the intended way in (CACHE_DB.md for the IDBFS persistence caveat). - Notebook 58 uses a synthetic panel (Section 4.5).
References
- Croushore, D., & Stark, T. (2001). "A Real-Time Data Set for Macroeconomists." Journal of Econometrics, 105(1), 111–130.
- Mankiw, N. G., & Shapiro, M. D. (1986). "News or Noise: An Analysis of GNP Revisions." Survey of Current Business, 66(5), 20–25.
- Banco de México. SIE API REST, https://www.banxico.org.mx/SieAPIRest/service/v1/ (token request: https://www.banxico.org.mx/SieAPIRest/service/v1/token_req.html).
- INEGI. API de Indicadores (Banco de Indicadores / BIE), https://www.inegi.org.mx/app/api/indicadores/desarrolladores/jsonxml/.
- Banco Central do Brasil. Sistema Gerenciador de Séries Temporais (SGS), endpoint https://api.bcb.gov.br/dados/serie/bcdata.sgs.{series_id}/dados?formato=json.
- Banco Central de Chile. Base de Datos Estadísticos, SIETE REST, https://si3.bcentral.cl/SieteRestWS/SieteRestWS.ashx (registration: https://si3.bcentral.cl/estadisticas/principal1/registro/index.html).