pyochain 0.5.0__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 +5 -0
- pyochain/_core/__init__.py +21 -0
- pyochain/_core/_main.py +184 -0
- pyochain/_core/_protocols.py +43 -0
- pyochain/_dict/__init__.py +4 -0
- pyochain/_dict/_exprs.py +115 -0
- pyochain/_dict/_filters.py +273 -0
- pyochain/_dict/_funcs.py +62 -0
- pyochain/_dict/_groups.py +176 -0
- pyochain/_dict/_iter.py +92 -0
- pyochain/_dict/_joins.py +137 -0
- pyochain/_dict/_main.py +307 -0
- pyochain/_dict/_nested.py +218 -0
- pyochain/_dict/_process.py +171 -0
- pyochain/_iter/__init__.py +3 -0
- pyochain/_iter/_aggregations.py +323 -0
- pyochain/_iter/_booleans.py +224 -0
- pyochain/_iter/_constructors.py +155 -0
- pyochain/_iter/_eager.py +195 -0
- pyochain/_iter/_filters.py +503 -0
- pyochain/_iter/_groups.py +264 -0
- pyochain/_iter/_joins.py +407 -0
- pyochain/_iter/_lists.py +306 -0
- pyochain/_iter/_main.py +224 -0
- pyochain/_iter/_maps.py +358 -0
- pyochain/_iter/_partitions.py +148 -0
- pyochain/_iter/_process.py +384 -0
- pyochain/_iter/_rolling.py +247 -0
- pyochain/_iter/_tuples.py +221 -0
- pyochain/py.typed +0 -0
- pyochain-0.5.0.dist-info/METADATA +295 -0
- pyochain-0.5.0.dist-info/RECORD +33 -0
- pyochain-0.5.0.dist-info/WHEEL +4 -0
pyochain/_dict/_main.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
import cytoolz as cz
|
|
9
|
+
|
|
10
|
+
from .._core import SupportsKeysAndGetItem
|
|
11
|
+
from ._exprs import IntoExpr, compute_exprs
|
|
12
|
+
from ._filters import FilterDict
|
|
13
|
+
from ._funcs import dict_repr
|
|
14
|
+
from ._groups import GroupsDict
|
|
15
|
+
from ._iter import IterDict
|
|
16
|
+
from ._joins import JoinsDict
|
|
17
|
+
from ._nested import NestedDict
|
|
18
|
+
from ._process import ProcessDict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Dict[K, V](
|
|
22
|
+
ProcessDict[K, V],
|
|
23
|
+
IterDict[K, V],
|
|
24
|
+
NestedDict[K, V],
|
|
25
|
+
JoinsDict[K, V],
|
|
26
|
+
FilterDict[K, V],
|
|
27
|
+
GroupsDict[K, V],
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Wrapper for Python dictionaries with chainable methods.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__slots__ = ()
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"{self.__class__.__name__}({dict_repr(self.unwrap())})"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_[G, I](data: Mapping[G, I] | SupportsKeysAndGetItem[G, I]) -> Dict[G, I]:
|
|
40
|
+
"""
|
|
41
|
+
Create a Dict from a mapping or SupportsKeysAndGetItem.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: A mapping or object supporting keys and item access to convert into a Dict.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
>>> import pyochain as pc
|
|
48
|
+
>>> class MyMapping:
|
|
49
|
+
... def __init__(self):
|
|
50
|
+
... self._data = {1: "a", 2: "b", 3: "c"}
|
|
51
|
+
...
|
|
52
|
+
... def keys(self):
|
|
53
|
+
... return self._data.keys()
|
|
54
|
+
...
|
|
55
|
+
... def __getitem__(self, key):
|
|
56
|
+
... return self._data[key]
|
|
57
|
+
>>>
|
|
58
|
+
>>> pc.Dict.from_(MyMapping()).unwrap()
|
|
59
|
+
{1: 'a', 2: 'b', 3: 'c'}
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
return Dict(dict(data))
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def from_object(obj: object) -> Dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Create a Dict from an object's __dict__ attribute.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
obj: The object whose `__dict__` attribute will be used to create the Dict.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
>>> import pyochain as pc
|
|
75
|
+
>>> class Person:
|
|
76
|
+
... def __init__(self, name: str, age: int):
|
|
77
|
+
... self.name = name
|
|
78
|
+
... self.age = age
|
|
79
|
+
>>> person = Person("Alice", 30)
|
|
80
|
+
>>> pc.Dict.from_object(person).unwrap()
|
|
81
|
+
{'name': 'Alice', 'age': 30}
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
"""
|
|
85
|
+
return Dict(obj.__dict__)
|
|
86
|
+
|
|
87
|
+
def select(self: Dict[str, Any], *exprs: IntoExpr) -> Dict[str, Any]:
|
|
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
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
return self.apply(partial(cz.dicttoolz.valmap, func))
|
|
195
|
+
|
|
196
|
+
def map_items[KR, VR](
|
|
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.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
func: Function to transform each (key, value) pair into a new (key, value) tuple.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
>>> import pyochain as pc
|
|
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.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
```python
|
|
292
|
+
>>> import pyochain as pc
|
|
293
|
+
>>> d1 = pc.Dict({"a": 1, "b": 2})
|
|
294
|
+
>>> d2 = pc.Dict({"a": 1, "b": 2})
|
|
295
|
+
>>> d3 = pc.Dict({"a": 1, "b": 3})
|
|
296
|
+
>>> d1.equals_to(d2)
|
|
297
|
+
True
|
|
298
|
+
>>> d1.equals_to(d3)
|
|
299
|
+
False
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
"""
|
|
303
|
+
return (
|
|
304
|
+
self.unwrap() == other.unwrap()
|
|
305
|
+
if isinstance(other, Dict)
|
|
306
|
+
else self.unwrap() == other
|
|
307
|
+
)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Concatenate
|
|
6
|
+
|
|
7
|
+
import cytoolz as cz
|
|
8
|
+
|
|
9
|
+
from .._core import MappingWrapper
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .._dict import Dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NestedDict[K, V](MappingWrapper[K, V]):
|
|
16
|
+
def struct[**P, R, U: dict[Any, Any]](
|
|
17
|
+
self: NestedDict[K, U],
|
|
18
|
+
func: Callable[Concatenate[Dict[K, U], P], R],
|
|
19
|
+
*args: P.args,
|
|
20
|
+
**kwargs: P.kwargs,
|
|
21
|
+
) -> Dict[K, R]:
|
|
22
|
+
"""
|
|
23
|
+
Apply a function to each value after wrapping it in a Dict.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
func: Function to apply to each value after wrapping it in a Dict.
|
|
27
|
+
*args: Positional arguments to pass to the function.
|
|
28
|
+
**kwargs: Keyword arguments to pass to the function.
|
|
29
|
+
|
|
30
|
+
Syntactic sugar for `map_values(lambda data: func(pc.Dict(data), *args, **kwargs))`
|
|
31
|
+
```python
|
|
32
|
+
>>> import pyochain as pc
|
|
33
|
+
>>> data = {
|
|
34
|
+
... "person1": {"name": "Alice", "age": 30, "city": "New York"},
|
|
35
|
+
... "person2": {"name": "Bob", "age": 25, "city": "Los Angeles"},
|
|
36
|
+
... }
|
|
37
|
+
>>> pc.Dict(data).struct(lambda d: d.map_keys(str.upper).drop("AGE").unwrap())
|
|
38
|
+
... # doctest: +NORMALIZE_WHITESPACE
|
|
39
|
+
Dict({
|
|
40
|
+
'person1': {'NAME': 'Alice', 'CITY': 'New York'},
|
|
41
|
+
'person2': {'NAME': 'Bob', 'CITY': 'Los Angeles'}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
from ._main import Dict
|
|
47
|
+
|
|
48
|
+
def _struct(data: Mapping[K, U]) -> dict[K, R]:
|
|
49
|
+
def _(v: dict[Any, Any]) -> R:
|
|
50
|
+
return func(Dict(v), *args, **kwargs)
|
|
51
|
+
|
|
52
|
+
return cz.dicttoolz.valmap(_, data)
|
|
53
|
+
|
|
54
|
+
return self.apply(_struct)
|
|
55
|
+
|
|
56
|
+
def flatten(
|
|
57
|
+
self: NestedDict[str, Any], sep: str = ".", max_depth: int | None = None
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Flatten a nested dictionary, concatenating keys with the specified separator.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
sep: Separator to use when concatenating keys
|
|
64
|
+
max_depth: Maximum depth to flatten. If None, flattens completely.
|
|
65
|
+
```python
|
|
66
|
+
>>> import pyochain as pc
|
|
67
|
+
>>> data = {
|
|
68
|
+
... "config": {"params": {"retries": 3, "timeout": 30}, "mode": "fast"},
|
|
69
|
+
... "version": 1.0,
|
|
70
|
+
... }
|
|
71
|
+
>>> pc.Dict(data).flatten().unwrap()
|
|
72
|
+
{'config.params.retries': 3, 'config.params.timeout': 30, 'config.mode': 'fast', 'version': 1.0}
|
|
73
|
+
>>> pc.Dict(data).flatten(sep="_").unwrap()
|
|
74
|
+
{'config_params_retries': 3, 'config_params_timeout': 30, 'config_mode': 'fast', 'version': 1.0}
|
|
75
|
+
>>> pc.Dict(data).flatten(max_depth=1).unwrap()
|
|
76
|
+
{'config.params': {'retries': 3, 'timeout': 30}, 'config.mode': 'fast', 'version': 1.0}
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def _flatten(
|
|
82
|
+
d: dict[Any, Any], parent_key: str = "", current_depth: int = 1
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
items: list[tuple[str, Any]] = []
|
|
85
|
+
for k, v in d.items():
|
|
86
|
+
new_key = parent_key + sep + k if parent_key else k
|
|
87
|
+
if isinstance(v, dict) and (
|
|
88
|
+
max_depth is None or current_depth < max_depth + 1
|
|
89
|
+
):
|
|
90
|
+
items.extend(
|
|
91
|
+
_flatten(v, new_key, current_depth + 1).items() # type: ignore
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
items.append((new_key, v)) # type: ignore
|
|
95
|
+
return dict(items)
|
|
96
|
+
|
|
97
|
+
return self.apply(_flatten)
|
|
98
|
+
|
|
99
|
+
def with_nested_key(self, *keys: K, value: V) -> Dict[K, V]:
|
|
100
|
+
"""
|
|
101
|
+
Set a nested key path and return a new Dict with new, potentially nested, key value pair.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
*keys: Sequence of keys representing the nested path.
|
|
105
|
+
value: Value to set at the specified nested path.
|
|
106
|
+
```python
|
|
107
|
+
>>> import pyochain as pc
|
|
108
|
+
>>> purchase = {
|
|
109
|
+
... "name": "Alice",
|
|
110
|
+
... "order": {"items": ["Apple", "Orange"], "costs": [0.50, 1.25]},
|
|
111
|
+
... "credit card": "5555-1234-1234-1234",
|
|
112
|
+
... }
|
|
113
|
+
>>> pc.Dict(purchase).with_nested_key(
|
|
114
|
+
... "order", "costs", value=[0.25, 1.00]
|
|
115
|
+
... ).unwrap()
|
|
116
|
+
{'name': 'Alice', 'order': {'items': ['Apple', 'Orange'], 'costs': [0.25, 1.0]}, 'credit card': '5555-1234-1234-1234'}
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
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
|
+
|
|
126
|
+
Args:
|
|
127
|
+
max_depth: Maximum depth to inspect. Nested dicts beyond this depth are marked as 'dict'.
|
|
128
|
+
|
|
129
|
+
When the max depth is reached, nested dicts are marked as 'dict'.
|
|
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)
|
|
170
|
+
|
|
171
|
+
def pluck[U: str | int](self: NestedDict[U, Any], *keys: str) -> Dict[U, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Extract values from nested dictionaries using a sequence of keys.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
*keys: Sequence of keys to extract values from the nested dictionaries.
|
|
177
|
+
```python
|
|
178
|
+
>>> import pyochain as pc
|
|
179
|
+
>>> data = {
|
|
180
|
+
... "person1": {"name": "Alice", "age": 30},
|
|
181
|
+
... "person2": {"name": "Bob", "age": 25},
|
|
182
|
+
... }
|
|
183
|
+
>>> pc.Dict(data).pluck("name").unwrap()
|
|
184
|
+
{'person1': 'Alice', 'person2': 'Bob'}
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
getter = partial(cz.dicttoolz.get_in, keys)
|
|
190
|
+
|
|
191
|
+
def _pluck(data: Mapping[U, Any]) -> dict[U, Any]:
|
|
192
|
+
return cz.dicttoolz.valmap(getter, data)
|
|
193
|
+
|
|
194
|
+
return self.apply(_pluck)
|
|
195
|
+
|
|
196
|
+
def get_in(self, *keys: K, default: Any = None) -> Any:
|
|
197
|
+
"""
|
|
198
|
+
Retrieve a value from a nested dictionary structure.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
*keys: Sequence of keys representing the nested path to retrieve the value.
|
|
202
|
+
default: Default value to return if the keys do not exist.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
>>> import pyochain as pc
|
|
206
|
+
>>> data = {"a": {"b": {"c": 1}}}
|
|
207
|
+
>>> pc.Dict(data).get_in("a", "b", "c")
|
|
208
|
+
1
|
|
209
|
+
>>> pc.Dict(data).get_in("a", "x", default="Not Found")
|
|
210
|
+
'Not Found'
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def _get_in(data: Mapping[K, V]) -> Any:
|
|
216
|
+
return cz.dicttoolz.get_in(keys, data, default)
|
|
217
|
+
|
|
218
|
+
return self.into(_get_in)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Concatenate
|
|
5
|
+
|
|
6
|
+
import cytoolz as cz
|
|
7
|
+
|
|
8
|
+
from .._core import MappingWrapper
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ._main import Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessDict[K, V](MappingWrapper[K, V]):
|
|
15
|
+
def for_each[**P](
|
|
16
|
+
self,
|
|
17
|
+
func: Callable[Concatenate[K, V, P], Any],
|
|
18
|
+
*args: P.args,
|
|
19
|
+
**kwargs: P.kwargs,
|
|
20
|
+
) -> Dict[K, V]:
|
|
21
|
+
"""
|
|
22
|
+
Apply a function to each key-value pair in the dict for side effects.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
func: Function to apply to each key-value pair.
|
|
26
|
+
*args: Positional arguments to pass to the function.
|
|
27
|
+
**kwargs: Keyword arguments to pass to the function.
|
|
28
|
+
|
|
29
|
+
Returns the original Dict unchanged.
|
|
30
|
+
```python
|
|
31
|
+
>>> import pyochain as pc
|
|
32
|
+
>>> pc.Dict({"a": 1, "b": 2}).for_each(
|
|
33
|
+
... lambda k, v: print(f"Key: {k}, Value: {v}")
|
|
34
|
+
... ).unwrap()
|
|
35
|
+
Key: a, Value: 1
|
|
36
|
+
Key: b, Value: 2
|
|
37
|
+
{'a': 1, 'b': 2}
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def _for_each(data: dict[K, V]) -> dict[K, V]:
|
|
43
|
+
for k, v in data.items():
|
|
44
|
+
func(k, v, *args, **kwargs)
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
return self.apply(_for_each)
|
|
48
|
+
|
|
49
|
+
def update_in(
|
|
50
|
+
self, *keys: K, func: Callable[[V], V], default: V | None = None
|
|
51
|
+
) -> Dict[K, V]:
|
|
52
|
+
"""
|
|
53
|
+
Update value in a (potentially) nested dictionary.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
*keys: Sequence of keys representing the nested path to update.
|
|
57
|
+
func: Function to apply to the value at the specified path.
|
|
58
|
+
default: Default value to use if the path does not exist, by default None
|
|
59
|
+
|
|
60
|
+
Applies the func to the value at the path specified by keys, returning a new Dict with the updated value.
|
|
61
|
+
|
|
62
|
+
If the path does not exist, it will be created with the default value (if provided) before applying func.
|
|
63
|
+
```python
|
|
64
|
+
>>> import pyochain as pc
|
|
65
|
+
>>> inc = lambda x: x + 1
|
|
66
|
+
>>> pc.Dict({"a": 0}).update_in("a", func=inc).unwrap()
|
|
67
|
+
{'a': 1}
|
|
68
|
+
>>> transaction = {
|
|
69
|
+
... "name": "Alice",
|
|
70
|
+
... "purchase": {"items": ["Apple", "Orange"], "costs": [0.50, 1.25]},
|
|
71
|
+
... "credit card": "5555-1234-1234-1234",
|
|
72
|
+
... }
|
|
73
|
+
>>> pc.Dict(transaction).update_in("purchase", "costs", func=sum).unwrap()
|
|
74
|
+
{'name': 'Alice', 'purchase': {'items': ['Apple', 'Orange'], 'costs': 1.75}, 'credit card': '5555-1234-1234-1234'}
|
|
75
|
+
>>> # updating a value when k0 is not in d
|
|
76
|
+
>>> pc.Dict({}).update_in(1, 2, 3, func=str, default="bar").unwrap()
|
|
77
|
+
{1: {2: {3: 'bar'}}}
|
|
78
|
+
>>> pc.Dict({1: "foo"}).update_in(2, 3, 4, func=inc, default=0).unwrap()
|
|
79
|
+
{1: 'foo', 2: {3: {4: 1}}}
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
"""
|
|
83
|
+
return self.apply(cz.dicttoolz.update_in, keys, func, default=default)
|
|
84
|
+
|
|
85
|
+
def with_key(self, key: K, value: V) -> Dict[K, V]:
|
|
86
|
+
"""
|
|
87
|
+
Return a new Dict with key set to value.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
key: Key to set in the dictionary.
|
|
91
|
+
value: Value to associate with the specified key.
|
|
92
|
+
|
|
93
|
+
Does not modify the initial dictionary.
|
|
94
|
+
```python
|
|
95
|
+
>>> import pyochain as pc
|
|
96
|
+
>>> pc.Dict({"x": 1}).with_key("x", 2).unwrap()
|
|
97
|
+
{'x': 2}
|
|
98
|
+
>>> pc.Dict({"x": 1}).with_key("y", 3).unwrap()
|
|
99
|
+
{'x': 1, 'y': 3}
|
|
100
|
+
>>> pc.Dict({}).with_key("x", 1).unwrap()
|
|
101
|
+
{'x': 1}
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
"""
|
|
105
|
+
return self.apply(cz.dicttoolz.assoc, key, value)
|
|
106
|
+
|
|
107
|
+
def drop(self, *keys: K) -> Dict[K, V]:
|
|
108
|
+
"""
|
|
109
|
+
Return a new Dict with given keys removed.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
*keys: Sequence of keys to remove from the dictionary.
|
|
113
|
+
|
|
114
|
+
New dict has d[key] deleted for each supplied key.
|
|
115
|
+
```python
|
|
116
|
+
>>> import pyochain as pc
|
|
117
|
+
>>> pc.Dict({"x": 1, "y": 2}).drop("y").unwrap()
|
|
118
|
+
{'x': 1}
|
|
119
|
+
>>> pc.Dict({"x": 1, "y": 2}).drop("y", "x").unwrap()
|
|
120
|
+
{}
|
|
121
|
+
>>> pc.Dict({"x": 1}).drop("y").unwrap() # Ignores missing keys
|
|
122
|
+
{'x': 1}
|
|
123
|
+
>>> pc.Dict({1: 2, 3: 4}).drop(1).unwrap()
|
|
124
|
+
{3: 4}
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
"""
|
|
128
|
+
return self.apply(cz.dicttoolz.dissoc, *keys)
|
|
129
|
+
|
|
130
|
+
def rename(self, mapping: Mapping[K, K]) -> Dict[K, V]:
|
|
131
|
+
"""
|
|
132
|
+
Return a new Dict with keys renamed according to the mapping.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
mapping: A dictionary mapping old keys to new keys.
|
|
136
|
+
|
|
137
|
+
Keys not in the mapping are kept as is.
|
|
138
|
+
```python
|
|
139
|
+
>>> import pyochain as pc
|
|
140
|
+
>>> d = {"a": 1, "b": 2, "c": 3}
|
|
141
|
+
>>> mapping = {"b": "beta", "c": "gamma"}
|
|
142
|
+
>>> pc.Dict(d).rename(mapping).unwrap()
|
|
143
|
+
{'a': 1, 'beta': 2, 'gamma': 3}
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def _rename(data: dict[K, V]) -> dict[K, V]:
|
|
149
|
+
return {mapping.get(k, k): v for k, v in data.items()}
|
|
150
|
+
|
|
151
|
+
return self.apply(_rename)
|
|
152
|
+
|
|
153
|
+
def sort(self, reverse: bool = False) -> Dict[K, V]:
|
|
154
|
+
"""
|
|
155
|
+
Sort the dictionary by its keys and return a new Dict.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
reverse: Whether to sort in descending order. Defaults to False.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
>>> import pyochain as pc
|
|
162
|
+
>>> pc.Dict({"b": 2, "a": 1}).sort().unwrap()
|
|
163
|
+
{'a': 1, 'b': 2}
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def _sort(data: dict[K, V]) -> dict[K, V]:
|
|
169
|
+
return dict(sorted(data.items(), reverse=reverse))
|
|
170
|
+
|
|
171
|
+
return self.apply(_sort)
|