> ## Documentation Index
> Fetch the complete documentation index at: https://octo-98895abd.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> silo-client — a thin, honest Python client over the read API: one method per endpoint, a capped response raises, views page, nothing is fabricated.

### Overview

`silo-client` is a small Python client for the Data API. One class, one method per endpoint, PostgREST underneath.

It exists to make the two things that bite hand-written callers impossible to miss: a **capped response raises** instead of returning a short series, and **views page while functions do not**.

It lives in the repository and is not on PyPI, deliberately, while the API's surface is still moving.

### Install

```bash theme={null}
git clone https://github.com/PedroDnT/SILO-BZ && cd SILO-BZ
pip install -e sdk/              # httpx + pandas
```

Python ≥ 3.10. Or copy `sdk/silo_client/` into your project.

### Configure

```python theme={null}
from silo_client import SiloClient

silo = SiloClient()   # public project + publishable key built in
```

Pass `url=` / `key=` or set `SILO_URL` / `SILO_ANON_KEY` to override the built-in project and key. To move to the signed-in tier, pass a user JWT as `token=` (or set `SILO_TOKEN`); `silo.tier` says which ceiling you are under.

### First calls

```python theme={null}
silo.catalog()                    # metric map + constraints + limits — cached, call once
silo.coverage()                   # as_of (landed) vs complete_through (served), per dataset
silo.lookup("petrobras")          # company rows carry tickers=["PETR3","PETR4"]

df = silo.panel(["PETR4", "05754060000113"],
                metrics=["close_return", "delinquency"])   # wide DataFrame, (date) × (id, metric)
df.corr()                         # reductions happen HERE, not over HTTP

silo.panel("PETR4", "close")      # one id, one metric — brackets optional
```

`panel` validates metric names against the live catalog, so a typo raises `ValueError` client-side instead of returning an empty panel. Missing observations stay `NaN` — never filled.

`ids` and `metrics` each accept one bare string or any sequence of them. The single-string case is handled explicitly, not left to Python's sequence semantics: `str` *is* a `Sequence[str]`, so an unguarded `list("PETR4")` would ask for five one-character ids and the server's empty answer would read as "no data for PETR4".

### Every method

One per published endpoint. Dates accept `date` objects or ISO strings; `None` means the server's default, which for fund windows is the honest one.

