narwhals-datafusion 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.
- narwhals_datafusion-0.1.0/.gitignore +30 -0
- narwhals_datafusion-0.1.0/.gitmodules +3 -0
- narwhals_datafusion-0.1.0/LICENSE.md +21 -0
- narwhals_datafusion-0.1.0/PKG-INFO +149 -0
- narwhals_datafusion-0.1.0/README.md +130 -0
- narwhals_datafusion-0.1.0/pyproject.toml +75 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/__init__.py +28 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/dataframe.py +438 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/expr.py +477 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/expr_dt.py +130 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/expr_list.py +50 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/expr_str.py +82 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/expr_struct.py +17 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/extra_functions.py +37 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/group_by.py +35 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/namespace.py +253 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/py.typed +0 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/selectors.py +34 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/testing.py +37 -0
- narwhals_datafusion-0.1.0/src/narwhals_datafusion/utils.py +260 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.so
|
|
4
|
+
.venv/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.hypothesis/
|
|
11
|
+
tmp/
|
|
12
|
+
|
|
13
|
+
# Rust (extra-functions-ffi)
|
|
14
|
+
target/
|
|
15
|
+
|
|
16
|
+
# IDEs
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
|
|
20
|
+
# IPython / Jupyter
|
|
21
|
+
.ipynb_checkpoints/
|
|
22
|
+
profile_default/
|
|
23
|
+
ipython_config.py
|
|
24
|
+
*.ipynb
|
|
25
|
+
|
|
26
|
+
# Claude Code
|
|
27
|
+
.claude/
|
|
28
|
+
|
|
29
|
+
# Local-only docs (kept out of git and any publishing)
|
|
30
|
+
docs/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 to present, Dmitrii Bugakov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: narwhals-datafusion
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Apache DataFusion backend for Narwhals, via the Narwhals plugin system
|
|
5
|
+
Project-URL: Repository, https://github.com/s5dsn-eqee/narwhals-datafusion
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE.md
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: datafusion-extra-functions-ffi<0.2,>=0.1
|
|
15
|
+
Requires-Dist: datafusion<55,>=54
|
|
16
|
+
Requires-Dist: narwhals<2.26,>=2.25
|
|
17
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# narwhals-datafusion
|
|
21
|
+
|
|
22
|
+
[](https://github.com/s5dsn-eqee/narwhals-datafusion/actions/workflows/ci.yml)
|
|
23
|
+
[](https://badge.fury.io/py/narwhals-datafusion)
|
|
24
|
+
[](https://pepy.tech/project/narwhals-datafusion)
|
|
25
|
+
[](https://peps.python.org/pep-0740/)
|
|
26
|
+
[](https://pypi.org/project/narwhals-datafusion)
|
|
27
|
+
|
|
28
|
+
[Apache DataFusion](https://datafusion.apache.org/python/) backend for
|
|
29
|
+
[Narwhals](https://github.com/narwhals-dev/narwhals), implemented as an
|
|
30
|
+
out-of-tree plugin via the `narwhals.plugins` entry-point system.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import narwhals as nw
|
|
36
|
+
import pyarrow as pa
|
|
37
|
+
from datafusion import SessionContext
|
|
38
|
+
|
|
39
|
+
ctx = SessionContext()
|
|
40
|
+
df = ctx.from_arrow(pa.table({"a": [1, 2, 3], "b": ["x", "y", "x"]}))
|
|
41
|
+
|
|
42
|
+
lf = nw.from_native(df) # -> nw.LazyFrame, dispatched to this plugin
|
|
43
|
+
result = (
|
|
44
|
+
lf.group_by("b")
|
|
45
|
+
.agg(nw.col("a").sum())
|
|
46
|
+
.sort("b")
|
|
47
|
+
.collect(backend="pyarrow")
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Everything stays lazy until `.collect()`: narwhals expressions are translated to
|
|
52
|
+
`datafusion.Expr`, frame verbs to `datafusion.DataFrame` methods, and the plan
|
|
53
|
+
executes in DataFusion's Rust engine. Dtypes are pyarrow end-to-end.
|
|
54
|
+
|
|
55
|
+
## Architecture
|
|
56
|
+
|
|
57
|
+
The backend sits on narwhals' shared SQL layer (`narwhals._sql`), the same
|
|
58
|
+
abstraction DuckDB, Ibis, and Spark use. It targets `narwhals==2.25` internals
|
|
59
|
+
(`narwhals._sql`, `narwhals._compliant`), vendored as the `narwhals/` git
|
|
60
|
+
submodule pinned to that release. The plugin provides:
|
|
61
|
+
|
|
62
|
+
| Module | Class |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `dataframe.py` | `DataFusionLazyFrame` — frame verbs over `datafusion.DataFrame` |
|
|
65
|
+
| `expr.py` | `DataFusionExpr` — the six `SQLExpr` hooks + backend specifics |
|
|
66
|
+
| `namespace.py` | `DataFusionNamespace` — the four `SQLNamespace` primitives, IO, horizontal fns |
|
|
67
|
+
| `group_by.py`, `selectors.py`, `expr_str/dt/list/struct.py` | supporting surface |
|
|
68
|
+
| `utils.py` | function-name remapping, window/sort builders, dtype bridge (delegates to `narwhals._arrow`) |
|
|
69
|
+
|
|
70
|
+
## `mode`, `skew`, `kurtosis` via `datafusion-extra-functions`
|
|
71
|
+
|
|
72
|
+
DataFusion core deliberately keeps its function library lean, so aggregates
|
|
73
|
+
like `mode`/`skewness`/`kurtosis` live in the contrib
|
|
74
|
+
[`datafusion-extra-functions`](https://github.com/datafusion-contrib/datafusion-extra-functions)
|
|
75
|
+
crate (Rust-only, no wheel on PyPI). The
|
|
76
|
+
[`datafusion-extra-functions-ffi`](https://github.com/s5dsn-eqee/datafusion-extra-functions-ffi)
|
|
77
|
+
package — a required dependency, installed as a prebuilt wheel — exposes its
|
|
78
|
+
aggregate UDFs to datafusion-python via the `__datafusion_aggregate_udf__`
|
|
79
|
+
PyCapsule protocol; they back `Expr.mode`, `Expr.skew`, and `Expr.kurtosis`.
|
|
80
|
+
Its FFI ABI is tied to the `datafusion` major it was compiled against
|
|
81
|
+
(currently 54), which the wheel's own dependency pin enforces.
|
|
82
|
+
|
|
83
|
+
## API coverage
|
|
84
|
+
|
|
85
|
+
Status of the narwhals public API on this backend (`narwhals==2.25`,
|
|
86
|
+
`datafusion==54`). ⚠️ entries work with the caveat in parentheses; details in
|
|
87
|
+
[Known limitations](#known-limitations-as-of-datafusion-54).
|
|
88
|
+
|
|
89
|
+
| Namespace | ✅ Supported | ⚠️ Partial | ❌ Not supported |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `Expr` | `abs` `alias` `all` `any` `any_value` `ceil` `clip` `cos` `count` `cum_count` `cum_max` `cum_min` `cum_sum` `diff` `exp` `fill_nan` `first` `floor` `is_between` `is_close` `is_duplicated` `is_finite` `is_first_distinct` `is_in` `is_last_distinct` `is_nan` `is_null` `is_unique` `kurtosis` `last` `len` `log` `max` `mean` `median` `min` `null_count` `over` `pipe` `rank` `rolling_mean` `rolling_std` `rolling_sum` `rolling_var` `round` `shift` `sin` `skew` `sqrt` `std` `sum` `var` | `cast` (no `Enum`) · `fill_null` (no `strategy` + `limit`) · `mode` (`keep="any"` only) · `n_unique` (not over windows) · `replace_strict` (explicit `default` required) | `cum_prod` `quantile` |
|
|
92
|
+
| `Expr.str` | `contains` `ends_with` `head` `len_chars` `pad_end` `pad_start` `replace_all` `slice` `split` `starts_with` `strip_chars` `strip_chars_end` `strip_chars_start` `tail` `to_lowercase` `to_uppercase` `zfill` | `to_date`/`to_datetime` (explicit `format` required) · `to_time` (`"HH:MM:SS"`-style only) · `to_titlecase` (no word breaks on digits) | `replace` (use `replace_all`) |
|
|
93
|
+
| `Expr.dt` | `convert_time_zone` `date` `day` `hour` `microsecond` `millisecond` `minute` `month` `nanosecond` `ordinal_day` `second` `to_string` `truncate` `weekday` `year` | `replace_time_zone` (`None`/`"UTC"` only) | `offset_by` `timestamp` `total_microseconds` `total_milliseconds` `total_minutes` `total_nanoseconds` `total_seconds` |
|
|
94
|
+
| `Expr.list` | `contains` `get` `len` `max` `min` `sort` | `unique` (`maintain_order=False` only) | `mean` `median` `sum` |
|
|
95
|
+
| `Expr.struct` | `field` | | |
|
|
96
|
+
| `LazyFrame` | `collect` `collect_schema` `drop` `drop_nulls` `filter` `group_by` `head` `join` `rename` `select` `sort` `top_k` `unique` `unpivot` `with_columns` `with_row_index` | `explode` (single column) · `sink_parquet` (file path only) | `join_asof` |
|
|
97
|
+
|
|
98
|
+
Not listed: methods narwhals itself doesn't support on *any* lazy/SQL backend
|
|
99
|
+
(`Expr.filter`, `Expr.drop_nulls`, `Expr.unique`, `Expr.map_batches`,
|
|
100
|
+
`Expr.ewm_mean`, `LazyFrame.tail`, `LazyFrame.gather_every`).
|
|
101
|
+
|
|
102
|
+
## Known limitations (as of datafusion 54)
|
|
103
|
+
|
|
104
|
+
- `join_asof`, exact `quantile`,
|
|
105
|
+
`cum_prod`, `list.sum/mean/median`, `dt.total_*`, `dt.offset_by`,
|
|
106
|
+
`dt.timestamp`, `str.replace`, `Enum` casts — no engine support;
|
|
107
|
+
raise `NotImplementedError`.
|
|
108
|
+
- `n_unique().over(...)` raises: DataFusion silently ignores `DISTINCT` inside
|
|
109
|
+
window aggregates, which would return wrong results. (The same engine quirk
|
|
110
|
+
drops `ORDER BY`/`IGNORE NULLS` declared inside window aggregates — this
|
|
111
|
+
backend moves those modifiers onto the window itself.)
|
|
112
|
+
- `fill_null(strategy=..., limit=n)` raises: bounded window frames with
|
|
113
|
+
`first_value`/`last_value` need `retract_batch`, unimplemented engine-side.
|
|
114
|
+
- `replace_time_zone` supports `None` (strip) and `"UTC"` only; use
|
|
115
|
+
`convert_time_zone` for instant-preserving conversions.
|
|
116
|
+
- `str.to_datetime`/`to_date` require an explicit `format`;
|
|
117
|
+
`str.to_time` parses `"HH:MM:SS"`-style strings via Arrow's cast, ignoring
|
|
118
|
+
custom formats.
|
|
119
|
+
- `replace_strict` requires an explicit `default`.
|
|
120
|
+
- `str.to_titlecase` uses `initcap`, which doesn't break words on digits.
|
|
121
|
+
- No row-order guarantees except after `sort` (standard for SQL engines);
|
|
122
|
+
backward `fill_null` may reorder rows.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
git submodule update --init # narwhals, pinned to the targeted release
|
|
128
|
+
uv sync --group tests
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Running narwhals' own test suite against this backend
|
|
132
|
+
|
|
133
|
+
The narwhals repo is vendored as a git submodule pinned to the targeted
|
|
134
|
+
release:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
git submodule update --init
|
|
138
|
+
uv run --group tests python run_tests.py # known failures deselected
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For the full, unfiltered run:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
uv run --group tests pytest narwhals/tests -c narwhals/pyproject.toml \
|
|
145
|
+
-p narwhals_datafusion.testing -p env --use-external-constructor
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Regenerate the deselect list after fixing tests or bumping the submodule with
|
|
149
|
+
`uv run --group tests python update_run_tests.py`.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# narwhals-datafusion
|
|
2
|
+
|
|
3
|
+
[](https://github.com/s5dsn-eqee/narwhals-datafusion/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/py/narwhals-datafusion)
|
|
5
|
+
[](https://pepy.tech/project/narwhals-datafusion)
|
|
6
|
+
[](https://peps.python.org/pep-0740/)
|
|
7
|
+
[](https://pypi.org/project/narwhals-datafusion)
|
|
8
|
+
|
|
9
|
+
[Apache DataFusion](https://datafusion.apache.org/python/) backend for
|
|
10
|
+
[Narwhals](https://github.com/narwhals-dev/narwhals), implemented as an
|
|
11
|
+
out-of-tree plugin via the `narwhals.plugins` entry-point system.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import narwhals as nw
|
|
17
|
+
import pyarrow as pa
|
|
18
|
+
from datafusion import SessionContext
|
|
19
|
+
|
|
20
|
+
ctx = SessionContext()
|
|
21
|
+
df = ctx.from_arrow(pa.table({"a": [1, 2, 3], "b": ["x", "y", "x"]}))
|
|
22
|
+
|
|
23
|
+
lf = nw.from_native(df) # -> nw.LazyFrame, dispatched to this plugin
|
|
24
|
+
result = (
|
|
25
|
+
lf.group_by("b")
|
|
26
|
+
.agg(nw.col("a").sum())
|
|
27
|
+
.sort("b")
|
|
28
|
+
.collect(backend="pyarrow")
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Everything stays lazy until `.collect()`: narwhals expressions are translated to
|
|
33
|
+
`datafusion.Expr`, frame verbs to `datafusion.DataFrame` methods, and the plan
|
|
34
|
+
executes in DataFusion's Rust engine. Dtypes are pyarrow end-to-end.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
The backend sits on narwhals' shared SQL layer (`narwhals._sql`), the same
|
|
39
|
+
abstraction DuckDB, Ibis, and Spark use. It targets `narwhals==2.25` internals
|
|
40
|
+
(`narwhals._sql`, `narwhals._compliant`), vendored as the `narwhals/` git
|
|
41
|
+
submodule pinned to that release. The plugin provides:
|
|
42
|
+
|
|
43
|
+
| Module | Class |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `dataframe.py` | `DataFusionLazyFrame` — frame verbs over `datafusion.DataFrame` |
|
|
46
|
+
| `expr.py` | `DataFusionExpr` — the six `SQLExpr` hooks + backend specifics |
|
|
47
|
+
| `namespace.py` | `DataFusionNamespace` — the four `SQLNamespace` primitives, IO, horizontal fns |
|
|
48
|
+
| `group_by.py`, `selectors.py`, `expr_str/dt/list/struct.py` | supporting surface |
|
|
49
|
+
| `utils.py` | function-name remapping, window/sort builders, dtype bridge (delegates to `narwhals._arrow`) |
|
|
50
|
+
|
|
51
|
+
## `mode`, `skew`, `kurtosis` via `datafusion-extra-functions`
|
|
52
|
+
|
|
53
|
+
DataFusion core deliberately keeps its function library lean, so aggregates
|
|
54
|
+
like `mode`/`skewness`/`kurtosis` live in the contrib
|
|
55
|
+
[`datafusion-extra-functions`](https://github.com/datafusion-contrib/datafusion-extra-functions)
|
|
56
|
+
crate (Rust-only, no wheel on PyPI). The
|
|
57
|
+
[`datafusion-extra-functions-ffi`](https://github.com/s5dsn-eqee/datafusion-extra-functions-ffi)
|
|
58
|
+
package — a required dependency, installed as a prebuilt wheel — exposes its
|
|
59
|
+
aggregate UDFs to datafusion-python via the `__datafusion_aggregate_udf__`
|
|
60
|
+
PyCapsule protocol; they back `Expr.mode`, `Expr.skew`, and `Expr.kurtosis`.
|
|
61
|
+
Its FFI ABI is tied to the `datafusion` major it was compiled against
|
|
62
|
+
(currently 54), which the wheel's own dependency pin enforces.
|
|
63
|
+
|
|
64
|
+
## API coverage
|
|
65
|
+
|
|
66
|
+
Status of the narwhals public API on this backend (`narwhals==2.25`,
|
|
67
|
+
`datafusion==54`). ⚠️ entries work with the caveat in parentheses; details in
|
|
68
|
+
[Known limitations](#known-limitations-as-of-datafusion-54).
|
|
69
|
+
|
|
70
|
+
| Namespace | ✅ Supported | ⚠️ Partial | ❌ Not supported |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `Expr` | `abs` `alias` `all` `any` `any_value` `ceil` `clip` `cos` `count` `cum_count` `cum_max` `cum_min` `cum_sum` `diff` `exp` `fill_nan` `first` `floor` `is_between` `is_close` `is_duplicated` `is_finite` `is_first_distinct` `is_in` `is_last_distinct` `is_nan` `is_null` `is_unique` `kurtosis` `last` `len` `log` `max` `mean` `median` `min` `null_count` `over` `pipe` `rank` `rolling_mean` `rolling_std` `rolling_sum` `rolling_var` `round` `shift` `sin` `skew` `sqrt` `std` `sum` `var` | `cast` (no `Enum`) · `fill_null` (no `strategy` + `limit`) · `mode` (`keep="any"` only) · `n_unique` (not over windows) · `replace_strict` (explicit `default` required) | `cum_prod` `quantile` |
|
|
73
|
+
| `Expr.str` | `contains` `ends_with` `head` `len_chars` `pad_end` `pad_start` `replace_all` `slice` `split` `starts_with` `strip_chars` `strip_chars_end` `strip_chars_start` `tail` `to_lowercase` `to_uppercase` `zfill` | `to_date`/`to_datetime` (explicit `format` required) · `to_time` (`"HH:MM:SS"`-style only) · `to_titlecase` (no word breaks on digits) | `replace` (use `replace_all`) |
|
|
74
|
+
| `Expr.dt` | `convert_time_zone` `date` `day` `hour` `microsecond` `millisecond` `minute` `month` `nanosecond` `ordinal_day` `second` `to_string` `truncate` `weekday` `year` | `replace_time_zone` (`None`/`"UTC"` only) | `offset_by` `timestamp` `total_microseconds` `total_milliseconds` `total_minutes` `total_nanoseconds` `total_seconds` |
|
|
75
|
+
| `Expr.list` | `contains` `get` `len` `max` `min` `sort` | `unique` (`maintain_order=False` only) | `mean` `median` `sum` |
|
|
76
|
+
| `Expr.struct` | `field` | | |
|
|
77
|
+
| `LazyFrame` | `collect` `collect_schema` `drop` `drop_nulls` `filter` `group_by` `head` `join` `rename` `select` `sort` `top_k` `unique` `unpivot` `with_columns` `with_row_index` | `explode` (single column) · `sink_parquet` (file path only) | `join_asof` |
|
|
78
|
+
|
|
79
|
+
Not listed: methods narwhals itself doesn't support on *any* lazy/SQL backend
|
|
80
|
+
(`Expr.filter`, `Expr.drop_nulls`, `Expr.unique`, `Expr.map_batches`,
|
|
81
|
+
`Expr.ewm_mean`, `LazyFrame.tail`, `LazyFrame.gather_every`).
|
|
82
|
+
|
|
83
|
+
## Known limitations (as of datafusion 54)
|
|
84
|
+
|
|
85
|
+
- `join_asof`, exact `quantile`,
|
|
86
|
+
`cum_prod`, `list.sum/mean/median`, `dt.total_*`, `dt.offset_by`,
|
|
87
|
+
`dt.timestamp`, `str.replace`, `Enum` casts — no engine support;
|
|
88
|
+
raise `NotImplementedError`.
|
|
89
|
+
- `n_unique().over(...)` raises: DataFusion silently ignores `DISTINCT` inside
|
|
90
|
+
window aggregates, which would return wrong results. (The same engine quirk
|
|
91
|
+
drops `ORDER BY`/`IGNORE NULLS` declared inside window aggregates — this
|
|
92
|
+
backend moves those modifiers onto the window itself.)
|
|
93
|
+
- `fill_null(strategy=..., limit=n)` raises: bounded window frames with
|
|
94
|
+
`first_value`/`last_value` need `retract_batch`, unimplemented engine-side.
|
|
95
|
+
- `replace_time_zone` supports `None` (strip) and `"UTC"` only; use
|
|
96
|
+
`convert_time_zone` for instant-preserving conversions.
|
|
97
|
+
- `str.to_datetime`/`to_date` require an explicit `format`;
|
|
98
|
+
`str.to_time` parses `"HH:MM:SS"`-style strings via Arrow's cast, ignoring
|
|
99
|
+
custom formats.
|
|
100
|
+
- `replace_strict` requires an explicit `default`.
|
|
101
|
+
- `str.to_titlecase` uses `initcap`, which doesn't break words on digits.
|
|
102
|
+
- No row-order guarantees except after `sort` (standard for SQL engines);
|
|
103
|
+
backward `fill_null` may reorder rows.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
git submodule update --init # narwhals, pinned to the targeted release
|
|
109
|
+
uv sync --group tests
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Running narwhals' own test suite against this backend
|
|
113
|
+
|
|
114
|
+
The narwhals repo is vendored as a git submodule pinned to the targeted
|
|
115
|
+
release:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
git submodule update --init
|
|
119
|
+
uv run --group tests python run_tests.py # known failures deselected
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For the full, unfiltered run:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
uv run --group tests pytest narwhals/tests -c narwhals/pyproject.toml \
|
|
126
|
+
-p narwhals_datafusion.testing -p env --use-external-constructor
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Regenerate the deselect list after fixing tests or bumping the submodule with
|
|
130
|
+
`uv run --group tests python update_run_tests.py`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "narwhals-datafusion"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Apache DataFusion backend for Narwhals, via the Narwhals plugin system"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
# targets narwhals 2.25 internals (`narwhals._sql`); widen only after testing
|
|
14
|
+
"narwhals>=2.25,<2.26",
|
|
15
|
+
# must match the FFI shim's compiled ABI (see extra-functions-ffi dependency)
|
|
16
|
+
"datafusion>=54,<55",
|
|
17
|
+
"pyarrow>=14.0.0",
|
|
18
|
+
# FFI shim for `mode`/`skew`/`kurtosis`; prebuilt wheels from
|
|
19
|
+
# https://github.com/s5dsn-eqee/datafusion-extra-functions-ffi
|
|
20
|
+
"datafusion-extra-functions-ffi>=0.1,<0.2",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Repository = "https://github.com/s5dsn-eqee/narwhals-datafusion"
|
|
32
|
+
|
|
33
|
+
[project.entry-points.'narwhals.plugins']
|
|
34
|
+
narwhals-datafusion = 'narwhals_datafusion'
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/narwhals_datafusion"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
# ship only what's needed to build the wheel; `narwhals/` is the dev submodule
|
|
41
|
+
exclude = ["docs", "narwhals", "tests", "tmp", ".github", ".hypothesis", "run_tests.py", "update_run_tests.py", "uv.lock"]
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = ["pytest>=8"]
|
|
45
|
+
# Deps for running narwhals' own suite from the `narwhals/` submodule
|
|
46
|
+
# (daft-style): `uv run --group tests python run_tests.py`.
|
|
47
|
+
tests = [
|
|
48
|
+
"pytest>=9.0.3",
|
|
49
|
+
"pytest-env",
|
|
50
|
+
"pytest-randomly",
|
|
51
|
+
"pytest-xdist",
|
|
52
|
+
"hypothesis>=6.0.0",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.uv.sources]
|
|
56
|
+
# narwhals comes editable from the pinned submodule (run
|
|
57
|
+
# `git submodule update --init` after cloning), so the plugin is always
|
|
58
|
+
# developed and tested against the exact internals it targets.
|
|
59
|
+
narwhals = { path = "narwhals", editable = true }
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
|
|
64
|
+
# Standalone lint config (decoupled from the narwhals repo's root settings).
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py310"
|
|
68
|
+
extend-exclude = ["narwhals"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = ["E", "F", "I", "W", "UP", "B", "SIM", "TC"]
|
|
72
|
+
ignore = ["SIM108"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.isort]
|
|
75
|
+
known-first-party = ["narwhals_datafusion"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Apache DataFusion backend for Narwhals, registered via the `narwhals.plugins` entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import datafusion
|
|
9
|
+
from narwhals._utils import Version
|
|
10
|
+
from typing_extensions import TypeIs
|
|
11
|
+
|
|
12
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
NATIVE_PACKAGE = "datafusion"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __narwhals_namespace__(version: Version) -> DataFusionNamespace: # noqa: N807
|
|
20
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
21
|
+
|
|
22
|
+
return DataFusionNamespace(version=version)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_native(native_object: object) -> TypeIs[datafusion.DataFrame]:
|
|
26
|
+
import datafusion
|
|
27
|
+
|
|
28
|
+
return isinstance(native_object, datafusion.DataFrame)
|