Skip to main content

tabular-py

Python reader for dclimate-tabular/1 — content-addressed entity/telemetry data on IPFS.

The Python counterpart to tabular-js, reading the same format from the same CIDs. Built on py-hamt, which supplies the HAMT and the content-addressed store the same way @dclimate/ipld-index does for JS.

tabular-js  ->  @dclimate/ipld-index      (HAMT, CAS, range reads)
tabular-py  ->  py-hamt                   (same, already existed)

Status

Reader only. Publishing, compaction, and rollup stay in tabular-js — the ETL that writes these datasets is JS, and a second writer would be a second thing to keep byte-identical for no current gain. Everything needed to read a dataset published by tabular-js is here.

dclimate-tabular/1 only. Roots written under /0 are refused with the remedy rather than misparsed; there is no dual-read shim. /1 replaced the station model with the entity model: an entity is whatever a dataset is keyed by, and need not be a place, so a dataset of derivative contracts is now expressible. See the changelog.

Install

uv pip install dclimate-tabular-py

Usage

import asyncio
from tabular_py import GatewayRangeSource, EntityDataset

async def main():
    source = GatewayRangeSource("https://ipfs-gateway.dclimate.net")
    ds = await EntityDataset.open(source, root_cid)

    # nearest station to a point, then a year of readings
    near = await ds.nearest(34.05, -118.24, max_km=100)
    rows = await near.time_range("2024-01-01", "2024-12-31").elements("PRCP").rows()
    for row in rows[:5]:
        print(row.entity_id, row.ts, row.values)

asyncio.run(main())

The chainable selection API mirrors tabular-js:

ds.select("USW00023174")           # explicit entity ids
ds.circle(34.05, -118.24, 50)      # within 50 km
ds.rectangle(33.0, -119.0, 35.0, -117.0)
ds.polygon([[(lon, lat), ...]])
await ds.nearest(lat, lon)         # async: reads the geo index
ds.time_range(start, end)
ds.elements("PRCP", "TMAX")
ds.where(gt("TMAX", 300))          # pushed down to fragment statistics

Selections are immutable — each call returns a new EntityDataset, so a base dataset can be reused across queries.

Terminal operations:

Call Returns
await ds.rows() list[ResultRow]
await ds.to_records() list[dict]entity_id, time, values
await ds.to_records("TMAX") list[dict]entity_id, time, value
await ds.to_arrow() pyarrow.Table
await ds.plan() QueryPlan — what would be fetched, without fetching
await ds.list_entities() list[EntityInfo]
ds.columns() list[EntityColumn] — the dataset's vocabulary, with units
await ds.columns_for(id) list[EntityColumn] — what one entity reports
await ds.gaps_for(id) list[DataGap] — windows known to be unknown

How reads stay small

A query never scans the dataset. Three things prune before any Parquet byte is fetched:

  1. The entity index (a HAMT keyed by entity id) resolves named entities directly.
  2. The geo projection answers region queries by reading one or two shard blocks instead of walking every entity.
  3. Fragment statistics in the manifest — per-column min/max and null counts — let a predicate skip whole fragments unread.

What survives is fetched with HTTP range requests against the exact column-chunk byte ranges the manifest records, so a query for one column of one year moves kilobytes.

One deviation from tabular-js, and why

tabular-js synthesizes Parquet FileMetaData client-side from the manifest and reads a fragment with zero footer fetches. PyArrow exposes no public FileMetaData constructor, so that trick does not transfer.

Instead this reader fetches the footer by its manifest-recorded footer_offset/footer_length in a single ranged GET, verifies it against the manifest's footer_digest, and hands the parsed metadata to PyArrow. Cost is one extra range request per fragment — and it buys a corruption check tabular-js does not perform. Footers are cached per fragment CID, so a repeated query pays it once.

Development

uv sync
uv run pytest                     # unit tests, no network
uv run pytest -m network          # conformance against the live gateway
uv run ruff check . && uv run mypy tabular_py

How this is tested against tabular-js

tests/test_golden_cids.py is the cross-language contract. It builds the same small structures tabular-js builds in its own test/wire.test.ts, and asserts they encode to the same CIDs. Because a CID is a hash of the encoded bytes, agreement means the two libraries produce byte-identical blocks for identical inputs — checkable offline, against no published dataset at all.

If a vector mismatches, fix the encoder rather than the vector. When an encoding changes deliberately, change it in tabular-js first and copy the new CID across, so the two are never quietly updated to match each other.

The rest of the suite runs against a synthetic /1 dataset assembled in tests/helpers/build.py — both index variants, an entity with no position, a pre-epoch timestamp, declared gaps, and real ZSTD Parquet fragments read through byte ranges. It is a test helper, not a writer: it composes the public *_to_wire encoders and does no fragment planning, bucketing, compaction, or rollup.

tests/test_conformance.py runs against the four published /1 datasets — GHCNd (132,437 entities, 1.15 B rows), SCAN, SNOTEL, and NDBC. It covers the one thing the offline suite cannot: that both libraries agree end to end on data neither of them wrote. Each root is fetched, decoded, and re-encoded back to its own CID, which is the strongest available statement that nothing was dropped, reordered, or silently defaulted.

When an ETL republishes, the roots move — update the CIDs and re-derive the asserted figures rather than loosening the assertions.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dclimate_tabular_py-0.3.0.tar.gz (159.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dclimate_tabular_py-0.3.0-py3-none-any.whl (59.4 kB view details)

Uploaded Python 3

File details

Details for the file dclimate_tabular_py-0.3.0.tar.gz.

File metadata

  • Download URL: dclimate_tabular_py-0.3.0.tar.gz
  • Upload date:
  • Size: 159.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.8

File hashes

Hashes for dclimate_tabular_py-0.3.0.tar.gz
Algorithm Hash digest
SHA256 2157cb55c3aa0f6986c8a9d3763e06f5cd68af83bb32ad2770ec547379b3cf9f
MD5 2efb2445fe46e2668253d3d0aad789b8
BLAKE2b-256 f4ca9b0ee8aa5d9497982483946f4e3f9ffb19a64c8ec958036c1b8275ef1f3b

See more details on using hashes here.

File details

Details for the file dclimate_tabular_py-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for dclimate_tabular_py-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ef678bffff6a1ed7c5025721f4eacb6340d5110d6c99f358d24b261f433cebbe
MD5 2eda79dfa8943678738844d6d3200b51
BLAKE2b-256 c6e6de269e68cad00422a170883383fcac5276266a15119fa934d3a202d4bba0

See more details on using hashes here.

Release history Release notifications | RSS feed

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page