dclimate-tabular-py 0.1.0__tar.gz

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,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.5
2
+ Name: dclimate-tabular-py
3
+ Version: 0.1.0
4
+ Summary: Content-addressed tabular/telemetry manifest for climate data on IPFS (dclimate-tabular/0)
5
+ Project-URL: Homepage, https://github.com/dClimate/tabular-py
6
+ Project-URL: Repository, https://github.com/dClimate/tabular-py
7
+ License: MIT
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: blake3>=0.4.1
10
+ Requires-Dist: dag-cbor>=0.3.3
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: multiformats[full]>=0.3.1.post4
13
+ Requires-Dist: py-hamt>=3.6.0
14
+ Requires-Dist: pyarrow>=18.0.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # tabular-py
18
+
19
+ Python reader for `dclimate-tabular/0` — content-addressed station/telemetry data on IPFS.
20
+
21
+ The Python counterpart to [`tabular-js`](https://github.com/dClimate/tabular-js), reading the
22
+ same format from the same CIDs. Built on [`py-hamt`](https://github.com/dClimate/py-hamt),
23
+ which supplies the HAMT and the content-addressed store the same way
24
+ `@dclimate/ipld-index` does for JS.
25
+
26
+ ```
27
+ tabular-js -> @dclimate/ipld-index (HAMT, CAS, range reads)
28
+ tabular-py -> py-hamt (same, already existed)
29
+ ```
30
+
31
+ ## Status
32
+
33
+ **Reader only.** Publishing, compaction, and rollup stay in `tabular-js` — the ETL that
34
+ writes these datasets is JS, and a second writer would be a second thing to keep
35
+ byte-identical for no current gain. Everything needed to *read* a dataset published by
36
+ `tabular-js` is here.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ uv pip install dclimate-tabular-py
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ import asyncio
48
+ from tabular_py import GatewayRangeSource, StationDataset
49
+
50
+ async def main():
51
+ source = GatewayRangeSource("https://ipfs-gateway.dclimate.net")
52
+ ds = await StationDataset.open(source, "bafyr4if2wbttslbxpzmro427j4l4nvcrxqo4tufuffqqmqz7afj2pxyu4a")
53
+
54
+ # nearest station to a point, then a year of readings
55
+ near = await ds.nearest(34.05, -118.24, max_km=100)
56
+ rows = await near.time_range("2024-01-01", "2024-12-31").elements("PRCP").rows()
57
+ for row in rows[:5]:
58
+ print(row.station_id, row.ts, row.values)
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ The chainable selection API mirrors `tabular-js`:
64
+
65
+ ```python
66
+ ds.select("USW00023174") # explicit station ids
67
+ ds.circle(34.05, -118.24, 50) # within 50 km
68
+ ds.rectangle(33.0, -119.0, 35.0, -117.0)
69
+ ds.polygon([[(lon, lat), ...]])
70
+ await ds.nearest(lat, lon) # async: reads the geo index
71
+ ds.time_range(start, end)
72
+ ds.elements("PRCP", "TMAX")
73
+ ds.where(gt("TMAX", 300)) # pushed down to fragment statistics
74
+ ```
75
+
76
+ Selections are immutable — each call returns a new `StationDataset`, so a base dataset can
77
+ be reused across queries.
78
+
79
+ Terminal operations:
80
+
81
+ | Call | Returns |
82
+ |---|---|
83
+ | `await ds.rows()` | `list[ResultRow]` |
84
+ | `await ds.to_records()` | `list[dict]` — `station_id`, `time`, `values` |
85
+ | `await ds.to_records("TMAX")` | `list[dict]` — `station_id`, `time`, `value` |
86
+ | `await ds.to_arrow()` | `pyarrow.Table` |
87
+ | `await ds.plan()` | `QueryPlan` — what *would* be fetched, without fetching |
88
+ | `await ds.list_stations()` | `list[StationInfo]` |
89
+
90
+ ## How reads stay small
91
+
92
+ A query never scans the dataset. Three things prune before any Parquet byte is fetched:
93
+
94
+ 1. **The station index** (a HAMT keyed by station id) resolves named stations directly.
95
+ 2. **The geo projection** answers region queries by reading one or two shard blocks
96
+ instead of walking all 132k stations.
97
+ 3. **Fragment statistics in the manifest** — per-column min/max and null counts — let a
98
+ predicate skip whole fragments unread.
99
+
100
+ What survives is fetched with HTTP range requests against the exact column-chunk byte
101
+ ranges the manifest records, so a query for one column of one year moves kilobytes.
102
+
103
+ ### One deviation from `tabular-js`, and why
104
+
105
+ `tabular-js` synthesizes Parquet `FileMetaData` client-side from the manifest and reads a
106
+ fragment with **zero** footer fetches. PyArrow exposes no public `FileMetaData`
107
+ constructor, so that trick does not transfer.
108
+
109
+ Instead this reader fetches the footer by its manifest-recorded
110
+ `footer_offset`/`footer_length` in a single ranged GET, verifies it against the
111
+ manifest's `footer_digest`, and hands the parsed metadata to PyArrow. Cost is one extra
112
+ range request per fragment — and it buys a corruption check `tabular-js` does not
113
+ perform. Footers are cached per fragment CID, so a repeated query pays it once.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ uv sync
119
+ uv run pytest # unit tests, no network
120
+ uv run pytest -m network # conformance against the live gateway
121
+ uv run ruff check . && uv run mypy tabular_py
122
+ ```
123
+
124
+ `tests/test_conformance.py` reads a real published GHCNd dataset
125
+ (`bafyr4if2wbttslbxpzmro427j4l4nvcrxqo4tufuffqqmqz7afj2pxyu4a`, 132,437 stations,
126
+ 1.15 B rows) and asserts this implementation agrees with `tabular-js` on decoded values.
@@ -0,0 +1,110 @@
1
+ # tabular-py
2
+
3
+ Python reader for `dclimate-tabular/0` — content-addressed station/telemetry data on IPFS.
4
+
5
+ The Python counterpart to [`tabular-js`](https://github.com/dClimate/tabular-js), reading the
6
+ same format from the same CIDs. Built on [`py-hamt`](https://github.com/dClimate/py-hamt),
7
+ which supplies the HAMT and the content-addressed store the same way
8
+ `@dclimate/ipld-index` does for JS.
9
+
10
+ ```
11
+ tabular-js -> @dclimate/ipld-index (HAMT, CAS, range reads)
12
+ tabular-py -> py-hamt (same, already existed)
13
+ ```
14
+
15
+ ## Status
16
+
17
+ **Reader only.** Publishing, compaction, and rollup stay in `tabular-js` — the ETL that
18
+ writes these datasets is JS, and a second writer would be a second thing to keep
19
+ byte-identical for no current gain. Everything needed to *read* a dataset published by
20
+ `tabular-js` is here.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ uv pip install dclimate-tabular-py
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import asyncio
32
+ from tabular_py import GatewayRangeSource, StationDataset
33
+
34
+ async def main():
35
+ source = GatewayRangeSource("https://ipfs-gateway.dclimate.net")
36
+ ds = await StationDataset.open(source, "bafyr4if2wbttslbxpzmro427j4l4nvcrxqo4tufuffqqmqz7afj2pxyu4a")
37
+
38
+ # nearest station to a point, then a year of readings
39
+ near = await ds.nearest(34.05, -118.24, max_km=100)
40
+ rows = await near.time_range("2024-01-01", "2024-12-31").elements("PRCP").rows()
41
+ for row in rows[:5]:
42
+ print(row.station_id, row.ts, row.values)
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ The chainable selection API mirrors `tabular-js`:
48
+
49
+ ```python
50
+ ds.select("USW00023174") # explicit station ids
51
+ ds.circle(34.05, -118.24, 50) # within 50 km
52
+ ds.rectangle(33.0, -119.0, 35.0, -117.0)
53
+ ds.polygon([[(lon, lat), ...]])
54
+ await ds.nearest(lat, lon) # async: reads the geo index
55
+ ds.time_range(start, end)
56
+ ds.elements("PRCP", "TMAX")
57
+ ds.where(gt("TMAX", 300)) # pushed down to fragment statistics
58
+ ```
59
+
60
+ Selections are immutable — each call returns a new `StationDataset`, so a base dataset can
61
+ be reused across queries.
62
+
63
+ Terminal operations:
64
+
65
+ | Call | Returns |
66
+ |---|---|
67
+ | `await ds.rows()` | `list[ResultRow]` |
68
+ | `await ds.to_records()` | `list[dict]` — `station_id`, `time`, `values` |
69
+ | `await ds.to_records("TMAX")` | `list[dict]` — `station_id`, `time`, `value` |
70
+ | `await ds.to_arrow()` | `pyarrow.Table` |
71
+ | `await ds.plan()` | `QueryPlan` — what *would* be fetched, without fetching |
72
+ | `await ds.list_stations()` | `list[StationInfo]` |
73
+
74
+ ## How reads stay small
75
+
76
+ A query never scans the dataset. Three things prune before any Parquet byte is fetched:
77
+
78
+ 1. **The station index** (a HAMT keyed by station id) resolves named stations directly.
79
+ 2. **The geo projection** answers region queries by reading one or two shard blocks
80
+ instead of walking all 132k stations.
81
+ 3. **Fragment statistics in the manifest** — per-column min/max and null counts — let a
82
+ predicate skip whole fragments unread.
83
+
84
+ What survives is fetched with HTTP range requests against the exact column-chunk byte
85
+ ranges the manifest records, so a query for one column of one year moves kilobytes.
86
+
87
+ ### One deviation from `tabular-js`, and why
88
+
89
+ `tabular-js` synthesizes Parquet `FileMetaData` client-side from the manifest and reads a
90
+ fragment with **zero** footer fetches. PyArrow exposes no public `FileMetaData`
91
+ constructor, so that trick does not transfer.
92
+
93
+ Instead this reader fetches the footer by its manifest-recorded
94
+ `footer_offset`/`footer_length` in a single ranged GET, verifies it against the
95
+ manifest's `footer_digest`, and hands the parsed metadata to PyArrow. Cost is one extra
96
+ range request per fragment — and it buys a corruption check `tabular-js` does not
97
+ perform. Footers are cached per fragment CID, so a repeated query pays it once.
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ uv sync
103
+ uv run pytest # unit tests, no network
104
+ uv run pytest -m network # conformance against the live gateway
105
+ uv run ruff check . && uv run mypy tabular_py
106
+ ```
107
+
108
+ `tests/test_conformance.py` reads a real published GHCNd dataset
109
+ (`bafyr4if2wbttslbxpzmro427j4l4nvcrxqo4tufuffqqmqz7afj2pxyu4a`, 132,437 stations,
110
+ 1.15 B rows) and asserts this implementation agrees with `tabular-js` on decoded values.
@@ -0,0 +1,88 @@
1
+ [project]
2
+ # PyPI rejects "tabular-py": it normalises to the existing "tabularpy" project.
3
+ # The distribution name is namespaced instead, matching @dclimate/tabular on npm.
4
+ # The import name stays `tabular_py`, which PyPI has no opinion about.
5
+ name = "dclimate-tabular-py"
6
+ version = "0.1.0"
7
+ description = "Content-addressed tabular/telemetry manifest for climate data on IPFS (dclimate-tabular/0)"
8
+ readme = "README.md"
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "dag-cbor>=0.3.3",
13
+ "multiformats[full]>=0.3.1.post4",
14
+ # Declared explicitly rather than relied on through multiformats[full]:
15
+ # py-hamt imports blake3 the same way without declaring it, and a transitive
16
+ # dep that the format's only hash function depends on is not one to inherit.
17
+ "blake3>=0.4.1",
18
+ "py-hamt>=3.6.0",
19
+ "pyarrow>=18.0.0",
20
+ "httpx>=0.28.1",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/dClimate/tabular-py"
25
+ Repository = "https://github.com/dClimate/tabular-py"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["tabular_py"]
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest>=8.3.3",
37
+ "pytest-asyncio>=1.0.0",
38
+ "pytest-cov>=6.0.0",
39
+ "ruff>=0.7.1",
40
+ "mypy>=1.15.0",
41
+ ]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+ # py-hamt pins every async fixture/test to a function-scoped loop; the same rule
46
+ # applies here because KuboCAS caches an httpx client per running loop.
47
+ asyncio_default_fixture_loop_scope = "function"
48
+ testpaths = ["tests"]
49
+ markers = [
50
+ "network: hits a real IPFS gateway; deselect with -m 'not network'",
51
+ ]
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py312"
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "I", "UP", "B", "SIM"]
59
+ # TC00x is deliberately off. `from __future__ import annotations` is on
60
+ # everywhere, so annotations are already lazy, and the types these rules want
61
+ # moved into TYPE_CHECKING blocks are needed at runtime anyway -- CID and the
62
+ # model classes back isinstance checks, and the dataclass field types are
63
+ # resolved when slots are built.
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ # Tests reach into private attributes to corrupt a block and assert the
67
+ # integrity check fires; that is the point of the test.
68
+ "tests/*" = ["SLF001"]
69
+
70
+ [tool.mypy]
71
+ python_version = "3.12"
72
+ strict = true
73
+ # Off because the exhaustiveness fallbacks at the end of the predicate
74
+ # dispatchers are unreachable only as long as the Literal unions stay in sync
75
+ # with the branches above them. Keeping the guard is worth more than the check.
76
+ warn_unreachable = false
77
+
78
+ [[tool.mypy.overrides]]
79
+ # Neither ships a py.typed marker, so everything they export arrives as Any.
80
+ module = ["py_hamt.*", "pyarrow.*"]
81
+ ignore_missing_imports = true
82
+
83
+ [[tool.mypy.overrides]]
84
+ # The wire decoders funnel every value through helpers that narrow `Any` by
85
+ # hand and raise otherwise. mypy cannot see through that, so it reports each
86
+ # validated return as "returning Any"; the validation is the point.
87
+ module = ["tabular_py.wire", "tabular_py.source", "tabular_py.reader"]
88
+ warn_return_any = false
@@ -0,0 +1,106 @@
1
+ """Capture a real published dataset into an offline fixture.
2
+
3
+ Records every block a small set of queries touches, so the test suite exercises
4
+ genuine `tabular-js`-written bytes without needing a gateway. Re-run when the
5
+ reference dataset changes:
6
+
7
+ uv run python scripts/capture_fixture.py
8
+
9
+ Blocks are stored base64 in a single JSON file, keyed by CID string.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import base64
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
21
+
22
+ from multiformats import CID # noqa: E402
23
+
24
+ from tabular_py import ( # noqa: E402
25
+ Circle,
26
+ GatewayRangeSource,
27
+ StationDataset,
28
+ cid_str,
29
+ )
30
+
31
+ GATEWAY = "https://ipfs-gateway.dclimate.net"
32
+ ROOT_CID = "bafyr4if2wbttslbxpzmro427j4l4nvcrxqo4tufuffqqmqz7afj2pxyu4a"
33
+ OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "ghcnd_sample.json"
34
+
35
+ # Stations chosen for coverage rather than convenience: one with pre-epoch
36
+ # timestamps, one modern, one in the LA cluster the geo tests query.
37
+ STATIONS = ["US1GAFS0003", "USC00045111"]
38
+
39
+ # Ids the dataset does not contain, so the not-found path is covered offline.
40
+ MISSING_STATIONS = ["NO_SUCH_STATION", "ZZZZZZZZZZZ9"]
41
+
42
+
43
+ class RecordingSource:
44
+ """Passes through to a real source and records every block it serves."""
45
+
46
+ def __init__(self, inner: GatewayRangeSource) -> None:
47
+ self._inner = inner
48
+ self.blocks: dict[str, bytes] = {}
49
+
50
+ async def get_block(self, cid: CID) -> bytes:
51
+ key = cid_str(cid)
52
+ if key not in self.blocks:
53
+ self.blocks[key] = await self._inner.get_block(cid)
54
+ return self.blocks[key]
55
+
56
+ async def get_range(self, cid: CID, offset: int, length: int) -> bytes:
57
+ # Record the whole block so the fixture can serve any range of it.
58
+ data = await self.get_block(cid)
59
+ return data[offset : offset + length]
60
+
61
+
62
+ async def main() -> None:
63
+ async with GatewayRangeSource(GATEWAY) as gateway:
64
+ source = RecordingSource(gateway)
65
+ ds = await StationDataset.open(source, ROOT_CID)
66
+
67
+ for station_id in STATIONS:
68
+ await ds.info_for(station_id)
69
+ await ds.columns_for(station_id)
70
+ await ds.select(station_id).elements("PRCP").rows()
71
+
72
+ # A lookup that misses still descends the trie, and lands on different
73
+ # nodes than any successful lookup does. Capturing that path is what
74
+ # lets the offline suite test "station is not in this dataset".
75
+ from tabular_py import lookup_station
76
+
77
+ for absent in MISSING_STATIONS:
78
+ await lookup_station(ds.reader.root.stations, source, absent)
79
+
80
+ # Geo paths: the projection directory plus the shards around LA.
81
+ await ds.find_nearest_station(34.05, -118.24)
82
+ from tabular_py import lookup_geo_stations
83
+
84
+ projection = ds.reader.root.projections.get("geo")
85
+ if projection is not None:
86
+ await lookup_geo_stations(
87
+ projection, source, Circle(34_052_000, -118_243_000, 25_000)
88
+ )
89
+
90
+ OUT.parent.mkdir(parents=True, exist_ok=True)
91
+ payload = {
92
+ "root": ROOT_CID,
93
+ "gateway": GATEWAY,
94
+ "stations": STATIONS,
95
+ "blocks": {
96
+ key: base64.b64encode(data).decode("ascii")
97
+ for key, data in sorted(source.blocks.items())
98
+ },
99
+ }
100
+ OUT.write_text(json.dumps(payload, indent=1))
101
+ total = sum(len(v) for v in source.blocks.values())
102
+ print(f"wrote {OUT} -- {len(source.blocks)} blocks, {total} bytes")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ asyncio.run(main())
@@ -0,0 +1,245 @@
1
+ """tabular-py -- Python reader for ``dclimate-tabular/0``.
2
+
3
+ The Python counterpart to ``@dclimate/tabular``, reading the same content-addressed
4
+ station datasets from the same CIDs, built on ``py-hamt``.
5
+
6
+ from tabular_py import GatewayRangeSource, StationDataset
7
+
8
+ source = GatewayRangeSource("https://ipfs-gateway.dclimate.net")
9
+ ds = await StationDataset.open(source, root_cid)
10
+ rows = await ds.select("USW00023174").time_range("2024-01-01", "2024-12-31").rows()
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .codec import (
16
+ BLOCK_HARD_LIMIT_BYTES,
17
+ BLOCK_TARGET_BYTES,
18
+ blake3_digest,
19
+ cid_for_bytes,
20
+ cid_str,
21
+ decode_block,
22
+ encode_block,
23
+ make_block,
24
+ verify_cid,
25
+ )
26
+ from .errors import (
27
+ BlockSizeError,
28
+ CidMismatchError,
29
+ CodecError,
30
+ DatasetIntegrityError,
31
+ DatasetReaderError,
32
+ DclimateTabularError,
33
+ GeoFilterError,
34
+ GeoIndexError,
35
+ PredicateError,
36
+ RangeSourceError,
37
+ StationIndexError,
38
+ StationSelectionError,
39
+ WireError,
40
+ )
41
+ from .geo import (
42
+ BBox,
43
+ Circle,
44
+ GeoFilter,
45
+ Polygon,
46
+ haversine_meters,
47
+ matches_geo_filter,
48
+ validate_geo_filter,
49
+ )
50
+ from .geo_index import (
51
+ bounding_boxes_for_filter,
52
+ first_accepted_in_order,
53
+ lookup_geo_stations,
54
+ nearest_station,
55
+ nearest_station_where,
56
+ )
57
+ from .model import (
58
+ SPEC_VERSION,
59
+ CellValue,
60
+ ColumnStat,
61
+ DatasetRoot,
62
+ FragmentEntry,
63
+ GeoProjection,
64
+ GeoShardRef,
65
+ HamtStationIndex,
66
+ InlineStationIndex,
67
+ StationEntry,
68
+ StationIndex,
69
+ StationSummary,
70
+ TableField,
71
+ TableSchema,
72
+ columns_from_fragments,
73
+ correction_schema,
74
+ default_column_key,
75
+ )
76
+ from .predicate import (
77
+ And,
78
+ Comparison,
79
+ Not,
80
+ NullCheck,
81
+ Or,
82
+ Predicate,
83
+ and_,
84
+ eq,
85
+ evaluate_row_predicate,
86
+ evaluate_stats_predicate,
87
+ ge,
88
+ gt,
89
+ is_null,
90
+ le,
91
+ lt,
92
+ ne,
93
+ not_,
94
+ not_null,
95
+ or_,
96
+ predicate_elements,
97
+ )
98
+ from .reader import (
99
+ DatasetReader,
100
+ PlanRange,
101
+ Query,
102
+ QueryPlan,
103
+ QueryPlanFragment,
104
+ ResultRow,
105
+ )
106
+ from .source import (
107
+ CasRangeSource,
108
+ CountingSource,
109
+ GatewayRangeSource,
110
+ MemoryRangeSource,
111
+ RangeSource,
112
+ )
113
+ from .station_dataset import NearestStation, StationDataset, StationInfo, WithinRange
114
+ from .station_index import (
115
+ list_station_entries,
116
+ list_stations,
117
+ lookup_station,
118
+ station_count,
119
+ )
120
+ from .wire import (
121
+ fragment_from_wire,
122
+ fragment_to_wire,
123
+ geo_projection_from_wire,
124
+ geo_projection_to_wire,
125
+ geo_shard_from_wire,
126
+ geo_shard_to_wire,
127
+ root_from_wire,
128
+ root_to_wire,
129
+ station_entry_from_wire,
130
+ station_entry_to_wire,
131
+ station_summary_from_wire,
132
+ station_summary_to_wire,
133
+ table_schema_from_wire,
134
+ table_schema_to_wire,
135
+ )
136
+
137
+ __version__ = "0.1.0"
138
+
139
+ __all__ = [
140
+ "BLOCK_HARD_LIMIT_BYTES",
141
+ "BLOCK_TARGET_BYTES",
142
+ "SPEC_VERSION",
143
+ "And",
144
+ "BBox",
145
+ "BlockSizeError",
146
+ "CasRangeSource",
147
+ "CellValue",
148
+ "CidMismatchError",
149
+ "Circle",
150
+ "CodecError",
151
+ "ColumnStat",
152
+ "Comparison",
153
+ "CountingSource",
154
+ "DatasetIntegrityError",
155
+ "DatasetReader",
156
+ "DatasetReaderError",
157
+ "DatasetRoot",
158
+ "DclimateTabularError",
159
+ "FragmentEntry",
160
+ "GatewayRangeSource",
161
+ "GeoFilter",
162
+ "GeoFilterError",
163
+ "GeoIndexError",
164
+ "GeoProjection",
165
+ "GeoShardRef",
166
+ "HamtStationIndex",
167
+ "InlineStationIndex",
168
+ "MemoryRangeSource",
169
+ "NearestStation",
170
+ "Not",
171
+ "NullCheck",
172
+ "Or",
173
+ "PlanRange",
174
+ "Polygon",
175
+ "Predicate",
176
+ "PredicateError",
177
+ "Query",
178
+ "QueryPlan",
179
+ "QueryPlanFragment",
180
+ "RangeSource",
181
+ "RangeSourceError",
182
+ "ResultRow",
183
+ "StationDataset",
184
+ "StationEntry",
185
+ "StationIndex",
186
+ "StationIndexError",
187
+ "StationInfo",
188
+ "StationSelectionError",
189
+ "StationSummary",
190
+ "TableField",
191
+ "TableSchema",
192
+ "WireError",
193
+ "WithinRange",
194
+ "__version__",
195
+ "and_",
196
+ "blake3_digest",
197
+ "bounding_boxes_for_filter",
198
+ "cid_for_bytes",
199
+ "cid_str",
200
+ "columns_from_fragments",
201
+ "correction_schema",
202
+ "decode_block",
203
+ "default_column_key",
204
+ "encode_block",
205
+ "eq",
206
+ "first_accepted_in_order",
207
+ "evaluate_row_predicate",
208
+ "evaluate_stats_predicate",
209
+ "fragment_from_wire",
210
+ "fragment_to_wire",
211
+ "ge",
212
+ "geo_projection_from_wire",
213
+ "geo_projection_to_wire",
214
+ "geo_shard_from_wire",
215
+ "geo_shard_to_wire",
216
+ "gt",
217
+ "haversine_meters",
218
+ "is_null",
219
+ "le",
220
+ "list_station_entries",
221
+ "list_stations",
222
+ "lookup_geo_stations",
223
+ "lookup_station",
224
+ "lt",
225
+ "make_block",
226
+ "matches_geo_filter",
227
+ "ne",
228
+ "nearest_station",
229
+ "nearest_station_where",
230
+ "not_",
231
+ "not_null",
232
+ "or_",
233
+ "predicate_elements",
234
+ "root_from_wire",
235
+ "root_to_wire",
236
+ "station_count",
237
+ "station_entry_from_wire",
238
+ "station_entry_to_wire",
239
+ "station_summary_from_wire",
240
+ "station_summary_to_wire",
241
+ "table_schema_from_wire",
242
+ "table_schema_to_wire",
243
+ "validate_geo_filter",
244
+ "verify_cid",
245
+ ]