| Method | Endpoint | Notes |
| - | - | - |
| `catalog()`, `metrics()`, `limits()` | `catalog` | cached once per client |
| `coverage()` | `coverage` | `as_of` is the newest *elapsed* period; `newest_period` can be in the future |
| `metric_coverage()` | `metric_coverage` | which `(family, metric)` pairs are filed, and since when |
| `lookup(query)` | `lookup` | never a name-inferred ticker↔CNPJ |
| `panel(ids, metrics, start, end, freq, wide, entity_type, min_nav, min_months)` | `panel` | raises `SiloOverCap` above one page; `wide=False` for long rows |
| `iter_panel(...)` · `panel_all(..., wide)` | `panel` + `p_after` | `ids=None` + `entity_type=` is universe mode (signed in) |
| `prices(tickers, start, end, fields=None)` | `quote_history` + `p_after`, per ticker | one polars DataFrame from one data revision; `close_adj` by default, `fields=["close"]` for the raw close |
| `quote_history(ticker, start, end, board, fields)` · `quote_latest(ticker, board)` | `quote_history` · `quote_latest` | one page; `close_adj` by default (quote\_latest is raw) |
| `iter_quote_history(...)` · `quote_history_all(...)` | `quote_history` + `p_after` | pages on `trade_date`; never combine two data revisions |
| `research_universe(as_of)` | `research_universe` | one row per ticker+ISIN pair; `as_of=T` keeps `first_observed <= T <= last_observed` |
| `index_history(index, start, end)` · `iter_index_history(...)` · `index_history_all(...)` | `index_history` + `p_after` | an index **code**, never a ticker; IBOV from 1968 is 15 pages; every code is a total-return index, as B3 labels it |
| `trade_consolidated_history(ticker, start, end)` · `iter_trade_consolidated_history(...)` · `trade_consolidated_history_all(...)` | `trade_consolidated_history` + `p_after` | B3's FORWARD segment (fixed-income ETFs such as IMAB11); the close is `last_price`, no open, not adjusted for distributions; a COTAHIST ticker is refused |
| `iter_fund_nav(cnpj, entity_type, start, end)` · `fund_nav_all(...)` | `fund_nav` + `p_after` | **`entity_type` is required**, checked client-side |
| `option_chain(prefix, trade_date, expiry_from, limit)` · `option_history(codneg, …)` · `option_exercises(prefix, …)` | options | prefix ≥ 3 chars, checked client-side |
| `termo_history(codneg, start, end)` | `termo_history` | |
| `fund_profile(cnpj)` · `fund_nav(cnpj, …)` · `search_funds(query, limit)` | funds | |
| `fund_holdings(cnpj \| ticker, start, end, kind, limit)` | `fund_holdings` | exactly one of holder / held ticker |
| `fund_debentures(cnpj \| issuer, start, end, limit)` | `fund_debentures` | issuer = ticker, CVM code or any CPF/CNPJ |
| `fidc_cedentes(cnpj \| cedente, …)` · `fidc_sacados(cnpj, …)` · `fidc_portfolio(cnpj, kind, …)` | [FIDC](/api-docs/fidc) | `kind` = sector / scr\_debtor / scr\_operation / tax\_debt |
| `fidc_tranches(cnpj, start, end, series)` · `fidc_aging(cnpj, start, end)` | [FIDC tranches and aging](/api-docs/fidc-structure) | as filed, from 2025-01; `flows` keeps CVM's labels |
| `fund_documents(cnpj, start, end, tipo)` · `fund_restatements(cnpj, start, end, tipo_fundo)` · `fund_restatement_diff(cnpj, start, end, tipo, fnet_id)` | [Fund documents and restatements](/api-docs/fnet-documents) | fund by FNET link only; versions paired by a stated key; the diff needs `cnpj` or `fnet_id` |
| `screen_restatements(months, end, …)` · `screen_late_filers(months, end, …, family)` · `screen_silent_filers(min_silent_months, max_silent_months, family)` | [Filing-behaviour screens](/api-docs/filing-screens) | signals, not verdicts; `late_filers` cites its rule on every row |
| `financials(id, statement, start, end, scope, doc_type, as_of)` · `company_financials(id, …, as_of)` | financials | read `period_months` before comparing rows; `as_of` makes the read point-in-time (also on `income_statements`, `balance_sheets`, `cash_flow_statements`) |
| `company_events(id, start, end, category)` | [Company events](/api-docs/company-events) | id resolved like `financials`; newest version per protocol; from 2015 |
| `anbima_classes(category, metric, level, start, end)` | `anbima_classes` | industry aggregates, no fund join |
| `inflation(series, family, start, end)` · `inflation_items(level, item, start, end)` | [inflation](/api-docs/inflation) | as published, in percent; `acc_12m` and `contribution` are the derived columns |
| `macro_series(series, start, end)` · `ptax(currency, start, end)` | [Macro series and PTAX](/api-docs/macro) | as published; read `unit` on every row |
| `future_curve(root, trade_date)` · `future_series(ticker, start, end)` · `curve(curve, trade_date)` · `curve_history(curve, tenor_days, start, end)` | [DI futures and B3 reference curves](/api-docs/rates) | as published; DI1 quotes are rates, read `rate_basis` on the curves |
| `short_interest(...)` · `short_interest_by_sector(...)` · `lending_trades(...)` · `lending_participants(...)` · `investor_flow(...)` | [lending](/api-docs/lending) · [flows](/api-docs/flows) | view readers, PostgREST filters passed through |
| `view(name, **filters)` · `view_all(name, **filters)` · `iter_view(...)` | the 13 views | filter syntax passed through verbatim |

