pyochain 0.5.3__py3-none-any.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.
- pyochain/__init__.py +5 -0
- pyochain/_core/__init__.py +23 -0
- pyochain/_core/_format.py +34 -0
- pyochain/_core/_main.py +205 -0
- pyochain/_core/_protocols.py +38 -0
- pyochain/_dict/__init__.py +3 -0
- pyochain/_dict/_filters.py +268 -0
- pyochain/_dict/_groups.py +175 -0
- pyochain/_dict/_iter.py +135 -0
- pyochain/_dict/_joins.py +139 -0
- pyochain/_dict/_main.py +113 -0
- pyochain/_dict/_maps.py +142 -0
- pyochain/_dict/_nested.py +272 -0
- pyochain/_dict/_process.py +204 -0
- pyochain/_iter/__init__.py +3 -0
- pyochain/_iter/_aggregations.py +324 -0
- pyochain/_iter/_booleans.py +227 -0
- pyochain/_iter/_dicts.py +243 -0
- pyochain/_iter/_eager.py +233 -0
- pyochain/_iter/_filters.py +510 -0
- pyochain/_iter/_joins.py +404 -0
- pyochain/_iter/_lists.py +308 -0
- pyochain/_iter/_main.py +466 -0
- pyochain/_iter/_maps.py +360 -0
- pyochain/_iter/_partitions.py +145 -0
- pyochain/_iter/_process.py +366 -0
- pyochain/_iter/_rolling.py +241 -0
- pyochain/_iter/_tuples.py +326 -0
- pyochain/py.typed +0 -0
- pyochain-0.5.3.dist-info/METADATA +261 -0
- pyochain-0.5.3.dist-info/RECORD +32 -0
- pyochain-0.5.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
7
|
+
|
|
8
|
+
import cytoolz as cz
|
|
9
|
+
import more_itertools as mit
|
|
10
|
+
|
|
11
|
+
from .._core import IterWrapper
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ._main import Iter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseTuples[T](IterWrapper[T]):
|
|
18
|
+
def enumerate(self) -> Iter[tuple[int, T]]:
|
|
19
|
+
"""
|
|
20
|
+
Return a Iter of (index, value) pairs.
|
|
21
|
+
```python
|
|
22
|
+
>>> import pyochain as pc
|
|
23
|
+
>>> pc.Iter.from_(["a", "b"]).enumerate().into(list)
|
|
24
|
+
[(0, 'a'), (1, 'b')]
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
return self._lazy(enumerate)
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def combinations(self, r: Literal[2]) -> Iter[tuple[T, T]]: ...
|
|
32
|
+
@overload
|
|
33
|
+
def combinations(self, r: Literal[3]) -> Iter[tuple[T, T, T]]: ...
|
|
34
|
+
@overload
|
|
35
|
+
def combinations(self, r: Literal[4]) -> Iter[tuple[T, T, T, T]]: ...
|
|
36
|
+
@overload
|
|
37
|
+
def combinations(self, r: Literal[5]) -> Iter[tuple[T, T, T, T, T]]: ...
|
|
38
|
+
def combinations(self, r: int) -> Iter[tuple[T, ...]]:
|
|
39
|
+
"""
|
|
40
|
+
Return all combinations of length r.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
r: Length of each combination.
|
|
44
|
+
```python
|
|
45
|
+
>>> import pyochain as pc
|
|
46
|
+
>>> pc.Iter.from_([1, 2, 3]).combinations(2).into(list)
|
|
47
|
+
[(1, 2), (1, 3), (2, 3)]
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
"""
|
|
51
|
+
return self._lazy(itertools.combinations, r)
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def permutations(self, r: Literal[2]) -> Iter[tuple[T, T]]: ...
|
|
55
|
+
@overload
|
|
56
|
+
def permutations(self, r: Literal[3]) -> Iter[tuple[T, T, T]]: ...
|
|
57
|
+
@overload
|
|
58
|
+
def permutations(self, r: Literal[4]) -> Iter[tuple[T, T, T, T]]: ...
|
|
59
|
+
@overload
|
|
60
|
+
def permutations(self, r: Literal[5]) -> Iter[tuple[T, T, T, T, T]]: ...
|
|
61
|
+
def permutations(self, r: int | None = None) -> Iter[tuple[T, ...]]:
|
|
62
|
+
"""
|
|
63
|
+
Return all permutations of length r.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
r: Length of each permutation. Defaults to the length of the iterable.
|
|
67
|
+
```python
|
|
68
|
+
>>> import pyochain as pc
|
|
69
|
+
>>> pc.Iter.from_([1, 2, 3]).permutations(2).into(list)
|
|
70
|
+
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
return self._lazy(itertools.permutations, r)
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def combinations_with_replacement(self, r: Literal[2]) -> Iter[tuple[T, T]]: ...
|
|
78
|
+
@overload
|
|
79
|
+
def combinations_with_replacement(self, r: Literal[3]) -> Iter[tuple[T, T, T]]: ...
|
|
80
|
+
@overload
|
|
81
|
+
def combinations_with_replacement(
|
|
82
|
+
self, r: Literal[4]
|
|
83
|
+
) -> Iter[tuple[T, T, T, T]]: ...
|
|
84
|
+
@overload
|
|
85
|
+
def combinations_with_replacement(
|
|
86
|
+
self, r: Literal[5]
|
|
87
|
+
) -> Iter[tuple[T, T, T, T, T]]: ...
|
|
88
|
+
def combinations_with_replacement(self, r: int) -> Iter[tuple[T, ...]]:
|
|
89
|
+
"""
|
|
90
|
+
Return all combinations with replacement of length r.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
r: Length of each combination.
|
|
94
|
+
```python
|
|
95
|
+
>>> import pyochain as pc
|
|
96
|
+
>>> pc.Iter.from_([1, 2, 3]).combinations_with_replacement(2).into(list)
|
|
97
|
+
[(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
"""
|
|
101
|
+
return self._lazy(itertools.combinations_with_replacement, r)
|
|
102
|
+
|
|
103
|
+
def pairwise(self) -> Iter[tuple[T, T]]:
|
|
104
|
+
"""
|
|
105
|
+
Return an iterator over pairs of consecutive elements.
|
|
106
|
+
```python
|
|
107
|
+
>>> import pyochain as pc
|
|
108
|
+
>>> pc.Iter.from_([1, 2, 3]).pairwise().into(list)
|
|
109
|
+
[(1, 2), (2, 3)]
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
"""
|
|
113
|
+
return self._lazy(itertools.pairwise)
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def map_juxt[R1, R2](
|
|
117
|
+
self,
|
|
118
|
+
func1: Callable[[T], R1],
|
|
119
|
+
func2: Callable[[T], R2],
|
|
120
|
+
/,
|
|
121
|
+
) -> Iter[tuple[R1, R2]]: ...
|
|
122
|
+
@overload
|
|
123
|
+
def map_juxt[R1, R2, R3](
|
|
124
|
+
self,
|
|
125
|
+
func1: Callable[[T], R1],
|
|
126
|
+
func2: Callable[[T], R2],
|
|
127
|
+
func3: Callable[[T], R3],
|
|
128
|
+
/,
|
|
129
|
+
) -> Iter[tuple[R1, R2, R3]]: ...
|
|
130
|
+
@overload
|
|
131
|
+
def map_juxt[R1, R2, R3, R4](
|
|
132
|
+
self,
|
|
133
|
+
func1: Callable[[T], R1],
|
|
134
|
+
func2: Callable[[T], R2],
|
|
135
|
+
func3: Callable[[T], R3],
|
|
136
|
+
func4: Callable[[T], R4],
|
|
137
|
+
/,
|
|
138
|
+
) -> Iter[tuple[R1, R2, R3, R4]]: ...
|
|
139
|
+
def map_juxt(self, *funcs: Callable[[T], object]) -> Iter[tuple[object, ...]]:
|
|
140
|
+
"""
|
|
141
|
+
Apply several functions to each item.
|
|
142
|
+
|
|
143
|
+
Returns a new Iter where each item is a tuple of the results of applying each function to the original item.
|
|
144
|
+
```python
|
|
145
|
+
>>> import pyochain as pc
|
|
146
|
+
>>> def is_even(n: int) -> bool:
|
|
147
|
+
... return n % 2 == 0
|
|
148
|
+
>>> def is_positive(n: int) -> bool:
|
|
149
|
+
... return n > 0
|
|
150
|
+
>>>
|
|
151
|
+
>>> pc.Iter.from_([1, -2, 3]).map_juxt(is_even, is_positive).into(list)
|
|
152
|
+
[(False, True), (True, False), (False, True)]
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
return self._lazy(partial(map, cz.functoolz.juxt(*funcs)))
|
|
157
|
+
|
|
158
|
+
def adjacent(
|
|
159
|
+
self, predicate: Callable[[T], bool], distance: int = 1
|
|
160
|
+
) -> Iter[tuple[bool, T]]:
|
|
161
|
+
"""
|
|
162
|
+
Return an iterable over (bool, item) tuples.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
predicate: Function to determine if an item satisfies the condition.
|
|
166
|
+
distance: Number of places to consider as adjacent. Defaults to 1.
|
|
167
|
+
|
|
168
|
+
The output is a sequence of tuples where the item is drawn from iterable.
|
|
169
|
+
|
|
170
|
+
The bool indicates whether that item satisfies the predicate or is adjacent to an item that does.
|
|
171
|
+
|
|
172
|
+
For example, to find whether items are adjacent to a 3:
|
|
173
|
+
```python
|
|
174
|
+
>>> import pyochain as pc
|
|
175
|
+
>>> pc.Iter.from_(range(6)).adjacent(lambda x: x == 3).into(list)
|
|
176
|
+
[(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)]
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Set distance to change what counts as adjacent.
|
|
180
|
+
For example, to find whether items are two places away from a 3:
|
|
181
|
+
```python
|
|
182
|
+
>>> pc.Iter.from_(range(6)).adjacent(lambda x: x == 3, distance=2).into(list)
|
|
183
|
+
[(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)]
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This is useful for contextualizing the results of a search function.
|
|
188
|
+
|
|
189
|
+
For example, a code comparison tool might want to identify lines that have changed, but also surrounding lines to give the viewer of the diff context.
|
|
190
|
+
|
|
191
|
+
The predicate function will only be called once for each item in the iterable.
|
|
192
|
+
|
|
193
|
+
See also groupby_transform, which can be used with this function to group ranges of items with the same bool value.
|
|
194
|
+
"""
|
|
195
|
+
return self._lazy(partial(mit.adjacent, predicate, distance=distance))
|
|
196
|
+
|
|
197
|
+
def classify_unique(self) -> Iter[tuple[T, bool, bool]]:
|
|
198
|
+
"""
|
|
199
|
+
Classify each element in terms of its uniqueness.\n
|
|
200
|
+
For each element in the input iterable, return a 3-tuple consisting of:
|
|
201
|
+
|
|
202
|
+
- The element itself
|
|
203
|
+
- False if the element is equal to the one preceding it in the input, True otherwise (i.e. the equivalent of unique_justseen)
|
|
204
|
+
- False if this element has been seen anywhere in the input before, True otherwise (i.e. the equivalent of unique_everseen)
|
|
205
|
+
|
|
206
|
+
This function is analogous to unique_everseen and is subject to the same performance considerations.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
>>> import pyochain as pc
|
|
210
|
+
>>> pc.Iter.from_("otto").classify_unique().into(list)
|
|
211
|
+
... # doctest: +NORMALIZE_WHITESPACE
|
|
212
|
+
[('o', True, True),
|
|
213
|
+
('t', True, True),
|
|
214
|
+
('t', False, False),
|
|
215
|
+
('o', True, False)]
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
"""
|
|
219
|
+
return self._lazy(mit.classify_unique)
|
|
220
|
+
|
|
221
|
+
@overload
|
|
222
|
+
def group_by_transform(
|
|
223
|
+
self,
|
|
224
|
+
keyfunc: None = None,
|
|
225
|
+
valuefunc: None = None,
|
|
226
|
+
reducefunc: None = None,
|
|
227
|
+
) -> Iter[tuple[T, Iterator[T]]]: ...
|
|
228
|
+
@overload
|
|
229
|
+
def group_by_transform[U](
|
|
230
|
+
self,
|
|
231
|
+
keyfunc: Callable[[T], U],
|
|
232
|
+
valuefunc: None,
|
|
233
|
+
reducefunc: None,
|
|
234
|
+
) -> Iter[tuple[U, Iterator[T]]]: ...
|
|
235
|
+
@overload
|
|
236
|
+
def group_by_transform[V](
|
|
237
|
+
self,
|
|
238
|
+
keyfunc: None,
|
|
239
|
+
valuefunc: Callable[[T], V],
|
|
240
|
+
reducefunc: None,
|
|
241
|
+
) -> Iter[tuple[T, Iterator[V]]]: ...
|
|
242
|
+
@overload
|
|
243
|
+
def group_by_transform[U, V](
|
|
244
|
+
self,
|
|
245
|
+
keyfunc: Callable[[T], U],
|
|
246
|
+
valuefunc: Callable[[T], V],
|
|
247
|
+
reducefunc: None,
|
|
248
|
+
) -> Iter[tuple[U, Iterator[V]]]: ...
|
|
249
|
+
@overload
|
|
250
|
+
def group_by_transform[W](
|
|
251
|
+
self,
|
|
252
|
+
keyfunc: None,
|
|
253
|
+
valuefunc: None,
|
|
254
|
+
reducefunc: Callable[[Iterator[T]], W],
|
|
255
|
+
) -> Iter[tuple[T, W]]: ...
|
|
256
|
+
@overload
|
|
257
|
+
def group_by_transform[U, W](
|
|
258
|
+
self,
|
|
259
|
+
keyfunc: Callable[[T], U],
|
|
260
|
+
valuefunc: None,
|
|
261
|
+
reducefunc: Callable[[Iterator[T]], W],
|
|
262
|
+
) -> Iter[tuple[U, W]]: ...
|
|
263
|
+
@overload
|
|
264
|
+
def group_by_transform[V, W](
|
|
265
|
+
self,
|
|
266
|
+
keyfunc: None,
|
|
267
|
+
valuefunc: Callable[[T], V],
|
|
268
|
+
reducefunc: Callable[[Iterator[V]], W],
|
|
269
|
+
) -> Iter[tuple[T, W]]: ...
|
|
270
|
+
@overload
|
|
271
|
+
def group_by_transform[U, V, W](
|
|
272
|
+
self,
|
|
273
|
+
keyfunc: Callable[[T], U],
|
|
274
|
+
valuefunc: Callable[[T], V],
|
|
275
|
+
reducefunc: Callable[[Iterator[V]], W],
|
|
276
|
+
) -> Iter[tuple[U, W]]: ...
|
|
277
|
+
def group_by_transform[U, V](
|
|
278
|
+
self,
|
|
279
|
+
keyfunc: Callable[[T], U] | None = None,
|
|
280
|
+
valuefunc: Callable[[T], V] | None = None,
|
|
281
|
+
reducefunc: Any = None,
|
|
282
|
+
) -> Iter[tuple[Any, ...]]:
|
|
283
|
+
"""
|
|
284
|
+
An extension of ``Iter.groupby`` that can apply transformations to the grouped data.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
keyfunc: Function to compute the key for grouping. Defaults to None.
|
|
288
|
+
valuefunc: Function to transform individual items after grouping. Defaults to None.
|
|
289
|
+
reducefunc: Function to transform each group of items. Defaults to None.
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
```python
|
|
293
|
+
>>> import pyochain as pc
|
|
294
|
+
>>> data = pc.Iter.from_("aAAbBBcCC")
|
|
295
|
+
>>> data.group_by_transform(
|
|
296
|
+
... lambda k: k.upper(), lambda v: v.lower(), lambda g: "".join(g)
|
|
297
|
+
... ).into(list)
|
|
298
|
+
[('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')]
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
Each optional argument defaults to an identity function if not specified.
|
|
302
|
+
|
|
303
|
+
group_by_transform is useful when grouping elements of an iterable using a separate iterable as the key.
|
|
304
|
+
|
|
305
|
+
To do this, zip the iterables and pass a keyfunc that extracts the first element and a valuefunc that extracts the second element:
|
|
306
|
+
|
|
307
|
+
Note that the order of items in the iterable is significant.
|
|
308
|
+
|
|
309
|
+
Only adjacent items are grouped together, so if you don't want any duplicate groups, you should sort the iterable by the key function.
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
```python
|
|
313
|
+
>>> from operator import itemgetter
|
|
314
|
+
>>> data = pc.Iter.from_([0, 0, 1, 1, 1, 2, 2, 2, 3])
|
|
315
|
+
>>> data.zip("abcdefghi").group_by_transform(itemgetter(0), itemgetter(1)).map(
|
|
316
|
+
... lambda kv: (kv[0], "".join(kv[1]))
|
|
317
|
+
... ).into(list)
|
|
318
|
+
[(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')]
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def _group_by_transform(data: Iterable[T]) -> Iterator[tuple[Any, ...]]:
|
|
324
|
+
return mit.groupby_transform(data, keyfunc, valuefunc, reducefunc)
|
|
325
|
+
|
|
326
|
+
return self._lazy(_group_by_transform)
|
pyochain/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyochain
|
|
3
|
+
Version: 0.5.3
|
|
4
|
+
Summary: Method chaining for iterables and dictionaries in Python.
|
|
5
|
+
Requires-Dist: cytoolz>=1.0.1
|
|
6
|
+
Requires-Dist: more-itertools>=10.8.0
|
|
7
|
+
Requires-Dist: rolling>=0.5.0
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# pyochain ⛓️
|
|
12
|
+
|
|
13
|
+
**_Functional-style method chaining for Python data structures._**
|
|
14
|
+
|
|
15
|
+
`pyochain` brings a fluent, declarative API inspired by Rust's `Iterator` and DataFrame libraries like Polars to your everyday Python iterables and dictionaries.
|
|
16
|
+
|
|
17
|
+
Manipulate data through composable chains of operations, enhancing readability and reducing boilerplate.
|
|
18
|
+
|
|
19
|
+
## Notice on Stability ⚠️
|
|
20
|
+
|
|
21
|
+
`pyochain` is currently in early development (< 1.0), and the API may undergo significant changes multiple times before reaching a stable 1.0 release.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add pyochain
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API Reference 📖
|
|
30
|
+
|
|
31
|
+
The full API reference can be found at:
|
|
32
|
+
<https://outsquarecapital.github.io/pyochain/>
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
### Philosophy
|
|
37
|
+
|
|
38
|
+
* **Declarative over Imperative:** Replace explicit `for` and `while` loops with sequences of high-level operations (map, filter, group, join...).
|
|
39
|
+
* **Fluent Chaining:** Each method transforms the data and returns a new wrapper instance, allowing for seamless chaining.
|
|
40
|
+
* **Lazy and Eager:** `Iter` operates lazily for efficiency on large or infinite sequences, while `Seq` represents materialized sequences for eager operations.
|
|
41
|
+
* **100% Type-safe:** Extensive use of generics and overloads ensures type safety and improves developer experience.
|
|
42
|
+
* **Documentation-first:** Each method is thoroughly documented with clear explanations, and usage examples. Before any commit is made, each docstring is automatically tested to ensure accuracy. This also allows for a convenient experience in IDEs, where developers can easily access documentation with a simple hover of the mouse.
|
|
43
|
+
* **Functional paradigm:** Design encourages building complex data transformations by composing simple, reusable functions on known buildings blocks, rather than implementing customs classes each time.
|
|
44
|
+
|
|
45
|
+
### Inspirations
|
|
46
|
+
|
|
47
|
+
* **Rust's language and Rust `Iterator` Trait:** Emulate naming conventions (`from_()`, `into()`) and leverage concepts from Rust's powerful iterator traits (method chaining, lazy evaluation) to bring similar expressiveness to Python.
|
|
48
|
+
* **Python iterators libraries:** Libraries like `rolling`, `cytoolz`, and `more-itertools` provided ideas, inspiration, and implementations for many of the iterator methods.
|
|
49
|
+
* **PyFunctional:** Although not directly used (because I started writing pyochain before discovering it), also shares similar goals and ideas.
|
|
50
|
+
|
|
51
|
+
### Core Components
|
|
52
|
+
|
|
53
|
+
#### `Iter[T]`
|
|
54
|
+
|
|
55
|
+
To instantiate it, wrap a Python `Iterator` or `Generator`, or take any Iterable (`list`, `tuple`, etc...) and call Iter.from_ (which will call the builtin `iter()` on it).
|
|
56
|
+
|
|
57
|
+
All operations that return a new `Iter` are **lazy**, consuming the underlying iterator on demand.
|
|
58
|
+
|
|
59
|
+
Provides a vast array of methods for transformation, filtering, aggregation, joining, etc..
|
|
60
|
+
|
|
61
|
+
#### `Seq[T]`
|
|
62
|
+
|
|
63
|
+
Wraps a Python `Sequence` (`list`, `tuple`...), and represents **eagerly** evaluated data.
|
|
64
|
+
|
|
65
|
+
Exposes a subset of the `Iter` methods who operate on the full dataset (e.g., `sort`, `union`) or who aggregate it.
|
|
66
|
+
|
|
67
|
+
It is most useful when you need to reuse the data multiple times without re-iterating it.
|
|
68
|
+
|
|
69
|
+
Use `.iter()` to switch back to lazy processing.
|
|
70
|
+
|
|
71
|
+
#### `Dict[K, V]`
|
|
72
|
+
|
|
73
|
+
Wraps a Python `dict` (or any Mapping via ``Dict.from_``) and provides chainable methods specific to dictionaries (manipulating keys, values, items, nesting, joins, grouping).
|
|
74
|
+
|
|
75
|
+
Promote immutability by returning new `Dict` instances on each operation, and avoiding in-place modifications.
|
|
76
|
+
|
|
77
|
+
Can work easily on known data structure (e.g `dict[str, int]`), with methods like `map_values`, `filter_keys`, etc., who works on the whole `dict` in a performant way, mostly thanks to `cytoolz` functions.
|
|
78
|
+
|
|
79
|
+
But `Dict` can work also as well as on "irregular" structures (e.g., `dict[Any, Any]`, TypedDict, etc..), by providing a set of utilities for working with nested data, including:
|
|
80
|
+
|
|
81
|
+
* `pluck` to extract multiple fields at once.
|
|
82
|
+
* `flatten` to collapse nested structures into a single level.
|
|
83
|
+
|
|
84
|
+
#### `Wrapper[T]`
|
|
85
|
+
|
|
86
|
+
A generic wrapper for any Python object, allowing integration into `pyochain`'s fluent style using `pipe`, `apply`, and `into`.
|
|
87
|
+
|
|
88
|
+
Can be for example used to wrap numpy arrays, json outputs from requests, or any custom class instance, as a way to integrate them into a chain of operations, rather than breaking the chain to reference intermediate variables.
|
|
89
|
+
|
|
90
|
+
### Core Piping Methods
|
|
91
|
+
|
|
92
|
+
All wrappers inherit from `CommonBase`:
|
|
93
|
+
|
|
94
|
+
* `into[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> R`
|
|
95
|
+
Passes the **unwrapped** data to `func` and returns the raw result (terminal).
|
|
96
|
+
* `apply[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> "CurrentWrapper"[R]`
|
|
97
|
+
Passes the **unwrapped** data to`func` and **re-wraps** the result for continued chaining.
|
|
98
|
+
* `pipe[**P, R](func: Callable[Concatenate[Self, P]], *args: P.args, **kwargs: P.kwargs) -> R`
|
|
99
|
+
Passes the **wrapped instance** (`self`) to `func` and returns the raw result (can be terminal).
|
|
100
|
+
* `println()`
|
|
101
|
+
Prints the unwrapped data and returns `self`.
|
|
102
|
+
|
|
103
|
+
### Rich Lazy Iteration (`Iter`)
|
|
104
|
+
|
|
105
|
+
Leverage dozens of methods inspired by Rust's `Iterator`, `itertools`, `cytoolz`, and `more-itertools`.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import pyochain as pc
|
|
109
|
+
|
|
110
|
+
result = (
|
|
111
|
+
pc.Iter.from_count(1) # Infinite iterator: 1, 2, 3, ...
|
|
112
|
+
.filter(lambda x: x % 2 != 0) # Keep odd numbers: 1, 3, 5, ...
|
|
113
|
+
.map(lambda x: x * x) # Square them: 1, 9, 25, ...
|
|
114
|
+
.take(5) # Take the first 5: 1, 9, 25, 49, 81
|
|
115
|
+
.into(list) # Consume into a list
|
|
116
|
+
)
|
|
117
|
+
# result: [1, 9, 25, 49, 81]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Typing enforcement
|
|
121
|
+
|
|
122
|
+
Each method and class make extensive use of generics, type hints, and overloads (when necessary) to ensure type safety and improve developer experience.
|
|
123
|
+
|
|
124
|
+
Since there's much less need for intermediate variables, the developper don't have to annotate them as much, whilst still keeping a type-safe codebase.
|
|
125
|
+
|
|
126
|
+
### Convenience mappers: itr and struct
|
|
127
|
+
|
|
128
|
+
Operate on iterables of iterables or iterables of dicts without leaving the chain.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import pyochain as pc
|
|
132
|
+
|
|
133
|
+
nested = pc.Iter.from_([[1, 2, 3], [4, 5]])
|
|
134
|
+
totals = nested.itr(lambda it: it.sum()).into(list)
|
|
135
|
+
# [6, 9]
|
|
136
|
+
|
|
137
|
+
records = pc.Iter.from_(
|
|
138
|
+
[
|
|
139
|
+
{"name": "Alice", "age": 30},
|
|
140
|
+
{"name": "Bob", "age": 25},
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
names = records.struct(lambda d: d.pluck("name").unwrap()).into(list)
|
|
144
|
+
# ['Alice', 'Bob']
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Key Dependencies and credits
|
|
148
|
+
|
|
149
|
+
Most of the computations are done with implementations from the `cytoolz`, `more-itertools`, and `rolling` libraries.
|
|
150
|
+
|
|
151
|
+
An extensive use of the `itertools` stdlib module is also to be noted.
|
|
152
|
+
|
|
153
|
+
pyochain acts as a unifying API layer over these powerful tools.
|
|
154
|
+
|
|
155
|
+
<https://github.com/pytoolz/cytoolz>
|
|
156
|
+
|
|
157
|
+
<https://github.com/more-itertools/more-itertools>
|
|
158
|
+
|
|
159
|
+
<https://github.com/ajcr/rolling>
|
|
160
|
+
|
|
161
|
+
The stubs used for the developpement, made by the maintainer of pyochain, can be found here:
|
|
162
|
+
|
|
163
|
+
<https://github.com/py-stubs/cytoolz-stubs>
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Real-life simple example
|
|
168
|
+
|
|
169
|
+
In one of my project, I have to introspect some modules from plotly to get some lists of colors.
|
|
170
|
+
|
|
171
|
+
I want to check wether the colors are in hex format or not, and I want to get a dictionary of palettes.
|
|
172
|
+
We can see here that pyochain allow to keep the same style than polars, with method chaining, but for plain python objects.
|
|
173
|
+
|
|
174
|
+
Due to the freedom of python, multiple paradigms are implemented across libraries.
|
|
175
|
+
|
|
176
|
+
If you like the fluent, functional, chainable style, pyochain can help you to keep it across your codebase, rather than mixing object().method().method() and then another where it's [[... for ... in ...] ... ].
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
|
|
180
|
+
from types import ModuleType
|
|
181
|
+
|
|
182
|
+
import polars as pl
|
|
183
|
+
import pyochain as pc
|
|
184
|
+
from plotly.express.colors import cyclical, qualitative, sequential
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
MODULES: set[ModuleType] = {
|
|
189
|
+
sequential,
|
|
190
|
+
cyclical,
|
|
191
|
+
qualitative,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def get_palettes() -> pc.Dict[str, list[str]]:
|
|
195
|
+
clr = "color"
|
|
196
|
+
scl = "scale"
|
|
197
|
+
df: pl.DataFrame = (
|
|
198
|
+
pc.Iter.from_(MODULES)
|
|
199
|
+
.map(
|
|
200
|
+
lambda mod: pc.Dict.from_object(mod)
|
|
201
|
+
.filter_values(lambda v: isinstance(v, list))
|
|
202
|
+
.unwrap()
|
|
203
|
+
)
|
|
204
|
+
.into(pl.LazyFrame)
|
|
205
|
+
.unpivot(value_name=clr, variable_name=scl)
|
|
206
|
+
.drop_nulls()
|
|
207
|
+
.filter(
|
|
208
|
+
pl.col(clr)
|
|
209
|
+
.list.eval(pl.element().first().str.starts_with("#").alias("is_hex"))
|
|
210
|
+
.list.first()
|
|
211
|
+
)
|
|
212
|
+
.sort(scl)
|
|
213
|
+
.collect()
|
|
214
|
+
)
|
|
215
|
+
keys: list[str] = df.get_column(scl).to_list()
|
|
216
|
+
values: list[list[str]] = df.get_column(clr).to_list()
|
|
217
|
+
return pc.Iter.from_(keys).with_values(values)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Ouput excerpt:
|
|
221
|
+
{'mygbm_r': ['#ef55f1',
|
|
222
|
+
'#c543fa',
|
|
223
|
+
'#9139fa',
|
|
224
|
+
'#6324f5',
|
|
225
|
+
'#2e21ea',
|
|
226
|
+
'#284ec8',
|
|
227
|
+
'#3d719a',
|
|
228
|
+
'#439064',
|
|
229
|
+
'#31ac28',
|
|
230
|
+
'#61c10b',
|
|
231
|
+
'#96d310',
|
|
232
|
+
'#c6e516',
|
|
233
|
+
'#f0ed35',
|
|
234
|
+
'#fcd471',
|
|
235
|
+
'#fbafa1',
|
|
236
|
+
'#fb84ce',
|
|
237
|
+
'#ef55f1']}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
However you can still easily go back with for loops when the readability is better this way.
|
|
241
|
+
|
|
242
|
+
In another place, I use this function to generate a Literal from the keys of the palettes.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
|
|
246
|
+
from enum import StrEnum
|
|
247
|
+
|
|
248
|
+
class Text(StrEnum):
|
|
249
|
+
CONTENT = "Palettes = Literal[\n"
|
|
250
|
+
END_CONTENT = "]\n"
|
|
251
|
+
...# rest of the class
|
|
252
|
+
|
|
253
|
+
def generate_palettes_literal() -> None:
|
|
254
|
+
literal_content: str = Text.CONTENT
|
|
255
|
+
for name in get_palettes().iter_keys().sort().unwrap():
|
|
256
|
+
literal_content += f' "{name}",\n'
|
|
257
|
+
literal_content += Text.END_CONTENT
|
|
258
|
+
...# rest of the function
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Since I have to reference the literal_content variable in the for loop, This is more reasonnable to use a for loop here rather than a map + reduce approach.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
pyochain/__init__.py,sha256=IbaRq48Skr5RhltOmUkmwNbffziiFy9JltsLMDYlSEg,126
|
|
2
|
+
pyochain/_core/__init__.py,sha256=tBC30VIS-bRGGRbtT3NmFZ4kJ1Du84AGF9WJ17upJCY,505
|
|
3
|
+
pyochain/_core/_format.py,sha256=7H9sAlLRoUpaOw8gzKso7YAGrtcUs9dTcRvlb2oO6xo,900
|
|
4
|
+
pyochain/_core/_main.py,sha256=8LPnMRJnv8SDz2Q3rmngG1rx9c7fhkgAlqm5BzlUd34,5745
|
|
5
|
+
pyochain/_core/_protocols.py,sha256=UgjOCINuz6cJpkkj7--6S0ULbUudv860HVNYEquBZvM,833
|
|
6
|
+
pyochain/_dict/__init__.py,sha256=z3_hkXG_BrmS63WGjQocu7QXNuWZxeFF5wAnERmobsQ,44
|
|
7
|
+
pyochain/_dict/_filters.py,sha256=a5ohEdwZlmCQwzrCO3KM00-EsSY1wPze-jfdGM1tQLs,8111
|
|
8
|
+
pyochain/_dict/_groups.py,sha256=2QPQsfCV7GvUTEJfpW72EDKXGw5Yvw17Pcp9-X2iBo4,6076
|
|
9
|
+
pyochain/_dict/_iter.py,sha256=y8S6zAFu6A9NK-13f-u0Q3-lt16jE3kuMOl8awn_nnI,3702
|
|
10
|
+
pyochain/_dict/_joins.py,sha256=a6wzbr_xAmne0ohtL5tDpdccJ_89uUKgc3MnbZr39vk,4420
|
|
11
|
+
pyochain/_dict/_main.py,sha256=HNpClIk7mlkTLIU79mfLoRTM8YxaE01IMrXd9q132LI,3066
|
|
12
|
+
pyochain/_dict/_maps.py,sha256=pxwgPvgnRiQ1Kj8vTwQwG1OjVDTu0qKMeQPRFfFwHoM,3941
|
|
13
|
+
pyochain/_dict/_nested.py,sha256=VE2MisvxaOReevNvtyfTMCA_HYXBGnFDl7Tad9i-R68,9286
|
|
14
|
+
pyochain/_dict/_process.py,sha256=g5O_6Xwp_NPt1528KDPJTphyykmu6wYqztBx5dvMHn0,6381
|
|
15
|
+
pyochain/_iter/__init__.py,sha256=a8YS8Yx_UbLXdzM70YQrt6gyv1v7QW_16i2ydsyGGV8,56
|
|
16
|
+
pyochain/_iter/_aggregations.py,sha256=VkAYF9w4GwVBDYx1H5pL2dkMIWfodj3QsZsOc4AchlA,8584
|
|
17
|
+
pyochain/_iter/_booleans.py,sha256=KE4x-lxayHH_recHoX5ZbNz7JVdC9WuvA2ewNBpqUL0,7210
|
|
18
|
+
pyochain/_iter/_dicts.py,sha256=eA6WafYcOrQS-ZrUES2B-yX2HTqewSgvWMl6neqEDk8,7652
|
|
19
|
+
pyochain/_iter/_eager.py,sha256=ARC995qZaEE1v9kiyZNEieM1qJKEXiOUdkIRJTpOkJs,6880
|
|
20
|
+
pyochain/_iter/_filters.py,sha256=_ppvUG-DgEIhqmuyaOB0_g4tbtH_4bNk7vdVpjP8luY,15515
|
|
21
|
+
pyochain/_iter/_joins.py,sha256=ivvnTvfiw67U9kVWMIoy78PJNBwN0oZ4Ko9AyfxyGYM,13043
|
|
22
|
+
pyochain/_iter/_lists.py,sha256=TU-HjyyM_KqJTwA_0V0fCyJHl9L6wsRi1n4Sl8g2Gro,11103
|
|
23
|
+
pyochain/_iter/_main.py,sha256=n9ljv7tL0U58QUpOkwFbesd-9Vd5dwJv-pV0JLmJle8,15047
|
|
24
|
+
pyochain/_iter/_maps.py,sha256=OE9t14gtCyyTBaDXyxE1ZqgHcJRjWCmoUDH6EKnHJm8,11491
|
|
25
|
+
pyochain/_iter/_partitions.py,sha256=MYxlzQRrCBtfjnhtIVdMUhkNq5FCTpFV1R9sJ9LznsM,5095
|
|
26
|
+
pyochain/_iter/_process.py,sha256=P3Zw3uInZkOL-VlDUG4xfTnwek6lIa4j2a3IwxVaLD0,11039
|
|
27
|
+
pyochain/_iter/_rolling.py,sha256=YJ5X23eZTizXEJYneaZvn98zORbvJzLWXP8gX1BCvGY,6979
|
|
28
|
+
pyochain/_iter/_tuples.py,sha256=rcEeqrz3eio1CEYyZ0lt2CC5P_OW7ARLkTL0g7yf3ws,11137
|
|
29
|
+
pyochain/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
pyochain-0.5.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
31
|
+
pyochain-0.5.3.dist-info/METADATA,sha256=-ahwMj7JiG8XTEAQUhzvayLpBus-nHu7baoHTGjjQ_M,9984
|
|
32
|
+
pyochain-0.5.3.dist-info/RECORD,,
|