Usage¶
Examples for installing and using this library.
See also: API Reference
Installation¶
Or with Poetry:
Until the first PyPI release is published, install from source:
pip install git+https://github.com/guilhermegor/filings-rfb.git.
Basic usage¶
Every dataset reader (read_companies, read_establishments, read_partners,
read_simples) takes a reference month (YYYY-MM) and yields typed chunks. read_lookup is the
exception: it takes str_table, path_zip, str_url, str_etag and returns a single DataFrame.
read_establishments (up to 2.3 GB per compressed part) and read_simples (~3.2 GB inflated)
are the heaviest; keep path_workdir= to resume instead of re-downloading.
Cataloguing the RFB CNPJ share¶
The Receita Federal publishes the CNPJ open data as one folder per reference month
(YYYY-MM) on a public WebDAV share. filings_rfb.ingestion.cnpj._share lists what is
there, without authentication, so a reader never builds a URL by hand:
from filings_rfb.ingestion.cnpj._share import (
file_url,
latest_reference_month,
list_files,
list_reference_months,
)
list_reference_months() # ['2023-05', ..., '2026-10']
str_month = latest_reference_month()
for cls_file in list_files(str_month):
print(cls_file.str_name, cls_file.int_size, cls_file.str_etag, cls_file.dt_last_modified)
file_url(str_month, "Empresas0.zip") # plain-GET download URL, no redirect
list_filesreturns oneWebdavEntryper file:str_name,int_size(bytes),str_etag(as the server sends it, quotes included) anddt_last_modified(UTC).- Requesting a month the share does not publish raises
ValueError; there is no silent fallback to another month. - Every listing is a live request, so it reflects what RFB publishes now. If RFB rotates the
share, change
SHARE_TOKENin_share.pyand nothing else.
Reading Empresas¶
read_companies(str_month) yields one typed pandas.DataFrame per chunk of each of the ten
Empresas0.zip .. Empresas9.zip parts, so a month that inflates to ~3 GB is never held in
memory at once. Each ZIP is streamed to disk with the resumable downloader and its single member
is read as a stream, never extracted.
from filings_rfb import read_companies
for df_chunk in read_companies("2026-10", int_chunk_rows=500_000):
print(df_chunk[["cnpj_basico", "razao_social", "capital_social"]].head(1))
capital_socialis parsed from decimal-comma text (120000000000,00) to an exactDecimal, never through a float, with no rounding.- Every row carries the provenance columns plus
source_etag(the part'sETag) andpart_index(0-9); all chunks of one call share oneingestion_run_id. - A part the share does not list raises
FileNotFoundErrorbefore any download starts; it is never skipped. - Pass
path_workdir=to keep the downloaded ZIPs (a rerun resumes from them); by default each ZIP is deleted once consumed.
Exporting to Parquet¶
export_companies_parquet(str_month, path_dest) drains the same iterator into one Parquet file
(capital_social stored as decimal128(38, 2)). It needs the optional extra, which pulls in
pyarrow:
Without it the call raises an ImportError that names the extra. Querying the Parquet lazily
(DuckDB, Polars scan_parquet) is up to you; see Examples.
⚠️ Do not read these files with Polars directly: it silently misparses the .zip and corrupts
latin-1 text.
Running tests¶
poe unit_tests # unit tests only
poe integration_tests # integration tests only
poe test_cov # unit tests + coverage report + badge
Linting and formatting¶
Downloading multi-GB archives¶
The RFB CNPJ archives reach 2.3 GB each (Estabelecimentos0.zip), so they must never be held
in memory. download_file_streaming writes the body to disk in fixed-size chunks and can
resume an interrupted transfer:
from pathlib import Path
from filings_rfb._internal.utils.stream_downloader import download_file_streaming
path_zip, str_etag = download_file_streaming(
"https://example.org/Estabelecimentos0.zip",
Path("data/raw/Estabelecimentos0.zip"),
)
- The body is streamed into
<dest>.part; only after the byte count matchesContent-Lengthis it renamed onto the destination, so the final path never holds a partial file. - The server
ETagis stored next to the partial file (<dest>.part.etag). A retry, or a later call after a crash, continues withRange: bytes=<n>-andIf-Range: <etag>. If theETagchanged, or the leftover has no recordedETag, the download restarts from zero. - After the rename the
ETagis recorded in<dest>.etag. Calling again for a destination that has that sidecar sendsIf-None-Match: a304returns the existing file untouched with the storedETag; a200downloads and replaces it. With no sidecar the file is downloaded afresh. This is how a rerun over a keptpath_workdiravoids re-downloading every part. - The returned
str_etag(empty when the server sends none) is for provenance. - A body shorter than
Content-LengthraisesOSErrorafter the bounded retries, keeping the partial file for the next attempt; a response withoutContent-LengthraisesValueError. - It shares the guards of
http_downloader(http/https only, public hosts only, redirects refused, bounded retries).download_filethere still buffers the whole body: use it only for small files.
Publishing to PyPI¶
Two GitHub Actions workflows handle releases (present when the repo has a GitHub remote):
release-test-pypi.yaml— publish to Test PyPI first.release-pypi.yaml— publish to PyPI and cut a GitHub release.
Trigger either from the Actions tab (workflow_dispatch) with the version to release.
Both gate on the new version being greater than the latest already published, build with
python -m build, and publish through OIDC trusted publishing. No token secrets are needed.
Register a pending trusted publisher once on each index:
- pypi.org: workflow
release-pypi.yaml, environmentrelease-pypi. - test.pypi.org: workflow
release-test-pypi.yaml, environmentrelease-test-pypi.
GitHub creates the environments on the first run.
The docs site (https://guilhermegor.github.io/filings-rfb/) returns 404 until the first
release-pypi.yaml run's deploy_docs job (mike) creates the gh-pages branch. After that,
run poe enable_pages once.