pyochain 0.5.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.

Potentially problematic release.


This version of pyochain might be problematic. Click here for more details.

Files changed (33) hide show
  1. pyochain-0.5.0/PKG-INFO +295 -0
  2. pyochain-0.5.0/README.md +285 -0
  3. pyochain-0.5.0/pyproject.toml +35 -0
  4. pyochain-0.5.0/src/pyochain/__init__.py +5 -0
  5. pyochain-0.5.0/src/pyochain/_core/__init__.py +21 -0
  6. pyochain-0.5.0/src/pyochain/_core/_main.py +184 -0
  7. pyochain-0.5.0/src/pyochain/_core/_protocols.py +43 -0
  8. pyochain-0.5.0/src/pyochain/_dict/__init__.py +4 -0
  9. pyochain-0.5.0/src/pyochain/_dict/_exprs.py +115 -0
  10. pyochain-0.5.0/src/pyochain/_dict/_filters.py +273 -0
  11. pyochain-0.5.0/src/pyochain/_dict/_funcs.py +62 -0
  12. pyochain-0.5.0/src/pyochain/_dict/_groups.py +176 -0
  13. pyochain-0.5.0/src/pyochain/_dict/_iter.py +92 -0
  14. pyochain-0.5.0/src/pyochain/_dict/_joins.py +137 -0
  15. pyochain-0.5.0/src/pyochain/_dict/_main.py +307 -0
  16. pyochain-0.5.0/src/pyochain/_dict/_nested.py +218 -0
  17. pyochain-0.5.0/src/pyochain/_dict/_process.py +171 -0
  18. pyochain-0.5.0/src/pyochain/_iter/__init__.py +3 -0
  19. pyochain-0.5.0/src/pyochain/_iter/_aggregations.py +323 -0
  20. pyochain-0.5.0/src/pyochain/_iter/_booleans.py +224 -0
  21. pyochain-0.5.0/src/pyochain/_iter/_constructors.py +155 -0
  22. pyochain-0.5.0/src/pyochain/_iter/_eager.py +195 -0
  23. pyochain-0.5.0/src/pyochain/_iter/_filters.py +503 -0
  24. pyochain-0.5.0/src/pyochain/_iter/_groups.py +264 -0
  25. pyochain-0.5.0/src/pyochain/_iter/_joins.py +407 -0
  26. pyochain-0.5.0/src/pyochain/_iter/_lists.py +306 -0
  27. pyochain-0.5.0/src/pyochain/_iter/_main.py +224 -0
  28. pyochain-0.5.0/src/pyochain/_iter/_maps.py +358 -0
  29. pyochain-0.5.0/src/pyochain/_iter/_partitions.py +148 -0
  30. pyochain-0.5.0/src/pyochain/_iter/_process.py +384 -0
  31. pyochain-0.5.0/src/pyochain/_iter/_rolling.py +247 -0
  32. pyochain-0.5.0/src/pyochain/_iter/_tuples.py +221 -0
  33. pyochain-0.5.0/src/pyochain/py.typed +0 -0
