batchcorder-datafusion 55.0.0__cp310-abi3-win_amd64.whl

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,36 @@
1
+ """
2
+ DataFusion table provider that tees one read-once Arrow stream to every scan.
3
+
4
+ Register a :class:`TeeTableProvider` with a ``SessionContext`` from a
5
+ DataFusion host (datafusion-python or xorq-datafusion) and every scan of the table in a query replays the same
6
+ upstream stream, which is read only once. With the default
7
+ ``bounding="sealed"``, each planned scan reserves a reader at planning time,
8
+ and each batch is evicted as soon as every scan has read past it.
9
+
10
+ Example::
11
+
12
+ import pyarrow as pa
13
+ from datafusion import SessionContext
14
+ from batchcorder_datafusion import TeeTableProvider
15
+
16
+ ctx = SessionContext()
17
+ ctx.register_table("t", TeeTableProvider(reader))
18
+ ctx.sql("select * from t a join t b using (k)").collect()
19
+
20
+ A sealed provider serves one query. Register a new provider per query, or
21
+ pass ``bounding="unbounded"`` for a table that is queried repeatedly.
22
+
23
+ The host must be built on the same DataFusion major as this wheel.
24
+ Registering the provider with any other host raises :class:`RuntimeError`.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from importlib.metadata import version
30
+
31
+ from ._batchcorder_datafusion import TeeTableProvider
32
+
33
+
34
+ __all__ = ["TeeTableProvider"]
35
+
36
+ __version__: str = version("batchcorder-datafusion")
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: batchcorder_datafusion/_batchcorder_datafusion.pyd
3
+ size: 61756416 bytes
4
+ sha256: f55e64eb0161e1b09d321b16bc203ca8b995beff41abb752d219112246627869
@@ -0,0 +1,35 @@
1
+ import typing
2
+
3
+ import pyarrow as pa
4
+
5
+ __all__ = ["TeeTableProvider"]
6
+
7
+ @typing.final
8
+ class TeeTableProvider:
9
+ def __new__(
10
+ cls,
11
+ reader: typing.Any,
12
+ *,
13
+ memory_capacity: int | None = None,
14
+ disk_path: str | None = None,
15
+ disk_capacity: int | None = None,
16
+ write_policy: str = "on_insertion",
17
+ bounding: str = "sealed",
18
+ ) -> TeeTableProvider: ...
19
+ def __datafusion_table_provider__(self, session: typing.Any) -> typing.Any: ...
20
+ @property
21
+ def schema(self) -> pa.Schema: ...
22
+ @property
23
+ def scans_planned(self) -> int: ...
24
+ @property
25
+ def scans_executed(self) -> int: ...
26
+ @property
27
+ def ingested_count(self) -> int: ...
28
+ @property
29
+ def evicted_count(self) -> int: ...
30
+ @property
31
+ def upstream_exhausted(self) -> bool: ...
32
+ @property
33
+ def is_sealed(self) -> bool: ...
34
+ def seal(self) -> None: ...
35
+ def close(self) -> None: ...
@@ -0,0 +1,168 @@
1
+ """Check that a DataFusion host is built on the same DataFusion major as this wheel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ from importlib.metadata import PackageNotFoundError, version
8
+ from typing import TYPE_CHECKING
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+
15
+ # The provider crosses into the host through datafusion-ffi, whose structs are
16
+ # only compatible within one DataFusion major. Each entry maps a host
17
+ # distribution to the half-open range [low, high) of its versions built on the
18
+ # same DataFusion major as this wheel (datafusion-ffi in Cargo.toml).
19
+ #
20
+ # Keep this table and the compatibility table in README.md in sync.
21
+ SUPPORTED_HOSTS: dict[str, tuple[tuple[int, ...], tuple[int, ...]]] = {
22
+ "datafusion": ((55,), (56,)),
23
+ "xorq-datafusion": ((0, 3), (0, 4)),
24
+ }
25
+
26
+ # Top-level Python package of each host -> its distribution. A session class
27
+ # belongs to a host when its module is in the package, or when the package
28
+ # exports it (see `_host_distribution`).
29
+ HOST_PACKAGES: dict[str, str] = {
30
+ "datafusion": "datafusion",
31
+ "xorq_datafusion": "xorq-datafusion",
32
+ }
33
+
34
+ DATAFUSION_MAJOR = 55
35
+
36
+
37
+ def _release(text: str) -> tuple[int, ...]:
38
+ """
39
+ Return the leading numeric release segment of a version string.
40
+
41
+ Parameters
42
+ ----------
43
+ text
44
+ A version such as ``"55.0.0"`` or ``"0.3.0rc1"``.
45
+
46
+ Returns
47
+ -------
48
+ tuple[int, ...]
49
+ The release numbers, e.g. ``(55, 0, 0)`` or ``(0, 3, 0)``.
50
+
51
+ """
52
+ match = re.match(r"\d+(?:\.\d+)*", text)
53
+ if match is None:
54
+ return ()
55
+ return tuple(int(part) for part in match.group().split("."))
56
+
57
+
58
+ def _format(release: tuple[int, ...]) -> str:
59
+ return ".".join(map(str, release))
60
+
61
+
62
+ def _exports(package: str, cls: type) -> bool:
63
+ """
64
+ Tell whether ``package`` or one of its imported submodules exports ``cls``.
65
+
66
+ Parameters
67
+ ----------
68
+ package
69
+ A host's top-level Python package.
70
+ cls
71
+ The class of the session the host passed.
72
+
73
+ Returns
74
+ -------
75
+ bool
76
+ ``True`` if an imported module of ``package`` has ``cls`` as an
77
+ attribute.
78
+
79
+ """
80
+ prefix = package + "."
81
+ # Only modules already imported: the host imported its own package to
82
+ # build the session, and importing one here could have side effects.
83
+ for name, module in list(sys.modules.items()):
84
+ if module is None or not (name == package or name.startswith(prefix)):
85
+ continue
86
+ if any(value is cls for value in vars(module).values()):
87
+ return True
88
+ return False
89
+
90
+
91
+ def _host_distribution(session: object) -> str | None:
92
+ """
93
+ Return the distribution of the host that ``session`` belongs to.
94
+
95
+ Parameters
96
+ ----------
97
+ session
98
+ The ``session`` argument the host passed to
99
+ ``__datafusion_table_provider__``.
100
+
101
+ Returns
102
+ -------
103
+ str | None
104
+ The host's distribution, or ``None`` for an unknown host.
105
+
106
+ """
107
+ cls = type(session)
108
+ dist = HOST_PACKAGES.get(cls.__module__.partition(".")[0])
109
+ if dist is not None or cls.__module__ in sys.modules:
110
+ return dist
111
+ # A native (PyO3) class reports the module name given at build time,
112
+ # which need not be a real module: xorq-datafusion's SessionContext says
113
+ # `let`. Find the host package that exports the class itself. A class
114
+ # from a real module (a capsule is `builtins.PyCapsule`, which hosts
115
+ # re-export) is never looked up this way.
116
+ for package, dist in HOST_PACKAGES.items():
117
+ if _exports(package, cls):
118
+ return dist
119
+ return None
120
+
121
+
122
+ def check_host(
123
+ session: object,
124
+ get_version: Callable[[str], str] = version,
125
+ ) -> None:
126
+ """
127
+ Raise if ``session`` belongs to a host built on another DataFusion major.
128
+
129
+ Called by ``TeeTableProvider.__datafusion_table_provider__`` before any
130
+ FFI struct of the host is touched. The host is identified by the package
131
+ of the session's class, or by the host package that exports that class.
132
+ A codec capsule, or a session from an unknown host, cannot be identified
133
+ here; the provider then checks the datafusion-ffi version of the host's
134
+ codec instead.
135
+
136
+ Parameters
137
+ ----------
138
+ session
139
+ The ``session`` argument the host passed to
140
+ ``__datafusion_table_provider__``.
141
+ get_version
142
+ Callable mapping a distribution name to its installed version and
143
+ raising :class:`importlib.metadata.PackageNotFoundError` when it is
144
+ not installed. Defaults to :func:`importlib.metadata.version`.
145
+
146
+ Raises
147
+ ------
148
+ RuntimeError
149
+ If the host's installed version is outside its supported range.
150
+
151
+ """
152
+ dist = _host_distribution(session)
153
+ if dist is None:
154
+ return
155
+ try:
156
+ installed = get_version(dist)
157
+ except PackageNotFoundError:
158
+ return
159
+ low, high = SUPPORTED_HOSTS[dist]
160
+ if low <= _release(installed) < high:
161
+ return
162
+ raise RuntimeError(
163
+ f"TeeTableProvider cannot be registered with {dist} {installed}: "
164
+ f"batchcorder-datafusion is built on DataFusion {DATAFUSION_MAJOR} and "
165
+ f"needs {dist}>={_format(low)},<{_format(high)}. Install the "
166
+ f"batchcorder-datafusion release built on the same DataFusion major "
167
+ f"as {dist} {installed}, or a {dist} in that range."
168
+ )
File without changes
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: batchcorder-datafusion
3
+ Version: 55.0.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Dist: pyarrow>=18
14
+ Summary: DataFusion table provider that tees one read-once Arrow stream to every scan in a query
15
+ Author: Daniel Mesejo
16
+ License: Apache-2.0
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Project-URL: Homepage, https://github.com/mesejo/batchcorder
20
+ Project-URL: Repository, https://github.com/mesejo/batchcorder
21
+
22
+ # batchcorder-datafusion
23
+
24
+ A DataFusion table provider that tees one read-once Arrow stream to every
25
+ scan in a query.
26
+
27
+ Register a `TeeTableProvider` with a DataFusion `SessionContext`, and every
28
+ scan of the table in a query (self-joins, CTEs referenced twice, subqueries)
29
+ replays the same upstream stream, which is read only once. With the
30
+ default `bounding="sealed"`, each planned scan reserves a reader at planning
31
+ time, and each batch is evicted as soon as every scan has read past it.
32
+
33
+ ## Install
34
+
35
+ The provider plugs into a DataFusion host through `datafusion-ffi`. It does
36
+ not install a host: install the one you use, in a version from the
37
+ compatibility table below.
38
+
39
+ ```bash
40
+ pip install batchcorder-datafusion "datafusion>=55,<56" # datafusion-python
41
+ pip install batchcorder-datafusion "xorq-datafusion>=0.3,<0.4" # xorq-datafusion
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ import pyarrow as pa
48
+ from datafusion import SessionContext
49
+
50
+ from batchcorder_datafusion import TeeTableProvider
51
+
52
+
53
+ schema = pa.schema([("k", pa.int64())])
54
+ reader = pa.RecordBatchReader.from_batches(
55
+ schema, [pa.record_batch([[1, 2, 3]], schema=schema)]
56
+ )
57
+
58
+ ctx = SessionContext()
59
+ ctx.register_table("t", TeeTableProvider(reader))
60
+ ctx.sql("select * from t a join t b using (k)").collect()
61
+ ```
62
+
63
+ The upstream can be any object that implements the Arrow PyCapsule stream
64
+ interface (`__arrow_c_stream__`).
65
+
66
+ A sealed provider serves one query. Register a new provider per query, or
67
+ pass `bounding="unbounded"` for a table that is queried repeatedly.
68
+
69
+ ### Do not pass a `batchcorder.StreamCache`
70
+
71
+ The provider keeps its own cache. Passing a `batchcorder` reader caches the
72
+ data twice. Pass the original upstream reader, unless you also need to replay
73
+ the stream outside DataFusion.
74
+
75
+ ## Compatibility
76
+
77
+ `datafusion-ffi` structs are only compatible within one DataFusion major, so
78
+ the provider and the host must be built on the same one. The major version of
79
+ `batchcorder-datafusion` is that DataFusion major.
80
+
81
+ | batchcorder-datafusion | DataFusion | datafusion (datafusion-python) | xorq-datafusion |
82
+ |------------------------|------------|--------------------------------|-----------------|
83
+ | 55.x | 55 | `>=55,<56` | `>=0.3,<0.4` |
84
+
85
+ Registering a `TeeTableProvider` with a host outside these ranges raises
86
+ `RuntimeError`, naming the host version found and the range needed, before any
87
+ data crosses the FFI boundary.
88
+
89
+ `batchcorder-datafusion` does not depend on the `batchcorder` package. The
90
+ cache is compiled into its extension, and data enters through the Arrow C
91
+ stream interface.
92
+
@@ -0,0 +1,9 @@
1
+ batchcorder_datafusion/__init__.py,sha256=iP9G6JsrPgzSa4pG-1qNRNSz360d0Ds40dQosinSiAg,1293
2
+ batchcorder_datafusion/_batchcorder_datafusion.pyd,sha256=9V5k6wFh4bCdMhsWvCA8qLmVvv9Bq7dS0hkRIkZieGk,61756416
3
+ batchcorder_datafusion/_batchcorder_datafusion.pyi,sha256=Rw2X2PUdtQVfQ9E3Z6HlL9yCs7Yc4bshPtvhlCCNpcg,975
4
+ batchcorder_datafusion/_hosts.py,sha256=d7k1tnuA7E0v4faZbECWpsUHc-GQhHhQ2Zvwi4I7yCA,5508
5
+ batchcorder_datafusion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ batchcorder_datafusion-55.0.0.dist-info/METADATA,sha256=qqChFaj322NtiuVsZHPfvNA4IZzJ-kA5yF200fxWXMs,3590
7
+ batchcorder_datafusion-55.0.0.dist-info/WHEEL,sha256=TzjyIyBUptJHnlQyOymQYLWjtXe7MGI0m3QvmcG7PpI,96
8
+ batchcorder_datafusion-55.0.0.dist-info/sboms/batchcorder-datafusion-py.cyclonedx.json,sha256=_ZRMrakGPQW12EGJ3W2AtExVvjI28SD4Kda5ImubapY,345548
9
+ batchcorder_datafusion-55.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.16.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp310-abi3-win_amd64