polars-list-math 0.1.1__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.
- polars_list_math-0.1.1/LICENSE +21 -0
- polars_list_math-0.1.1/PKG-INFO +103 -0
- polars_list_math-0.1.1/README.md +74 -0
- polars_list_math-0.1.1/polars_list_math/__init__.py +26 -0
- polars_list_math-0.1.1/polars_list_math/_list_combinations.py +300 -0
- polars_list_math-0.1.1/polars_list_math/_list_mean_similarity.py +203 -0
- polars_list_math-0.1.1/polars_list_math/_list_similarity.py +200 -0
- polars_list_math-0.1.1/polars_list_math/_list_zip.py +162 -0
- polars_list_math-0.1.1/polars_list_math/py.typed +1 -0
- polars_list_math-0.1.1/pyproject.toml +91 -0
- polars_list_math-0.1.1/rust/Cargo.lock +2517 -0
- polars_list_math-0.1.1/rust/Cargo.toml +21 -0
- polars_list_math-0.1.1/rust/src/lib.rs +18 -0
- polars_list_math-0.1.1/rust/src/list_combinations.rs +446 -0
- polars_list_math-0.1.1/rust/src/list_mean_similarity.rs +313 -0
- polars_list_math-0.1.1/rust/src/list_similarity.rs +87 -0
- polars_list_math-0.1.1/rust/src/list_similarity_core.rs +481 -0
- polars_list_math-0.1.1/rust/src/list_zip.rs +235 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 polars-list-math contributors
|
|
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,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polars-list-math
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Rust
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: polars>=1.39.3
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Summary: List-oriented math and expression helpers for Polars.
|
|
19
|
+
Keywords: polars,list,dataframe,rust,pyo3
|
|
20
|
+
Home-Page: https://github.com/rufrozen/polars-list-math
|
|
21
|
+
License-Expression: MIT
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Project-URL: Documentation, https://github.com/rufrozen/polars-list-math/tree/main/docs
|
|
25
|
+
Project-URL: Homepage, https://github.com/rufrozen/polars-list-math
|
|
26
|
+
Project-URL: Issues, https://github.com/rufrozen/polars-list-math/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/rufrozen/polars-list-math
|
|
28
|
+
|
|
29
|
+
# polars-list-math
|
|
30
|
+
|
|
31
|
+
`polars-list-math` is a Python/Rust package with list-oriented expression
|
|
32
|
+
helpers for Polars.
|
|
33
|
+
|
|
34
|
+
Import the package once to register extra methods on `Expr.list`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import polars as pl
|
|
38
|
+
import polars_list_math # noqa: F401
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install polars-list-math
|
|
45
|
+
# or
|
|
46
|
+
uv add polars-list-math
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.12+ and `polars>=1.39.3`.
|
|
50
|
+
|
|
51
|
+
## Methods
|
|
52
|
+
|
|
53
|
+
| Method | Result | Docs |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `Expr.list.zip(...)` | Zip lists into `list[struct]` | [Polars proposal](https://github.com/pola-rs/polars/issues/22719) |
|
|
56
|
+
| `Expr.list.combinations(...)` | Pair each item with itself and later items in the same list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
|
|
57
|
+
| `Expr.list.combinations_to(...)` | Pair each item with each item from another list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
|
|
58
|
+
| `Expr.list.similarity(...)` | Weighted similarity between two lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
59
|
+
| `Expr.list.mean_similarity(...)` | Mean similarity inside a nested-list row | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
60
|
+
| `Expr.list.mean_similarity_to(...)` | Mean similarity to reference nested lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
61
|
+
|
|
62
|
+
The Python helper `py_list_similarity(...)` computes the same weighted
|
|
63
|
+
similarity for plain Python sequences.
|
|
64
|
+
|
|
65
|
+
If a future Polars release ships native methods with the same names, this
|
|
66
|
+
package leaves the native implementation untouched.
|
|
67
|
+
|
|
68
|
+
## Quick Examples
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
df = pl.DataFrame(
|
|
72
|
+
{
|
|
73
|
+
"a": [[1, 2, 3]],
|
|
74
|
+
"b": [[2, 1, 3]],
|
|
75
|
+
"groups": [[[1, 2, 3], [1, 2, 3], [4, 5, 6]]],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
df.with_columns(
|
|
80
|
+
pl.col("a").list.similarity("b").alias("similarity"),
|
|
81
|
+
pl.col("groups").list.mean_similarity().alias("mean_similarity"),
|
|
82
|
+
pl.col("a").list.combinations().alias("pairs"),
|
|
83
|
+
pl.col("a").list.zip("b", fields=["a", "b"]).alias("zipped"),
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
make install
|
|
91
|
+
make develop
|
|
92
|
+
make test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Build and check the package locally:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
make check-dist
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Release steps are in
|
|
102
|
+
[docs/publishing.md](https://github.com/rufrozen/polars-list-math/blob/main/docs/publishing.md).
|
|
103
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# polars-list-math
|
|
2
|
+
|
|
3
|
+
`polars-list-math` is a Python/Rust package with list-oriented expression
|
|
4
|
+
helpers for Polars.
|
|
5
|
+
|
|
6
|
+
Import the package once to register extra methods on `Expr.list`:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
import polars as pl
|
|
10
|
+
import polars_list_math # noqa: F401
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install polars-list-math
|
|
17
|
+
# or
|
|
18
|
+
uv add polars-list-math
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.12+ and `polars>=1.39.3`.
|
|
22
|
+
|
|
23
|
+
## Methods
|
|
24
|
+
|
|
25
|
+
| Method | Result | Docs |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `Expr.list.zip(...)` | Zip lists into `list[struct]` | [Polars proposal](https://github.com/pola-rs/polars/issues/22719) |
|
|
28
|
+
| `Expr.list.combinations(...)` | Pair each item with itself and later items in the same list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
|
|
29
|
+
| `Expr.list.combinations_to(...)` | Pair each item with each item from another list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
|
|
30
|
+
| `Expr.list.similarity(...)` | Weighted similarity between two lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
31
|
+
| `Expr.list.mean_similarity(...)` | Mean similarity inside a nested-list row | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
32
|
+
| `Expr.list.mean_similarity_to(...)` | Mean similarity to reference nested lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
|
|
33
|
+
|
|
34
|
+
The Python helper `py_list_similarity(...)` computes the same weighted
|
|
35
|
+
similarity for plain Python sequences.
|
|
36
|
+
|
|
37
|
+
If a future Polars release ships native methods with the same names, this
|
|
38
|
+
package leaves the native implementation untouched.
|
|
39
|
+
|
|
40
|
+
## Quick Examples
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
df = pl.DataFrame(
|
|
44
|
+
{
|
|
45
|
+
"a": [[1, 2, 3]],
|
|
46
|
+
"b": [[2, 1, 3]],
|
|
47
|
+
"groups": [[[1, 2, 3], [1, 2, 3], [4, 5, 6]]],
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
df.with_columns(
|
|
52
|
+
pl.col("a").list.similarity("b").alias("similarity"),
|
|
53
|
+
pl.col("groups").list.mean_similarity().alias("mean_similarity"),
|
|
54
|
+
pl.col("a").list.combinations().alias("pairs"),
|
|
55
|
+
pl.col("a").list.zip("b", fields=["a", "b"]).alias("zipped"),
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
make install
|
|
63
|
+
make develop
|
|
64
|
+
make test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Build and check the package locally:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
make check-dist
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Release steps are in
|
|
74
|
+
[docs/publishing.md](https://github.com/rufrozen/polars-list-math/blob/main/docs/publishing.md).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._list_combinations import install as _install_list_combinations
|
|
4
|
+
from ._list_mean_similarity import install as _install_list_mean_similarity
|
|
5
|
+
from ._list_similarity import (
|
|
6
|
+
install as _install_list_similarity,
|
|
7
|
+
)
|
|
8
|
+
from ._list_similarity import (
|
|
9
|
+
py_list_similarity,
|
|
10
|
+
)
|
|
11
|
+
from ._list_zip import install as _install_list_zip
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"install",
|
|
15
|
+
"py_list_similarity",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def install(*, overwrite: bool = False) -> None:
|
|
20
|
+
_install_list_combinations(overwrite=overwrite)
|
|
21
|
+
_install_list_zip(overwrite=overwrite)
|
|
22
|
+
_install_list_similarity(overwrite=overwrite)
|
|
23
|
+
_install_list_mean_similarity(overwrite=overwrite)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
install()
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
from polars._utils.wrap import wrap_expr
|
|
10
|
+
from polars.plugins import register_plugin_function
|
|
11
|
+
|
|
12
|
+
IntoExpr = Any
|
|
13
|
+
|
|
14
|
+
_INPUT_PREFIX = "__polars_list_math_list_combinations_input_"
|
|
15
|
+
_LIB = Path(__file__).parent
|
|
16
|
+
_NATIVE_EXTENSIONS = {".so", ".pyd", ".dll", ".dylib"}
|
|
17
|
+
_DEFAULT_LEFT_INDEX = "left_index"
|
|
18
|
+
_DEFAULT_RIGHT_INDEX = "right_index"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class _CombinationFields:
|
|
23
|
+
include_index: bool
|
|
24
|
+
left_index: str
|
|
25
|
+
right_index: str
|
|
26
|
+
left_value: str
|
|
27
|
+
right_value: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_combinations(
|
|
31
|
+
expr: IntoExpr,
|
|
32
|
+
*,
|
|
33
|
+
left_value: str = "left_value",
|
|
34
|
+
right_value: str = "right_value",
|
|
35
|
+
with_index: bool = False,
|
|
36
|
+
left_index: str | None = None,
|
|
37
|
+
right_index: str | None = None,
|
|
38
|
+
skip_null: bool = False,
|
|
39
|
+
) -> pl.Expr:
|
|
40
|
+
"""Build a list expression with all ``i <= j`` pairs from each input list."""
|
|
41
|
+
fields = _normalize_fields(left_index, right_index, left_value, right_value, with_index)
|
|
42
|
+
_validate_skip_null(skip_null)
|
|
43
|
+
if _native_library_available():
|
|
44
|
+
return register_plugin_function(
|
|
45
|
+
plugin_path=_LIB,
|
|
46
|
+
function_name="list_combinations",
|
|
47
|
+
args=[expr],
|
|
48
|
+
kwargs=_kwargs(fields, with_index, skip_null),
|
|
49
|
+
is_elementwise=True,
|
|
50
|
+
use_abs_path=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parsed_expr = _parse_into_expr(expr)
|
|
54
|
+
|
|
55
|
+
return parsed_expr.map_elements(
|
|
56
|
+
lambda value: _combinations_row(value, fields, skip_null),
|
|
57
|
+
return_dtype=None,
|
|
58
|
+
skip_nulls=False,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_combinations_to(
|
|
63
|
+
expr: IntoExpr,
|
|
64
|
+
target: IntoExpr,
|
|
65
|
+
*,
|
|
66
|
+
left_value: str = "left_value",
|
|
67
|
+
right_value: str = "right_value",
|
|
68
|
+
with_index: bool = False,
|
|
69
|
+
left_index: str | None = None,
|
|
70
|
+
right_index: str | None = None,
|
|
71
|
+
skip_null: bool = False,
|
|
72
|
+
) -> pl.Expr:
|
|
73
|
+
"""Build a list expression with all pairs from the input list to a target list."""
|
|
74
|
+
fields = _normalize_fields(left_index, right_index, left_value, right_value, with_index)
|
|
75
|
+
_validate_skip_null(skip_null)
|
|
76
|
+
if _native_library_available():
|
|
77
|
+
return register_plugin_function(
|
|
78
|
+
plugin_path=_LIB,
|
|
79
|
+
function_name="list_combinations_to",
|
|
80
|
+
args=[expr, target],
|
|
81
|
+
kwargs=_kwargs(fields, with_index, skip_null),
|
|
82
|
+
is_elementwise=True,
|
|
83
|
+
use_abs_path=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
parsed_exprs = [_parse_into_expr(expr), _parse_into_expr(target)]
|
|
87
|
+
aliases = [f"{_INPUT_PREFIX}{index}" for index in range(2)]
|
|
88
|
+
row_expr = pl.struct(
|
|
89
|
+
parsed.alias(alias) for parsed, alias in zip(parsed_exprs, aliases, strict=True)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def combinations_to_row(row: Any) -> list[dict[str, Any]] | None:
|
|
93
|
+
left, right = _row_values(row, aliases)
|
|
94
|
+
return _combinations_to_row(left, right, fields, skip_null)
|
|
95
|
+
|
|
96
|
+
return row_expr.map_elements(
|
|
97
|
+
combinations_to_row,
|
|
98
|
+
return_dtype=None,
|
|
99
|
+
skip_nulls=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def install(*, overwrite: bool = False) -> None:
|
|
104
|
+
"""Register the Polars list namespace helpers."""
|
|
105
|
+
list_namespace = type(pl.col("__polars_list_math_list_combinations_probe__").list)
|
|
106
|
+
|
|
107
|
+
if overwrite or not hasattr(list_namespace, "combinations"):
|
|
108
|
+
list_namespace.combinations = _expr_list_combinations # type: ignore
|
|
109
|
+
if overwrite or not hasattr(list_namespace, "combinations_to"):
|
|
110
|
+
list_namespace.combinations_to = _expr_list_combinations_to # type: ignore
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _expr_list_combinations(
|
|
114
|
+
self: Any,
|
|
115
|
+
*,
|
|
116
|
+
left_value: str = "left_value",
|
|
117
|
+
right_value: str = "right_value",
|
|
118
|
+
with_index: bool = False,
|
|
119
|
+
left_index: str | None = None,
|
|
120
|
+
right_index: str | None = None,
|
|
121
|
+
skip_null: bool = False,
|
|
122
|
+
) -> pl.Expr:
|
|
123
|
+
base_expr = wrap_expr(self._pyexpr)
|
|
124
|
+
return list_combinations(
|
|
125
|
+
base_expr,
|
|
126
|
+
left_value=left_value,
|
|
127
|
+
right_value=right_value,
|
|
128
|
+
with_index=with_index,
|
|
129
|
+
left_index=left_index,
|
|
130
|
+
right_index=right_index,
|
|
131
|
+
skip_null=skip_null,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _expr_list_combinations_to(
|
|
136
|
+
self: Any,
|
|
137
|
+
target: IntoExpr,
|
|
138
|
+
*,
|
|
139
|
+
left_value: str = "left_value",
|
|
140
|
+
right_value: str = "right_value",
|
|
141
|
+
with_index: bool = False,
|
|
142
|
+
left_index: str | None = None,
|
|
143
|
+
right_index: str | None = None,
|
|
144
|
+
skip_null: bool = False,
|
|
145
|
+
) -> pl.Expr:
|
|
146
|
+
base_expr = wrap_expr(self._pyexpr)
|
|
147
|
+
return list_combinations_to(
|
|
148
|
+
base_expr,
|
|
149
|
+
target,
|
|
150
|
+
left_value=left_value,
|
|
151
|
+
right_value=right_value,
|
|
152
|
+
with_index=with_index,
|
|
153
|
+
left_index=left_index,
|
|
154
|
+
right_index=right_index,
|
|
155
|
+
skip_null=skip_null,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _combinations_row(
|
|
160
|
+
value: Any,
|
|
161
|
+
fields: _CombinationFields,
|
|
162
|
+
skip_null: bool,
|
|
163
|
+
) -> list[dict[str, Any]] | None:
|
|
164
|
+
values = _list_or_none(value, "list.combinations")
|
|
165
|
+
if values is None:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
out: list[dict[str, Any]] = []
|
|
169
|
+
for left_index in range(len(values)):
|
|
170
|
+
for right_index in range(left_index, len(values)):
|
|
171
|
+
left_value = values[left_index]
|
|
172
|
+
right_value = values[right_index]
|
|
173
|
+
if skip_null and (left_value is None or right_value is None):
|
|
174
|
+
continue
|
|
175
|
+
out.append(_pair_record(fields, left_index, right_index, left_value, right_value))
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _combinations_to_row(
|
|
180
|
+
left: Any,
|
|
181
|
+
right: Any,
|
|
182
|
+
fields: _CombinationFields,
|
|
183
|
+
skip_null: bool,
|
|
184
|
+
) -> list[dict[str, Any]] | None:
|
|
185
|
+
left_values = _list_or_none(left, "list.combinations_to")
|
|
186
|
+
right_values = _list_or_none(right, "list.combinations_to")
|
|
187
|
+
if left_values is None or right_values is None:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
out: list[dict[str, Any]] = []
|
|
191
|
+
for left_index, left_value in enumerate(left_values):
|
|
192
|
+
for right_index, right_value in enumerate(right_values):
|
|
193
|
+
if skip_null and (left_value is None or right_value is None):
|
|
194
|
+
continue
|
|
195
|
+
out.append(_pair_record(fields, left_index, right_index, left_value, right_value))
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _pair_record(
|
|
200
|
+
fields: _CombinationFields,
|
|
201
|
+
left_index: int,
|
|
202
|
+
right_index: int,
|
|
203
|
+
left_value: Any,
|
|
204
|
+
right_value: Any,
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
out: dict[str, Any] = {}
|
|
207
|
+
if fields.include_index:
|
|
208
|
+
out[fields.left_index] = left_index
|
|
209
|
+
out[fields.right_index] = right_index
|
|
210
|
+
out[fields.left_value] = left_value
|
|
211
|
+
out[fields.right_value] = right_value
|
|
212
|
+
return out
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalize_fields(
|
|
216
|
+
left_index: str | None,
|
|
217
|
+
right_index: str | None,
|
|
218
|
+
left_value: str,
|
|
219
|
+
right_value: str,
|
|
220
|
+
with_index: bool,
|
|
221
|
+
) -> _CombinationFields:
|
|
222
|
+
_validate_with_index(with_index)
|
|
223
|
+
include_index = with_index or left_index is not None or right_index is not None
|
|
224
|
+
resolved_left_index = _DEFAULT_LEFT_INDEX if left_index is None else left_index
|
|
225
|
+
resolved_right_index = _DEFAULT_RIGHT_INDEX if right_index is None else right_index
|
|
226
|
+
|
|
227
|
+
index_names = (resolved_left_index, resolved_right_index) if include_index else ()
|
|
228
|
+
field_names = (*index_names, left_value, right_value)
|
|
229
|
+
if not all(isinstance(field, str) for field in field_names):
|
|
230
|
+
msg = "combination field names must be strings"
|
|
231
|
+
raise TypeError(msg)
|
|
232
|
+
if not (left_index is None or isinstance(left_index, str)) or not (
|
|
233
|
+
right_index is None or isinstance(right_index, str)
|
|
234
|
+
):
|
|
235
|
+
msg = "combination index field names must be strings or None"
|
|
236
|
+
raise TypeError(msg)
|
|
237
|
+
if len(set(field_names)) != len(field_names):
|
|
238
|
+
msg = "combination field names must be unique"
|
|
239
|
+
raise ValueError(msg)
|
|
240
|
+
return _CombinationFields(
|
|
241
|
+
include_index=include_index,
|
|
242
|
+
left_index=resolved_left_index,
|
|
243
|
+
right_index=resolved_right_index,
|
|
244
|
+
left_value=left_value,
|
|
245
|
+
right_value=right_value,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _validate_with_index(with_index: bool) -> None:
|
|
250
|
+
if not isinstance(with_index, bool):
|
|
251
|
+
msg = "with_index must be a bool"
|
|
252
|
+
raise TypeError(msg)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _validate_skip_null(skip_null: bool) -> None:
|
|
256
|
+
if not isinstance(skip_null, bool):
|
|
257
|
+
msg = "skip_null must be a bool"
|
|
258
|
+
raise TypeError(msg)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _kwargs(fields: _CombinationFields, with_index: bool, skip_null: bool) -> dict[str, Any]:
|
|
262
|
+
return {
|
|
263
|
+
"left_index": fields.left_index if fields.include_index else None,
|
|
264
|
+
"right_index": fields.right_index if fields.include_index else None,
|
|
265
|
+
"left_value": fields.left_value,
|
|
266
|
+
"right_value": fields.right_value,
|
|
267
|
+
"with_index": with_index,
|
|
268
|
+
"skip_null": skip_null,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _parse_into_expr(expr: IntoExpr) -> pl.Expr:
|
|
273
|
+
if isinstance(expr, pl.Expr):
|
|
274
|
+
return expr
|
|
275
|
+
if isinstance(expr, str):
|
|
276
|
+
return pl.col(expr)
|
|
277
|
+
return pl.lit(expr)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _list_or_none(value: Any, name: str) -> list[Any] | None:
|
|
281
|
+
if value is None:
|
|
282
|
+
return None
|
|
283
|
+
if isinstance(value, pl.Series):
|
|
284
|
+
return value.to_list()
|
|
285
|
+
if not isinstance(value, list):
|
|
286
|
+
msg = f"{name} expects a List expression"
|
|
287
|
+
raise TypeError(msg)
|
|
288
|
+
return value
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _row_values(row: Any, aliases: list[str]) -> list[Any]:
|
|
292
|
+
if row is None:
|
|
293
|
+
return [None] * len(aliases)
|
|
294
|
+
if isinstance(row, Mapping):
|
|
295
|
+
return [row[alias] for alias in aliases]
|
|
296
|
+
return list(row)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _native_library_available() -> bool:
|
|
300
|
+
return any(path.is_file() and path.suffix in _NATIVE_EXTENSIONS for path in _LIB.iterdir())
|