@@ -0,0 +1,295 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyochain
3
+ Version: 0.5.0
4
+ Summary: Add your description here
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
+ ## Installation
20
+
21
+ ```bash
22
+ uv add git+https://github.com/OutSquareCapital/pyochain.git
23
+ ```
24
+
25
+ ## API Reference 📖
26
+
27
+ The full API reference can be found at:
28
+ <https://outsquarecapital.github.io/pyochain/>
29
+
30
+ ## Overview
31
+
32
+ ### Philosophy
33
+
34
+ * **Declarative over Imperative:** Replace explicit `for` and `while` loops with sequences of high-level operations (map, filter, group, join...).
35
+ * **Fluent Chaining:** Each method transforms the data and returns a new wrapper instance, allowing for seamless chaining.
36
+ * **Lazy and Eager:** `Iter` operates lazily for efficiency on large or infinite sequences, while `Seq` represents materialized collections for eager operations.
37
+ * **100% Type-safe:** Extensive use of generics and overloads ensures type safety and improves developer experience.
38
+ * **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.
39
+ * **Functional paradigm:** Design encourages building complex data transformations by composing simple, reusable functions on known buildings blocks, rather than implementing customs classes each time.
40
+
41
+ ### Inspirations
42
+
43
+ * **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.
44
+ * **Polars API:** The powerful expression API for `pyochain.Dict` (`select`, `with_fields`, `key`) mimics the expressive power of Polars for selecting, transforming, and reshaping nested dictionary data.
45
+ * **Python iterators libraries:** Libraries like `rolling`, `cytoolz`, and `more-itertools` provided ideas, inspiration, and implementations for many of the iterator methods.
46
+ * **PyFunctional:** Although not directly used (because I started writing pyochain before discovering it), also shares similar goals and ideas.
47
+
48
+ ### Core Components
49
+
50
+ #### `Iter[T]`
51
+
52
+ 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).
53
+
54
+ All operations that return a new `Iter` are **lazy**, consuming the underlying iterator on demand.
55
+
56
+ Provides a vast array of methods for transformation, filtering, aggregation, joining, etc..
57
+
58
+ #### `Seq[T]`
59
+
60
+ Wraps a Python `Collection` (`list`, `tuple`, `set`...), and represents **eagerly** evaluated data.
61
+
62
+ Exposes a subset of the `Iter` methods who operate on the full dataset (e.g., `sort`, `union`) or who aggregate it.
63
+
64
+ It is most useful when you need to reuse the data multiple times without re-iterating it.
65
+
66
+ Use `.iter()` to switch back to lazy processing.
67
+
68
+ #### `Dict[K, V]`
69
+
70
+ Wraps a Python `dict` (or any Mapping via ``Dict.from_``) and provides chainable methods specific to dictionaries (manipulating keys, values, items, nesting, joins, grouping).
71
+
72
+ Promote immutability by returning new `Dict` instances on each operation, and avoiding in-place modifications.
73
+
74
+ 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.
75
+
76
+ 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:
77
+
78
+ * `pluck` to extract multiple fields at once.
79
+ * `flatten` to collapse nested structures into a single level.
80
+ * `schema` to infer the structure of the data by recursively analyzing keys and value types.
81
+ * `pyochain.key` expressions to compute/retrieve/select/create new fields from existing nested data in a declarative way.
82
+
83
+ #### `Wrapper[T]`
84
+
85
+ A generic wrapper for any Python object, allowing integration into `pyochain`'s fluent style using `pipe`, `apply`, and `into`.
86
+
87
+ 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.
88
+
89
+ ### Core Piping Methods
90
+
91
+ All wrappers inherit from `CommonBase`:
92
+
93
+ * `into[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> R`
94
+ Passes the **unwrapped** data to `func` and returns the raw result (terminal).
95
+ * `apply[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> "CurrentWrapper"[R]`
96
+ Passes the **unwrapped** data to`func` and **re-wraps** the result for continued chaining.
97
+ * `pipe[**P, R](func: Callable[Concatenate[Self, P]], *args: P.args, **kwargs: P.kwargs) -> R`
98
+ Passes the **wrapped instance** (`self`) to `func` and returns the raw result (can be terminal).
99
+ * `println()`
100
+ Prints the unwrapped data and returns `self`.
101
+
102
+ ### Rich Lazy Iteration (`Iter`)
103
+
104
+ Leverage dozens of methods inspired by Rust's `Iterator`, `itertools`, `cytoolz`, and `more-itertools`.
105
+
106
+ ```python
107
+ import pyochain as pc
108
+
109
+ result = (
110
+ pc.Iter.from_count(1) # Infinite iterator: 1, 2, 3, ...
111
+ .filter(lambda x: x % 2 != 0) # Keep odd numbers: 1, 3, 5, ...
112
+ .map(lambda x: x * x) # Square them: 1, 9, 25, ...
113
+ .take(5) # Take the first 5: 1, 9, 25, 49, 81
114
+ .into(list) # Consume into a list
115
+ )
116
+ # result: [1, 9, 25, 49, 81]
117
+ ```
118
+
119
+ ### Typing enforcement
120
+
121
+ Each method and class make extensive use of generics, type hints, and overloads (when necessary) to ensure type safety and improve developer experience.
122
+
123
+ 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.
124
+
125
+ Target: modern Python 3.13 syntax (PEP 695 generics, updated collections.abc types).
126
+
127
+ ### Expressions for Dict ``pyochain.key``
128
+
129
+ Compute new fields from existing nested data with key() and Expr.apply(), either selecting a new dict or merging into the root.
130
+
131
+ ```python
132
+ import pyochain as pc
133
+
134
+ # Build a compact view
135
+ data = pc.Dict(
136
+ {
137
+ "user": {"name": "Alice", "age": 30},
138
+ "scores": {"math": 18, "eng": 15},
139
+ }
140
+ )
141
+
142
+ view = data.select(
143
+ pc.key("user").key("name"),
144
+ pc.key("scores").key("math"),
145
+ pc.key("scores").key("eng"),
146
+ pc.key("user").key("age").apply(lambda x: x >= 18).alias("is_adult"),
147
+ )
148
+ # {"name": "Alice", "math": 18, "eng": 15, "is_adult": True}
149
+ merged = data.with_fields(
150
+ pc.key("scores").key("math").apply(lambda x: x * 10).alias("math_x10")
151
+ )
152
+ # {
153
+ # 'user': {'name': 'Alice', 'age': 30},
154
+ # 'scores': {'math': 18, 'eng': 15},
155
+ # 'math_x10': 180
156
+ # }
157
+
158
+ ```
159
+
160
+ ### Convenience mappers: itr and struct
161
+
162
+ Operate on iterables of iterables or iterables of dicts without leaving the chain.
163
+
164
+ ```python
165
+ import pyochain as pc
166
+
167
+ nested = pc.Iter.from_([[1, 2, 3], [4, 5]])
168
+ totals = nested.itr(lambda it: it.sum()).into(list)
169
+ # [6, 9]
170
+
171
+ records = pc.Iter.from_(
172
+ [
173
+ {"name": "Alice", "age": 30},
174
+ {"name": "Bob", "age": 25},
175
+ ]
176
+ )
177
+ names = records.struct(lambda d: d.pluck("name").unwrap()).into(list)
178
+ # ['Alice', 'Bob']
179
+ ```
180
+
181
+ ## Key Dependencies and credits
182
+
183
+ Most of the computations are done with implementations from the `cytoolz`, `more-itertools`, and `rolling` libraries.
184
+
185
+ An extensive use of the `itertools` stdlib module is also to be noted.
186
+
187
+ pyochain acts as a unifying API layer over these powerful tools.
188
+
189
+ <https://github.com/pytoolz/cytoolz>
190
+
191
+ <https://github.com/more-itertools/more-itertools>
192
+
193
+ <https://github.com/ajcr/rolling>
194
+
195
+ The stubs used for the developpement, made by the maintainer of pyochain, can be found here:
196
+
197
+ <https://github.com/py-stubs/cytoolz-stubs>
198
+
199
+ ---
200
+
201
+ ## Real-life simple example
202
+
203
+ In one of my project, I have to introspect some modules from plotly to get some lists of colors.
204
+
205
+ I want to check wether the colors are in hex format or not, and I want to get a dictionary of palettes.
206
+ We can see here that pyochain allow to keep the same style than polars, with method chaining, but for plain python objects.
207
+
208
+ Due to the freedom of python, multiple paradigms are implemented across libraries.
209
+
210
+ 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 ...] ... ].
211
+
212
+ ```python
213
+
214
+ from types import ModuleType
215
+
216
+ import polars as pl
217
+ import pyochain as pc
218
+ from plotly.express.colors import cyclical, qualitative, sequential
219
+
220
+
221
+
222
+ MODULES: set[ModuleType] = {
223
+ sequential,
224
+ cyclical,
225
+ qualitative,
226
+ }
227
+
228
+ def get_palettes() -> pc.Dict[str, list[str]]:
229
+ clr = "color"
230
+ scl = "scale"
231
+ df: pl.DataFrame = (
232
+ pc.Iter.from_(MODULES)
233
+ .map(
234
+ lambda mod: pc.Dict.from_object(mod)
235
+ .filter_values(lambda v: isinstance(v, list))
236
+ .unwrap()
237
+ )
238
+ .into(pl.LazyFrame)
239
+ .unpivot(value_name=clr, variable_name=scl)
240
+ .drop_nulls()
241
+ .filter(
242
+ pl.col(clr)
243
+ .list.eval(pl.element().first().str.starts_with("#").alias("is_hex"))
244
+ .list.first()
245
+ )
246
+ .sort(scl)
247
+ .collect()
248
+ )
249
+ keys: list[str] = df.get_column(scl).to_list()
250
+ values: list[list[str]] = df.get_column(clr).to_list()
251
+ return pc.Iter.from_(keys).with_values(values)
252
+
253
+
254
+ # Ouput excerpt:
255
+ {'mygbm_r': ['#ef55f1',
256
+ '#c543fa',
257
+ '#9139fa',
258
+ '#6324f5',
259
+ '#2e21ea',
260
+ '#284ec8',
261
+ '#3d719a',
262
+ '#439064',
263
+ '#31ac28',
264
+ '#61c10b',
265
+ '#96d310',
266
+ '#c6e516',
267
+ '#f0ed35',
268
+ '#fcd471',
269
+ '#fbafa1',
270
+ '#fb84ce',
271
+ '#ef55f1']}
272
+ ```
273
+
274
+ However you can still easily go back with for loops when the readability is better this way.
275
+
276
+ In another place, I use this function to generate a Literal from the keys of the palettes.
277
+
278
+ ```python
279
+
280
+ from enum import StrEnum
281
+
282
+ class Text(StrEnum):
283
+ CONTENT = "Palettes = Literal[\n"
284
+ END_CONTENT = "]\n"
285
+ ...# rest of the class
286
+
287
+ def generate_palettes_literal() -> None:
288
+ literal_content: str = Text.CONTENT
289
+ for name in get_palettes().iter_keys().sort().unwrap():
290
+ literal_content += f' "{name}",\n'
291
+ literal_content += Text.END_CONTENT
292
+ ...# rest of the function
293
+ ```
294
+
295
+ 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,285 @@
1
+ # pyochain ⛓️
2
+
3
+ **_Functional-style method chaining for Python data structures._**
4
+
5
+ `pyochain` brings a fluent, declarative API inspired by Rust's `Iterator` and DataFrame libraries like Polars to your everyday Python iterables and dictionaries.
6
+
7
+ Manipulate data through composable chains of operations, enhancing readability and reducing boilerplate.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ uv add git+https://github.com/OutSquareCapital/pyochain.git
13
+ ```
14
+
15
+ ## API Reference 📖
16
+
17
+ The full API reference can be found at:
18
+ <https://outsquarecapital.github.io/pyochain/>
19
+
20
+ ## Overview
21
+
22
+ ### Philosophy
23
+
24
+ * **Declarative over Imperative:** Replace explicit `for` and `while` loops with sequences of high-level operations (map, filter, group, join...).
25
+ * **Fluent Chaining:** Each method transforms the data and returns a new wrapper instance, allowing for seamless chaining.
26
+ * **Lazy and Eager:** `Iter` operates lazily for efficiency on large or infinite sequences, while `Seq` represents materialized collections for eager operations.
27
+ * **100% Type-safe:** Extensive use of generics and overloads ensures type safety and improves developer experience.
28
+ * **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.
29
+ * **Functional paradigm:** Design encourages building complex data transformations by composing simple, reusable functions on known buildings blocks, rather than implementing customs classes each time.
30
+
31
+ ### Inspirations
32
+
33
+ * **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.
34
+ * **Polars API:** The powerful expression API for `pyochain.Dict` (`select`, `with_fields`, `key`) mimics the expressive power of Polars for selecting, transforming, and reshaping nested dictionary data.
35
+ * **Python iterators libraries:** Libraries like `rolling`, `cytoolz`, and `more-itertools` provided ideas, inspiration, and implementations for many of the iterator methods.
36
+ * **PyFunctional:** Although not directly used (because I started writing pyochain before discovering it), also shares similar goals and ideas.
37
+
38
+ ### Core Components
39
+
40
+ #### `Iter[T]`
41
+
42
+ 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).
43
+
44
+ All operations that return a new `Iter` are **lazy**, consuming the underlying iterator on demand.
45
+
46
+ Provides a vast array of methods for transformation, filtering, aggregation, joining, etc..
47
+
48
+ #### `Seq[T]`
49
+
50
+ Wraps a Python `Collection` (`list`, `tuple`, `set`...), and represents **eagerly** evaluated data.
51
+
52
+ Exposes a subset of the `Iter` methods who operate on the full dataset (e.g., `sort`, `union`) or who aggregate it.
53
+
54
+ It is most useful when you need to reuse the data multiple times without re-iterating it.
55
+
56
+ Use `.iter()` to switch back to lazy processing.
57
+
58
+ #### `Dict[K, V]`
59
+
60
+ Wraps a Python `dict` (or any Mapping via ``Dict.from_``) and provides chainable methods specific to dictionaries (manipulating keys, values, items, nesting, joins, grouping).
61
+
62
+ Promote immutability by returning new `Dict` instances on each operation, and avoiding in-place modifications.
63
+
64
+ 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.
65
+
66
+ 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:
67
+
68
+ * `pluck` to extract multiple fields at once.
69
+ * `flatten` to collapse nested structures into a single level.
70
+ * `schema` to infer the structure of the data by recursively analyzing keys and value types.
71
+ * `pyochain.key` expressions to compute/retrieve/select/create new fields from existing nested data in a declarative way.
72
+
73
+ #### `Wrapper[T]`
74
+
75
+ A generic wrapper for any Python object, allowing integration into `pyochain`'s fluent style using `pipe`, `apply`, and `into`.
76
+
77
+ 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.
78
+
79
+ ### Core Piping Methods
80
+
81
+ All wrappers inherit from `CommonBase`:
82
+
83
+ * `into[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> R`
84
+ Passes the **unwrapped** data to `func` and returns the raw result (terminal).
85
+ * `apply[**P, R](func: Callable[Concatenate[T, P]], *args: P.args, **kwargs: P.kwargs) -> "CurrentWrapper"[R]`
86
+ Passes the **unwrapped** data to`func` and **re-wraps** the result for continued chaining.
87
+ * `pipe[**P, R](func: Callable[Concatenate[Self, P]], *args: P.args, **kwargs: P.kwargs) -> R`
88
+ Passes the **wrapped instance** (`self`) to `func` and returns the raw result (can be terminal).
89
+ * `println()`
90
+ Prints the unwrapped data and returns `self`.
91
+
92
+ ### Rich Lazy Iteration (`Iter`)
93
+
94
+ Leverage dozens of methods inspired by Rust's `Iterator`, `itertools`, `cytoolz`, and `more-itertools`.
95
+
96
+ ```python
97
+ import pyochain as pc
98
+
99
+ result = (
100
+ pc.Iter.from_count(1) # Infinite iterator: 1, 2, 3, ...
101
+ .filter(lambda x: x % 2 != 0) # Keep odd numbers: 1, 3, 5, ...
102
+ .map(lambda x: x * x) # Square them: 1, 9, 25, ...
103
+ .take(5) # Take the first 5: 1, 9, 25, 49, 81
104
+ .into(list) # Consume into a list
105
+ )
106
+ # result: [1, 9, 25, 49, 81]
107
+ ```
108
+
109
+ ### Typing enforcement
110
+
111
+ Each method and class make extensive use of generics, type hints, and overloads (when necessary) to ensure type safety and improve developer experience.
112
+
113
+ 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.
114
+
115
+ Target: modern Python 3.13 syntax (PEP 695 generics, updated collections.abc types).
116
+
117
+ ### Expressions for Dict ``pyochain.key``
118
+
119
+ Compute new fields from existing nested data with key() and Expr.apply(), either selecting a new dict or merging into the root.
120
+
121
+ ```python
122
+ import pyochain as pc
123
+
124
+ # Build a compact view
125
+ data = pc.Dict(
126
+ {
127
+ "user": {"name": "Alice", "age": 30},
128
+ "scores": {"math": 18, "eng": 15},
129
+ }
130
+ )
131
+
132
+ view = data.select(
133
+ pc.key("user").key("name"),
134
+ pc.key("scores").key("math"),
135
+ pc.key("scores").key("eng"),
136
+ pc.key("user").key("age").apply(lambda x: x >= 18).alias("is_adult"),
137
+ )
138
+ # {"name": "Alice", "math": 18, "eng": 15, "is_adult": True}
139
+ merged = data.with_fields(
140
+ pc.key("scores").key("math").apply(lambda x: x * 10).alias("math_x10")
141
+ )
142
+ # {
143
+ # 'user': {'name': 'Alice', 'age': 30},
144
+ # 'scores': {'math': 18, 'eng': 15},
145
+ # 'math_x10': 180
146
+ # }
147
+
148
+ ```
149
+
150
+ ### Convenience mappers: itr and struct
151
+
152
+ Operate on iterables of iterables or iterables of dicts without leaving the chain.
153
+
154
+ ```python
155
+ import pyochain as pc
156
+
157
+ nested = pc.Iter.from_([[1, 2, 3], [4, 5]])
158
+ totals = nested.itr(lambda it: it.sum()).into(list)
159
+ # [6, 9]
160
+
161
+ records = pc.Iter.from_(
162
+ [
163
+ {"name": "Alice", "age": 30},
164
+ {"name": "Bob", "age": 25},
165
+ ]
166
+ )
167
+ names = records.struct(lambda d: d.pluck("name").unwrap()).into(list)
168
+ # ['Alice', 'Bob']
169
+ ```
170
+
171
+ ## Key Dependencies and credits
172
+
173
+ Most of the computations are done with implementations from the `cytoolz`, `more-itertools`, and `rolling` libraries.
174
+
175
+ An extensive use of the `itertools` stdlib module is also to be noted.
176
+
177
+ pyochain acts as a unifying API layer over these powerful tools.
178
+
179
+ <https://github.com/pytoolz/cytoolz>
180
+
181
+ <https://github.com/more-itertools/more-itertools>
182
+
183
+ <https://github.com/ajcr/rolling>
184
+
185
+ The stubs used for the developpement, made by the maintainer of pyochain, can be found here:
186
+
187
+ <https://github.com/py-stubs/cytoolz-stubs>
188
+
189
+ ---
190
+
191
+ ## Real-life simple example
192
+
193
+ In one of my project, I have to introspect some modules from plotly to get some lists of colors.
194
+
195
+ I want to check wether the colors are in hex format or not, and I want to get a dictionary of palettes.
196
+ We can see here that pyochain allow to keep the same style than polars, with method chaining, but for plain python objects.
197
+
198
+ Due to the freedom of python, multiple paradigms are implemented across libraries.
199
+
200
+ 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 ...] ... ].
201
+
202
+ ```python
203
+
204
+ from types import ModuleType
205
+
206
+ import polars as pl
207
+ import pyochain as pc
208
+ from plotly.express.colors import cyclical, qualitative, sequential
209
+
210
+
211
+
212
+ MODULES: set[ModuleType] = {
213
+ sequential,
214
+ cyclical,
215
+ qualitative,
216
+ }
217
+
218
+ def get_palettes() -> pc.Dict[str, list[str]]:
219
+ clr = "color"
220
+ scl = "scale"
221
+ df: pl.DataFrame = (
222
+ pc.Iter.from_(MODULES)
223
+ .map(
224
+ lambda mod: pc.Dict.from_object(mod)
225
+ .filter_values(lambda v: isinstance(v, list))
226
+ .unwrap()
227
+ )
228
+ .into(pl.LazyFrame)
229
+ .unpivot(value_name=clr, variable_name=scl)
230
+ .drop_nulls()
231
+ .filter(
232
+ pl.col(clr)
233
+ .list.eval(pl.element().first().str.starts_with("#").alias("is_hex"))
234
+ .list.first()
235
+ )
236
+ .sort(scl)
237
+ .collect()
238
+ )
239
+ keys: list[str] = df.get_column(scl).to_list()
240
+ values: list[list[str]] = df.get_column(clr).to_list()
241
+ return pc.Iter.from_(keys).with_values(values)
242
+
243
+
244
+ # Ouput excerpt:
245
+ {'mygbm_r': ['#ef55f1',
246
+ '#c543fa',
247
+ '#9139fa',
248
+ '#6324f5',
249
+ '#2e21ea',
250
+ '#284ec8',
251
+ '#3d719a',
252
+ '#439064',
253
+ '#31ac28',
254
+ '#61c10b',
255
+ '#96d310',
256
+ '#c6e516',
257
+ '#f0ed35',
258
+ '#fcd471',
259
+ '#fbafa1',
260
+ '#fb84ce',
261
+ '#ef55f1']}
262
+ ```
263
+
264
+ However you can still easily go back with for loops when the readability is better this way.
265
+
266
+ In another place, I use this function to generate a Literal from the keys of the palettes.
267
+
268
+ ```python
269
+
270
+ from enum import StrEnum
271
+
272
+ class Text(StrEnum):
273
+ CONTENT = "Palettes = Literal[\n"
274
+ END_CONTENT = "]\n"
275
+ ...# rest of the class
276
+
277
+ def generate_palettes_literal() -> None:
278
+ literal_content: str = Text.CONTENT
279
+ for name in get_palettes().iter_keys().sort().unwrap():
280
+ literal_content += f' "{name}",\n'
281
+ literal_content += Text.END_CONTENT
282
+ ...# rest of the function
283
+ ```
284
+
285
+ 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,35 @@
1
+ [project]
2
+ description = "Add your description here"
3
+ name = "pyochain"
4
+ readme = "README.md"
5
+ requires-python = ">=3.12"
6
+ version = "0.5.0"
7
+
8
+ dependencies = ["cytoolz>=1.0.1", "more-itertools>=10.8.0", "rolling>=0.5.0"]
9
+
10
+ [dependency-groups]
11
+ dev = [
12
+ "cytoolz-stubs",
13
+ "doctester",
14
+ "griffe>=1.14.0",
15
+ "mkdocs>=1.6.1",
16
+ "mkdocs-autorefs>=1.4.3",
17
+ "mkdocs-material>=9.6.22",
18
+ "mkdocs-simple-hooks>=0.1.5",
19
+ "mkdocstrings>=0.30.1",
20
+ "mkdocstrings-python>=1.18.2",
21
+ "polars>=1.33.1",
22
+ "ruff>=0.14.1",
23
+ "rolling @ git+https://github.com/OutSquareCapital/rolling.git@add-type-stubs",
24
+ ]
25
+
26
+ [tool.ruff.format]
27
+ docstring-code-format = true
28
+
29
+ [tool.uv.sources]
30
+ cytoolz-stubs = { git = "https://github.com/py-stubs/cytoolz-stubs.git" }
31
+ doctester = { git = "https://github.com/OutSquareCapital/doctester.git" }
32
+
33
+ [build-system]
34
+ build-backend = "uv_build"
35
+ requires = ["uv_build>=0.8.15,<0.9.0"]
@@ -0,0 +1,5 @@
1
+ from ._core import Wrapper
2
+ from ._dict import Dict, Expr, key
3
+ from ._iter import Iter, Seq
4
+
5
+ __all__ = ["Dict", "Iter", "Wrapper", "key", "Seq", "Expr"]
@@ -0,0 +1,21 @@
1
+ from ._main import CommonBase, IterWrapper, MappingWrapper, Pipeable, Wrapper
2
+ from ._protocols import (
3
+ Peeked,
4
+ SizedIterable,
5
+ SupportsAllComparisons,
6
+ SupportsKeysAndGetItem,
7
+ SupportsRichComparison,
8
+ )
9
+
10
+ __all__ = [
11
+ "MappingWrapper",
12
+ "CommonBase",
13
+ "IterWrapper",
14
+ "Wrapper",
15
+ "SupportsAllComparisons",
16
+ "SupportsRichComparison",
17
+ "SupportsKeysAndGetItem",
18
+ "Peeked",
19
+ "SizedIterable",
20
+ "Pipeable",
21
+ ]