### The three rules the client enforces

#### A capped response raises

PostgREST stops every response at 1,000 rows and answers `200` with the oldest page. The client asks for a true count (`Prefer: count=exact`) and raises rather than hand you the short answer:

```python theme={null}
from silo_client import SiloTruncated

try:
    rows = silo.quote_history("PETR4", start="2019-01-02")
except SiloTruncated as e:
    print(e.returned, "of", e.total)      # 1000 of 4382
    print(e.rows[-1]["trade_date"])       # where the cut fell — inspect, never use
```

For the three long series the server refuses instead (`SiloOverCap`), and each pages with its own cursor:

```python theme={null}
from silo_client import SiloOverCap

try:
    df = silo.panel(["PETR4"], ["close"], start="2019-01-01", freq="day")
except SiloOverCap:
    rows = silo.panel_all(["PETR4"], ["close"], start="2019-01-01", freq="day")

quotes = silo.quote_history_all("PETR4", start="2019-01-02")

# nav pages within ONE family — the cursor is a bare period, and 385 CNPJs
# file under two in the same month, so the family is not optional
nav = silo.fund_nav_all("05754060000113", "fi", start="2019-01-01")

# a whole family, signed in: every FIDC with >= 12 filed months and >= R$10mm
fidcs = silo.panel_all(None, ["delinquency", "nav"], entity_type="fidc",
                       min_nav=1e7, min_months=12, wide=True)
```

For `option_history`, `termo_history`, `financials`, `company_financials`, `anbima_classes`, `inflation`, `inflation_items`, `fidc_tranches`, `fidc_aging`, `fund_documents`, `fund_restatements`, `fund_restatement_diff`, `company_events`, `macro_series`, `ptax`, `future_curve`, `future_series`, `curve`, `curve_history` and the `screen_*` functions there is no cursor: `SiloOverCap` means narrow the window.

`panel(wide=True)` refuses to pivot a CNPJ that files under two families in the same month — pass `entity_type=`, or take `wide=False` and keep `asset_class` in your own key.

#### Views page; functions do not

```python theme={null}
page2 = silo.view("funds", order="cnpj.asc", limit=1000, offset=1000)   # one explicit page
fidcs = silo.view_all("funds", entity_type="eq.fidc", order="cnpj.asc",
                      select="cnpj,fund_name,first_period,last_period")  # every page
```

`view_all` / `iter_view` **require an `order`** — offset paging without a total order can duplicate a row at one boundary and drop one at the next. A `view()` with no `limit` that lands on the cap still raises: that is the silent cut, not a page.

#### The client tells you when it is out of date

`catalog()` compares the server's catalog version against the one the client was written for and warns `SiloCatalogDrift` on a mismatch. It never refuses — everything read off the catalog keeps working. `silo.limits()` is the tier table straight from the server.

### Errors

| Exception | When |
| - | - |
| `SiloError` | any 4xx/5xx — carries `.status` and the server's own message |
| `SiloTruncated` | a capped response (views, series functions); `.returned`, `.total`, `.rows` |
| `SiloOverCap` | a window over one page (`22023`); page with the cursor or narrow |
| `SiloTimeout` | SQLSTATE `57014` — the tier's statement budget (3s / 8s) |
| `ValueError` | caught before the round trip: unknown metric, missing `order`, both/neither of an exactly-one pair |

The client never retries a 4xx into a different answer, and connection retries are safe only because every endpoint is read-only.

### Source

[`sdk/silo_client/client.py`](https://github.com/PedroDnT/SILO-BZ/blob/main/sdk/silo_client/client.py) is short enough to read in one sitting, and [`tests/test_sdk_client.py`](https://github.com/PedroDnT/SILO-BZ/blob/main/tests/test_sdk_client.py) is the contract: offline, via `httpx.MockTransport`, no network.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.