pyochain 0.5.1__py3-none-any.whl → 0.5.32__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.
Potentially problematic release.
This version of pyochain might be problematic. Click here for more details.
- pyochain/__init__.py +2 -2
- pyochain/_core/__init__.py +3 -1
- pyochain/_core/_format.py +34 -0
- pyochain/_core/_main.py +65 -44
- pyochain/_core/_protocols.py +2 -7
- pyochain/_dict/__init__.py +1 -2
- pyochain/_dict/_filters.py +38 -53
- pyochain/_dict/_groups.py +7 -8
- pyochain/_dict/_iter.py +52 -9
- pyochain/_dict/_joins.py +11 -9
- pyochain/_dict/_main.py +32 -226
- pyochain/_dict/_maps.py +142 -0
- pyochain/_dict/_nested.py +119 -65
- pyochain/_dict/_process.py +40 -7
- pyochain/_iter/_aggregations.py +1 -0
- pyochain/_iter/_booleans.py +3 -0
- pyochain/_iter/_dicts.py +243 -0
- pyochain/_iter/_eager.py +60 -22
- pyochain/_iter/_filters.py +40 -49
- pyochain/_iter/_joins.py +13 -16
- pyochain/_iter/_lists.py +11 -9
- pyochain/_iter/_main.py +297 -60
- pyochain/_iter/_maps.py +55 -39
- pyochain/_iter/_partitions.py +11 -14
- pyochain/_iter/_process.py +26 -44
- pyochain/_iter/_rolling.py +22 -28
- pyochain/_iter/_tuples.py +119 -14
- {pyochain-0.5.1.dist-info → pyochain-0.5.32.dist-info}/METADATA +8 -42
- pyochain-0.5.32.dist-info/RECORD +32 -0
- pyochain/_dict/_exprs.py +0 -115
- pyochain/_dict/_funcs.py +0 -62
- pyochain/_iter/_constructors.py +0 -155
- pyochain/_iter/_groups.py +0 -264
- pyochain-0.5.1.dist-info/RECORD +0 -33
- {pyochain-0.5.1.dist-info → pyochain-0.5.32.dist-info}/WHEEL +0 -0
pyochain/_dict/_main.py
CHANGED
|
@@ -1,47 +1,51 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections import
|
|
4
|
-
from
|
|
5
|
-
from functools import partial
|
|
6
|
-
from typing import Any, Self
|
|
7
|
-
|
|
8
|
-
import cytoolz as cz
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from typing import Any
|
|
9
5
|
|
|
10
6
|
from .._core import SupportsKeysAndGetItem
|
|
11
|
-
from ._exprs import IntoExpr, compute_exprs
|
|
12
7
|
from ._filters import FilterDict
|
|
13
|
-
from ._funcs import dict_repr
|
|
14
8
|
from ._groups import GroupsDict
|
|
15
9
|
from ._iter import IterDict
|
|
16
10
|
from ._joins import JoinsDict
|
|
11
|
+
from ._maps import MapDict
|
|
17
12
|
from ._nested import NestedDict
|
|
18
13
|
from ._process import ProcessDict
|
|
19
14
|
|
|
20
15
|
|
|
21
|
-
class
|
|
16
|
+
class DictCommonMethods[K, V](
|
|
22
17
|
ProcessDict[K, V],
|
|
23
18
|
IterDict[K, V],
|
|
24
19
|
NestedDict[K, V],
|
|
20
|
+
MapDict[K, V],
|
|
25
21
|
JoinsDict[K, V],
|
|
26
22
|
FilterDict[K, V],
|
|
27
23
|
GroupsDict[K, V],
|
|
28
24
|
):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Dict[K, V](DictCommonMethods[K, V]):
|
|
29
29
|
"""
|
|
30
30
|
Wrapper for Python dictionaries with chainable methods.
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
__slots__ = ()
|
|
34
34
|
|
|
35
|
-
def __repr__(self) -> str:
|
|
36
|
-
return f"{self.__class__.__name__}({dict_repr(self.unwrap())})"
|
|
37
|
-
|
|
38
35
|
@staticmethod
|
|
39
|
-
def from_[G, I](
|
|
36
|
+
def from_[G, I](
|
|
37
|
+
data: Mapping[G, I] | Iterable[tuple[G, I]] | SupportsKeysAndGetItem[G, I],
|
|
38
|
+
) -> Dict[G, I]:
|
|
40
39
|
"""
|
|
41
|
-
Create a Dict from a
|
|
40
|
+
Create a Dict from a convertible value.
|
|
42
41
|
|
|
43
42
|
Args:
|
|
44
|
-
data: A mapping or object supporting keys and item access to convert into a Dict.
|
|
43
|
+
data: A mapping, Iterable of tuples, or object supporting keys and item access to convert into a Dict.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A Dict instance containing the data from the input.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
45
49
|
|
|
46
50
|
```python
|
|
47
51
|
>>> import pyochain as pc
|
|
@@ -57,6 +61,8 @@ class Dict[K, V](
|
|
|
57
61
|
>>>
|
|
58
62
|
>>> pc.Dict.from_(MyMapping()).unwrap()
|
|
59
63
|
{1: 'a', 2: 'b', 3: 'c'}
|
|
64
|
+
>>> pc.Dict.from_([("d", "e"), ("f", "g")]).unwrap()
|
|
65
|
+
{'d': 'e', 'f': 'g'}
|
|
60
66
|
|
|
61
67
|
```
|
|
62
68
|
"""
|
|
@@ -84,224 +90,24 @@ class Dict[K, V](
|
|
|
84
90
|
"""
|
|
85
91
|
return Dict(obj.__dict__)
|
|
86
92
|
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
89
|
-
Select and alias fields from the dict based on expressions and/or strings.
|
|
90
|
-
|
|
91
|
-
Navigate nested fields using the `pyochain.key` function.
|
|
92
|
-
|
|
93
|
-
- Chain `key.key()` calls to access nested fields.
|
|
94
|
-
- Use `key.apply()` to transform values.
|
|
95
|
-
- Use `key.alias()` to rename fields in the resulting dict.
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
*exprs: Expressions or strings to select and alias fields from the dictionary.
|
|
99
|
-
|
|
100
|
-
```python
|
|
101
|
-
>>> import pyochain as pc
|
|
102
|
-
>>> data = {
|
|
103
|
-
... "name": "Alice",
|
|
104
|
-
... "age": 30,
|
|
105
|
-
... "scores": {"eng": [85, 90, 95], "math": [80, 88, 92]},
|
|
106
|
-
... }
|
|
107
|
-
>>> scores_expr = pc.key("scores") # save an expression for reuse
|
|
108
|
-
>>> pc.Dict(data).select(
|
|
109
|
-
... pc.key("name").alias("student_name"),
|
|
110
|
-
... "age", # shorthand for pc.key("age")
|
|
111
|
-
... scores_expr.key("math").alias("math_scores"),
|
|
112
|
-
... scores_expr.key("eng")
|
|
113
|
-
... .apply(lambda v: pc.Seq(v).mean())
|
|
114
|
-
... .alias("average_eng_score"),
|
|
115
|
-
... ).unwrap()
|
|
116
|
-
{'student_name': 'Alice', 'age': 30, 'math_scores': [80, 88, 92], 'average_eng_score': 90}
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
"""
|
|
120
|
-
|
|
121
|
-
def _select(data: dict[str, Any]) -> dict[str, Any]:
|
|
122
|
-
return compute_exprs(exprs, data, {})
|
|
123
|
-
|
|
124
|
-
return self.apply(_select)
|
|
125
|
-
|
|
126
|
-
def with_fields(self: Dict[str, Any], *exprs: IntoExpr) -> Dict[str, Any]:
|
|
127
|
-
"""
|
|
128
|
-
Merge aliased expressions into the root dict (overwrite on collision).
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
*exprs: Expressions to merge into the root dictionary.
|
|
132
|
-
|
|
133
|
-
```python
|
|
134
|
-
>>> import pyochain as pc
|
|
135
|
-
>>> data = {
|
|
136
|
-
... "name": "Alice",
|
|
137
|
-
... "age": 30,
|
|
138
|
-
... "scores": {"eng": [85, 90, 95], "math": [80, 88, 92]},
|
|
139
|
-
... }
|
|
140
|
-
>>> scores_expr = pc.key("scores") # save an expression for reuse
|
|
141
|
-
>>> pc.Dict(data).with_fields(
|
|
142
|
-
... scores_expr.key("eng")
|
|
143
|
-
... .apply(lambda v: pc.Seq(v).mean())
|
|
144
|
-
... .alias("average_eng_score"),
|
|
145
|
-
... ).unwrap()
|
|
146
|
-
{'name': 'Alice', 'age': 30, 'scores': {'eng': [85, 90, 95], 'math': [80, 88, 92]}, 'average_eng_score': 90}
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
def _with_fields(data: dict[str, Any]) -> dict[str, Any]:
|
|
152
|
-
return compute_exprs(exprs, data, data.copy())
|
|
153
|
-
|
|
154
|
-
return self.apply(_with_fields)
|
|
155
|
-
|
|
156
|
-
def map_keys[T](self, func: Callable[[K], T]) -> Dict[T, V]:
|
|
157
|
-
"""
|
|
158
|
-
Return a Dict with keys transformed by func.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
func: Function to apply to each key in the dictionary.
|
|
162
|
-
|
|
163
|
-
```python
|
|
164
|
-
>>> import pyochain as pc
|
|
165
|
-
>>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_keys(
|
|
166
|
-
... str.lower
|
|
167
|
-
... ).unwrap()
|
|
168
|
-
{'alice': [20, 15, 30], 'bob': [10, 35]}
|
|
169
|
-
>>>
|
|
170
|
-
>>> pc.Dict({1: "a"}).map_keys(str).unwrap()
|
|
171
|
-
{'1': 'a'}
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
"""
|
|
175
|
-
return self.apply(partial(cz.dicttoolz.keymap, func))
|
|
176
|
-
|
|
177
|
-
def map_values[T](self, func: Callable[[V], T]) -> Dict[K, T]:
|
|
178
|
-
"""
|
|
179
|
-
Return a Dict with values transformed by func.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
func: Function to apply to each value in the dictionary.
|
|
183
|
-
|
|
184
|
-
```python
|
|
185
|
-
>>> import pyochain as pc
|
|
186
|
-
>>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_values(sum).unwrap()
|
|
187
|
-
{'Alice': 65, 'Bob': 45}
|
|
188
|
-
>>>
|
|
189
|
-
>>> pc.Dict({1: 1}).map_values(lambda v: v + 1).unwrap()
|
|
190
|
-
{1: 2}
|
|
191
|
-
|
|
192
|
-
```
|
|
93
|
+
def pivot(self, *indices: int) -> Dict[Any, Any]:
|
|
193
94
|
"""
|
|
194
|
-
|
|
95
|
+
Pivot a nested dictionary by rearranging the key levels according to order.
|
|
195
96
|
|
|
196
|
-
|
|
197
|
-
self,
|
|
198
|
-
func: Callable[[tuple[K, V]], tuple[KR, VR]],
|
|
199
|
-
) -> Dict[KR, VR]:
|
|
200
|
-
"""
|
|
201
|
-
Transform (key, value) pairs using a function that takes a (key, value) tuple.
|
|
97
|
+
Syntactic sugar for to_arrays().rearrange(*indices).to_records()
|
|
202
98
|
|
|
203
99
|
Args:
|
|
204
|
-
|
|
100
|
+
indices: Indices specifying the new order of key levels
|
|
205
101
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
>>> pc.Dict({"Alice": 10, "Bob": 20}).map_items(
|
|
209
|
-
... lambda kv: (kv[0].upper(), kv[1] * 2)
|
|
210
|
-
... ).unwrap()
|
|
211
|
-
{'ALICE': 20, 'BOB': 40}
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
"""
|
|
215
|
-
return self.apply(partial(cz.dicttoolz.itemmap, func))
|
|
216
|
-
|
|
217
|
-
def map_kv[KR, VR](
|
|
218
|
-
self,
|
|
219
|
-
func: Callable[[K, V], tuple[KR, VR]],
|
|
220
|
-
) -> Dict[KR, VR]:
|
|
221
|
-
"""
|
|
222
|
-
Transform (key, value) pairs using a function that takes key and value as separate arguments.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
func: Function to transform each key and value into a new (key, value) tuple.
|
|
226
|
-
|
|
227
|
-
```python
|
|
228
|
-
>>> import pyochain as pc
|
|
229
|
-
>>> pc.Dict({1: 2}).map_kv(lambda k, v: (k + 1, v * 10)).unwrap()
|
|
230
|
-
{2: 20}
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
"""
|
|
234
|
-
|
|
235
|
-
def _map_kv(data: dict[K, V]) -> dict[KR, VR]:
|
|
236
|
-
def _(kv: tuple[K, V]) -> tuple[KR, VR]:
|
|
237
|
-
return func(kv[0], kv[1])
|
|
238
|
-
|
|
239
|
-
return cz.dicttoolz.itemmap(_, data)
|
|
240
|
-
|
|
241
|
-
return self.apply(_map_kv)
|
|
242
|
-
|
|
243
|
-
def invert(self) -> Dict[V, list[K]]:
|
|
244
|
-
"""
|
|
245
|
-
Invert the dictionary, grouping keys by common (and hashable) values.
|
|
246
|
-
```python
|
|
247
|
-
>>> import pyochain as pc
|
|
248
|
-
>>> d = {"a": 1, "b": 2, "c": 1}
|
|
249
|
-
>>> pc.Dict(d).invert().unwrap()
|
|
250
|
-
{1: ['a', 'c'], 2: ['b']}
|
|
251
|
-
|
|
252
|
-
```
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
def _invert(data: dict[K, V]) -> dict[V, list[K]]:
|
|
256
|
-
inverted: dict[V, list[K]] = defaultdict(list)
|
|
257
|
-
for k, v in data.items():
|
|
258
|
-
inverted[v].append(k)
|
|
259
|
-
return dict(inverted)
|
|
260
|
-
|
|
261
|
-
return self.apply(_invert)
|
|
262
|
-
|
|
263
|
-
def implode(self) -> Dict[K, list[V]]:
|
|
264
|
-
"""
|
|
265
|
-
Nest all the values in lists.
|
|
266
|
-
syntactic sugar for map_values(lambda v: [v])
|
|
267
|
-
```python
|
|
268
|
-
>>> import pyochain as pc
|
|
269
|
-
>>> pc.Dict({1: 2, 3: 4}).implode().unwrap()
|
|
270
|
-
{1: [2], 3: [4]}
|
|
271
|
-
|
|
272
|
-
```
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
def _implode(data: dict[K, V]) -> dict[K, list[V]]:
|
|
276
|
-
def _(v: V) -> list[V]:
|
|
277
|
-
return [v]
|
|
278
|
-
|
|
279
|
-
return cz.dicttoolz.valmap(_, data)
|
|
280
|
-
|
|
281
|
-
return self.apply(_implode)
|
|
282
|
-
|
|
283
|
-
def equals_to(self, other: Self | Mapping[Any, Any]) -> bool:
|
|
284
|
-
"""
|
|
285
|
-
Check if two records are equal based on their data.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
other: Another Dict or mapping to compare against.
|
|
102
|
+
Returns:
|
|
103
|
+
Pivoted dictionary with keys rearranged
|
|
289
104
|
|
|
290
105
|
Example:
|
|
291
106
|
```python
|
|
292
107
|
>>> import pyochain as pc
|
|
293
|
-
>>>
|
|
294
|
-
>>>
|
|
295
|
-
|
|
296
|
-
>>> d1.equals_to(d2)
|
|
297
|
-
True
|
|
298
|
-
>>> d1.equals_to(d3)
|
|
299
|
-
False
|
|
300
|
-
|
|
301
|
-
```
|
|
108
|
+
>>> d = {"A": {"X": 1, "Y": 2}, "B": {"X": 3, "Y": 4}}
|
|
109
|
+
>>> pc.Dict(d).pivot(1, 0).unwrap()
|
|
110
|
+
{'X': {'A': 1, 'B': 3}, 'Y': {'A': 2, 'B': 4}}
|
|
302
111
|
"""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if isinstance(other, Dict)
|
|
306
|
-
else self.unwrap() == other
|
|
307
|
-
)
|
|
112
|
+
|
|
113
|
+
return self.to_arrays().rearrange(*indices).to_records()
|
pyochain/_dict/_maps.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import cytoolz as cz
|
|
9
|
+
|
|
10
|
+
from .._core import MappingWrapper
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._main import Dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MapDict[K, V](MappingWrapper[K, V]):
|
|
17
|
+
def map_keys[T](self, func: Callable[[K], T]) -> Dict[T, V]:
|
|
18
|
+
"""
|
|
19
|
+
Return keys transformed by func.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
func: Function to apply to each key in the dictionary.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
>>> import pyochain as pc
|
|
26
|
+
>>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_keys(
|
|
27
|
+
... str.lower
|
|
28
|
+
... ).unwrap()
|
|
29
|
+
{'alice': [20, 15, 30], 'bob': [10, 35]}
|
|
30
|
+
>>>
|
|
31
|
+
>>> pc.Dict({1: "a"}).map_keys(str).unwrap()
|
|
32
|
+
{'1': 'a'}
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
return self._new(partial(cz.dicttoolz.keymap, func))
|
|
37
|
+
|
|
38
|
+
def map_values[T](self, func: Callable[[V], T]) -> Dict[K, T]:
|
|
39
|
+
"""
|
|
40
|
+
Return values transformed by func.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
func: Function to apply to each value in the dictionary.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
>>> import pyochain as pc
|
|
47
|
+
>>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_values(sum).unwrap()
|
|
48
|
+
{'Alice': 65, 'Bob': 45}
|
|
49
|
+
>>>
|
|
50
|
+
>>> pc.Dict({1: 1}).map_values(lambda v: v + 1).unwrap()
|
|
51
|
+
{1: 2}
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
return self._new(partial(cz.dicttoolz.valmap, func))
|
|
56
|
+
|
|
57
|
+
def map_items[KR, VR](
|
|
58
|
+
self,
|
|
59
|
+
func: Callable[[tuple[K, V]], tuple[KR, VR]],
|
|
60
|
+
) -> Dict[KR, VR]:
|
|
61
|
+
"""
|
|
62
|
+
Transform (key, value) pairs using a function that takes a (key, value) tuple.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
func: Function to transform each (key, value) pair into a new (key, value) tuple.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
>>> import pyochain as pc
|
|
69
|
+
>>> pc.Dict({"Alice": 10, "Bob": 20}).map_items(
|
|
70
|
+
... lambda kv: (kv[0].upper(), kv[1] * 2)
|
|
71
|
+
... ).unwrap()
|
|
72
|
+
{'ALICE': 20, 'BOB': 40}
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
return self._new(partial(cz.dicttoolz.itemmap, func))
|
|
77
|
+
|
|
78
|
+
def map_kv[KR, VR](
|
|
79
|
+
self,
|
|
80
|
+
func: Callable[[K, V], tuple[KR, VR]],
|
|
81
|
+
) -> Dict[KR, VR]:
|
|
82
|
+
"""
|
|
83
|
+
Transform (key, value) pairs using a function that takes key and value as separate arguments.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
func: Function to transform each key and value into a new (key, value) tuple.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
>>> import pyochain as pc
|
|
90
|
+
>>> pc.Dict({1: 2}).map_kv(lambda k, v: (k + 1, v * 10)).unwrap()
|
|
91
|
+
{2: 20}
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def _map_kv(data: dict[K, V]) -> dict[KR, VR]:
|
|
97
|
+
def _(kv: tuple[K, V]) -> tuple[KR, VR]:
|
|
98
|
+
return func(kv[0], kv[1])
|
|
99
|
+
|
|
100
|
+
return cz.dicttoolz.itemmap(_, data)
|
|
101
|
+
|
|
102
|
+
return self._new(_map_kv)
|
|
103
|
+
|
|
104
|
+
def invert(self) -> Dict[V, list[K]]:
|
|
105
|
+
"""
|
|
106
|
+
Invert the dictionary, grouping keys by common (and hashable) values.
|
|
107
|
+
```python
|
|
108
|
+
>>> import pyochain as pc
|
|
109
|
+
>>> d = {"a": 1, "b": 2, "c": 1}
|
|
110
|
+
>>> pc.Dict(d).invert().unwrap()
|
|
111
|
+
{1: ['a', 'c'], 2: ['b']}
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def _invert(data: dict[K, V]) -> dict[V, list[K]]:
|
|
117
|
+
inverted: dict[V, list[K]] = defaultdict(list)
|
|
118
|
+
for k, v in data.items():
|
|
119
|
+
inverted[v].append(k)
|
|
120
|
+
return dict(inverted)
|
|
121
|
+
|
|
122
|
+
return self._new(_invert)
|
|
123
|
+
|
|
124
|
+
def implode(self) -> Dict[K, list[V]]:
|
|
125
|
+
"""
|
|
126
|
+
Nest all the values in lists.
|
|
127
|
+
syntactic sugar for map_values(lambda v: [v])
|
|
128
|
+
```python
|
|
129
|
+
>>> import pyochain as pc
|
|
130
|
+
>>> pc.Dict({1: 2, 3: 4}).implode().unwrap()
|
|
131
|
+
{1: [2], 3: [4]}
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def _implode(data: dict[K, V]) -> dict[K, list[V]]:
|
|
137
|
+
def _(v: V) -> list[V]:
|
|
138
|
+
return [v]
|
|
139
|
+
|
|
140
|
+
return cz.dicttoolz.valmap(_, data)
|
|
141
|
+
|
|
142
|
+
return self._new(_implode)
|
pyochain/_dict/_nested.py
CHANGED
|
@@ -2,14 +2,44 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Mapping
|
|
4
4
|
from functools import partial
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Concatenate
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Concatenate, TypeIs
|
|
6
6
|
|
|
7
7
|
import cytoolz as cz
|
|
8
8
|
|
|
9
9
|
from .._core import MappingWrapper
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from
|
|
12
|
+
from ._main import Dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _drop_nones(
|
|
16
|
+
data: dict[Any, Any] | list[Any], remove_empty: bool = True
|
|
17
|
+
) -> dict[Any, Any] | list[Any] | None:
|
|
18
|
+
match data:
|
|
19
|
+
case dict():
|
|
20
|
+
pruned_dict: dict[Any, Any] = {}
|
|
21
|
+
for k, v in data.items():
|
|
22
|
+
pruned_v = _drop_nones(v, remove_empty)
|
|
23
|
+
|
|
24
|
+
is_empty = remove_empty and (pruned_v is None or pruned_v in ({}, []))
|
|
25
|
+
if not is_empty:
|
|
26
|
+
pruned_dict[k] = pruned_v
|
|
27
|
+
return pruned_dict if pruned_dict or not remove_empty else None
|
|
28
|
+
|
|
29
|
+
case list():
|
|
30
|
+
pruned_list = [_drop_nones(item, remove_empty) for item in data]
|
|
31
|
+
if remove_empty:
|
|
32
|
+
pruned_list = [
|
|
33
|
+
item
|
|
34
|
+
for item in pruned_list
|
|
35
|
+
if not (item is None or item in ({}, []))
|
|
36
|
+
]
|
|
37
|
+
return pruned_list if pruned_list or not remove_empty else None
|
|
38
|
+
|
|
39
|
+
case _:
|
|
40
|
+
if remove_empty and data is None:
|
|
41
|
+
return None
|
|
42
|
+
return data
|
|
13
43
|
|
|
14
44
|
|
|
15
45
|
class NestedDict[K, V](MappingWrapper[K, V]):
|
|
@@ -36,10 +66,8 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
36
66
|
... }
|
|
37
67
|
>>> pc.Dict(data).struct(lambda d: d.map_keys(str.upper).drop("AGE").unwrap())
|
|
38
68
|
... # doctest: +NORMALIZE_WHITESPACE
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'person2': {'NAME': 'Bob', 'CITY': 'Los Angeles'}
|
|
42
|
-
})
|
|
69
|
+
{'person1': {'CITY': 'New York', 'NAME': 'Alice'},
|
|
70
|
+
'person2': {'CITY': 'Los Angeles', 'NAME': 'Bob'}}
|
|
43
71
|
|
|
44
72
|
```
|
|
45
73
|
"""
|
|
@@ -51,7 +79,7 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
51
79
|
|
|
52
80
|
return cz.dicttoolz.valmap(_, data)
|
|
53
81
|
|
|
54
|
-
return self.
|
|
82
|
+
return self._new(_struct)
|
|
55
83
|
|
|
56
84
|
def flatten(
|
|
57
85
|
self: NestedDict[str, Any], sep: str = ".", max_depth: int | None = None
|
|
@@ -79,22 +107,52 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
79
107
|
"""
|
|
80
108
|
|
|
81
109
|
def _flatten(
|
|
82
|
-
d:
|
|
110
|
+
d: Mapping[Any, Any], parent_key: str = "", current_depth: int = 1
|
|
83
111
|
) -> dict[str, Any]:
|
|
112
|
+
def _can_recurse(v: object) -> TypeIs[Mapping[Any, Any]]:
|
|
113
|
+
return isinstance(v, Mapping) and (
|
|
114
|
+
max_depth is None or current_depth < max_depth + 1
|
|
115
|
+
)
|
|
116
|
+
|
|
84
117
|
items: list[tuple[str, Any]] = []
|
|
85
118
|
for k, v in d.items():
|
|
86
119
|
new_key = parent_key + sep + k if parent_key else k
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
):
|
|
90
|
-
items.extend(
|
|
91
|
-
_flatten(v, new_key, current_depth + 1).items() # type: ignore
|
|
92
|
-
)
|
|
120
|
+
if _can_recurse(v):
|
|
121
|
+
items.extend(_flatten(v, new_key, current_depth + 1).items())
|
|
93
122
|
else:
|
|
94
|
-
items.append((new_key, v))
|
|
123
|
+
items.append((new_key, v))
|
|
95
124
|
return dict(items)
|
|
96
125
|
|
|
97
|
-
return self.
|
|
126
|
+
return self._new(_flatten)
|
|
127
|
+
|
|
128
|
+
def unpivot(
|
|
129
|
+
self: NestedDict[str, Mapping[str, Any]],
|
|
130
|
+
) -> Dict[str, dict[str, Any]]:
|
|
131
|
+
"""
|
|
132
|
+
Unpivot a nested dictionary by swapping rows and columns.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
```python
|
|
136
|
+
>>> import pyochain as pc
|
|
137
|
+
>>> data = {
|
|
138
|
+
... "row1": {"col1": "A", "col2": "B"},
|
|
139
|
+
... "row2": {"col1": "C", "col2": "D"},
|
|
140
|
+
... }
|
|
141
|
+
>>> pc.Dict(data).unpivot()
|
|
142
|
+
... # doctest: +NORMALIZE_WHITESPACE
|
|
143
|
+
{'col1': {'row1': 'A', 'row2': 'C'}, 'col2': {'row1': 'B', 'row2': 'D'}}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def _unpivot(
|
|
147
|
+
data: Mapping[str, Mapping[str, Any]],
|
|
148
|
+
) -> dict[str, dict[str, Any]]:
|
|
149
|
+
out: dict[str, dict[str, Any]] = {}
|
|
150
|
+
for rkey, inner in data.items():
|
|
151
|
+
for ckey, val in inner.items():
|
|
152
|
+
out.setdefault(ckey, {})[rkey] = val
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
return self._new(_unpivot)
|
|
98
156
|
|
|
99
157
|
def with_nested_key(self, *keys: K, value: V) -> Dict[K, V]:
|
|
100
158
|
"""
|
|
@@ -117,56 +175,11 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
117
175
|
|
|
118
176
|
```
|
|
119
177
|
"""
|
|
120
|
-
return self.apply(cz.dicttoolz.assoc_in, keys, value=value)
|
|
121
|
-
|
|
122
|
-
def schema(self, max_depth: int = 1) -> Dict[str, Any]:
|
|
123
|
-
"""
|
|
124
|
-
Return the schema of the dictionary up to a maximum depth.
|
|
125
178
|
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
def _with_nested_key(data: dict[K, V]) -> dict[K, V]:
|
|
180
|
+
return cz.dicttoolz.assoc_in(data, keys, value=value)
|
|
128
181
|
|
|
129
|
-
|
|
130
|
-
For lists, only the first element is inspected.
|
|
131
|
-
```python
|
|
132
|
-
>>> import pyochain as pc
|
|
133
|
-
>>> # Depth 2: we see up to level2
|
|
134
|
-
>>> data = {
|
|
135
|
-
... "level1": {"level2": {"level3": {"key": "value"}}},
|
|
136
|
-
... "other_key": 123,
|
|
137
|
-
... "list_key": [{"sub_key": "sub_value"}],
|
|
138
|
-
... }
|
|
139
|
-
>>> pc.Dict(data).schema(max_depth=1).unwrap()
|
|
140
|
-
{'level1': 'dict', 'other_key': 'int', 'list_key': 'list'}
|
|
141
|
-
>>> pc.Dict(data).schema(max_depth=2).unwrap()
|
|
142
|
-
{'level1': {'level2': 'dict'}, 'other_key': 'int', 'list_key': 'dict'}
|
|
143
|
-
>>>
|
|
144
|
-
>>> # Depth 3: we see up to level3
|
|
145
|
-
>>> pc.Dict(data).schema(max_depth=3).unwrap()
|
|
146
|
-
{'level1': {'level2': {'level3': 'dict'}}, 'other_key': 'int', 'list_key': {'sub_key': 'str'}}
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
def _schema(data: dict[Any, Any]) -> Any:
|
|
152
|
-
def _recurse_schema(node: Any, current_depth: int) -> Any:
|
|
153
|
-
if isinstance(node, dict):
|
|
154
|
-
if current_depth >= max_depth:
|
|
155
|
-
return "dict"
|
|
156
|
-
return {
|
|
157
|
-
k: _recurse_schema(v, current_depth + 1)
|
|
158
|
-
for k, v in node.items() # type: ignore
|
|
159
|
-
}
|
|
160
|
-
elif cz.itertoolz.isiterable(node):
|
|
161
|
-
if current_depth >= max_depth:
|
|
162
|
-
return type(node).__name__
|
|
163
|
-
return _recurse_schema(cz.itertoolz.first(node), current_depth + 1)
|
|
164
|
-
else:
|
|
165
|
-
return type(node).__name__
|
|
166
|
-
|
|
167
|
-
return _recurse_schema(data, 0)
|
|
168
|
-
|
|
169
|
-
return self.apply(_schema)
|
|
182
|
+
return self._new(_with_nested_key)
|
|
170
183
|
|
|
171
184
|
def pluck[U: str | int](self: NestedDict[U, Any], *keys: str) -> Dict[U, Any]:
|
|
172
185
|
"""
|
|
@@ -191,7 +204,7 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
191
204
|
def _pluck(data: Mapping[U, Any]) -> dict[U, Any]:
|
|
192
205
|
return cz.dicttoolz.valmap(getter, data)
|
|
193
206
|
|
|
194
|
-
return self.
|
|
207
|
+
return self._new(_pluck)
|
|
195
208
|
|
|
196
209
|
def get_in(self, *keys: K, default: Any = None) -> Any:
|
|
197
210
|
"""
|
|
@@ -216,3 +229,44 @@ class NestedDict[K, V](MappingWrapper[K, V]):
|
|
|
216
229
|
return cz.dicttoolz.get_in(keys, data, default)
|
|
217
230
|
|
|
218
231
|
return self.into(_get_in)
|
|
232
|
+
|
|
233
|
+
def drop_nones(self, remove_empty: bool = True) -> Dict[K, V]:
|
|
234
|
+
"""
|
|
235
|
+
Recursively drop None values from the dictionary.
|
|
236
|
+
|
|
237
|
+
Options to also remove empty dicts and lists.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
remove_empty: If True (default), removes `None`, `{}` and `[]`.
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
```python
|
|
244
|
+
>>> import pyochain as pc
|
|
245
|
+
>>> data = {
|
|
246
|
+
... "a": 1,
|
|
247
|
+
... "b": None,
|
|
248
|
+
... "c": {},
|
|
249
|
+
... "d": [],
|
|
250
|
+
... "e": {"f": None, "g": 2},
|
|
251
|
+
... "h": [1, None, {}],
|
|
252
|
+
... "i": 0,
|
|
253
|
+
... }
|
|
254
|
+
>>> p_data = pc.Dict(data)
|
|
255
|
+
>>>
|
|
256
|
+
>>> p_data.drop_nones().unwrap()
|
|
257
|
+
{'a': 1, 'e': {'g': 2}, 'h': [1], 'i': 0}
|
|
258
|
+
>>>
|
|
259
|
+
>>> p_data.drop_nones().unwrap()
|
|
260
|
+
{'a': 1, 'e': {'g': 2}, 'h': [1], 'i': 0}
|
|
261
|
+
>>>
|
|
262
|
+
>>> p_data.drop_nones(remove_empty=False).unwrap()
|
|
263
|
+
{'a': 1, 'b': None, 'c': {}, 'd': [], 'e': {'f': None, 'g': 2}, 'h': [1, None, {}], 'i': 0}
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def _apply_drop_nones(data: dict[K, V]) -> dict[Any, Any]:
|
|
269
|
+
result = _drop_nones(data, remove_empty)
|
|
270
|
+
return result if isinstance(result, dict) else dict()
|
|
271
|
+
|
|
272
|
+
return self._new(_apply_drop_nones)
|