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,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
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 GroupsDict[K, V](MappingWrapper[K, V]):
|
|
15
|
+
def group_by_value[G](self, func: Callable[[V], G]) -> Dict[G, dict[K, V]]:
|
|
16
|
+
"""
|
|
17
|
+
Group dict items into sub-dictionaries based on a function of the value.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
func: Function to determine the group for each value.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
>>> import pyochain as pc
|
|
24
|
+
>>> d = {"a": 1, "b": 2, "c": 3, "d": 2}
|
|
25
|
+
>>> pc.Dict(d).group_by_value(lambda v: v % 2).unwrap()
|
|
26
|
+
{1: {'a': 1, 'c': 3}, 0: {'b': 2, 'd': 2}}
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def _group_by_value(data: dict[K, V]) -> dict[G, dict[K, V]]:
|
|
32
|
+
def _(kv: tuple[K, V]) -> G:
|
|
33
|
+
return func(kv[1])
|
|
34
|
+
|
|
35
|
+
return cz.dicttoolz.valmap(dict, cz.itertoolz.groupby(_, data.items()))
|
|
36
|
+
|
|
37
|
+
return self._new(_group_by_value)
|
|
38
|
+
|
|
39
|
+
def group_by_key[G](self, func: Callable[[K], G]) -> Dict[G, dict[K, V]]:
|
|
40
|
+
"""
|
|
41
|
+
Group dict items into sub-dictionaries based on a function of the key.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
func: Function to determine the group for each key.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
>>> import pyochain as pc
|
|
48
|
+
>>> d = {"user_1": 10, "user_2": 20, "admin_1": 100}
|
|
49
|
+
>>> pc.Dict(d).group_by_key(lambda k: k.split("_")[0]).unwrap()
|
|
50
|
+
{'user': {'user_1': 10, 'user_2': 20}, 'admin': {'admin_1': 100}}
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def _group_by_key(data: dict[K, V]) -> dict[G, dict[K, V]]:
|
|
56
|
+
def _(kv: tuple[K, V]) -> G:
|
|
57
|
+
return func(kv[0])
|
|
58
|
+
|
|
59
|
+
return cz.dicttoolz.valmap(dict, cz.itertoolz.groupby(_, data.items()))
|
|
60
|
+
|
|
61
|
+
return self._new(_group_by_key)
|
|
62
|
+
|
|
63
|
+
def group_by_key_agg[G, R](
|
|
64
|
+
self,
|
|
65
|
+
key_func: Callable[[K], G],
|
|
66
|
+
agg_func: Callable[[Dict[K, V]], R],
|
|
67
|
+
) -> Dict[G, R]:
|
|
68
|
+
"""
|
|
69
|
+
Group by key function, then apply aggregation function to each sub-dict.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
key_func: Function to determine the group for each key.
|
|
73
|
+
agg_func: Function to aggregate each sub-dictionary.
|
|
74
|
+
|
|
75
|
+
This avoids materializing intermediate `dict` objects if you only need
|
|
76
|
+
an aggregated result for each group.
|
|
77
|
+
```python
|
|
78
|
+
>>> import pyochain as pc
|
|
79
|
+
>>>
|
|
80
|
+
>>> data = {"user_1": 10, "user_2": 20, "admin_1": 100}
|
|
81
|
+
>>> pc.Dict(data).group_by_key_agg(
|
|
82
|
+
... key_func=lambda k: k.split("_")[0],
|
|
83
|
+
... agg_func=lambda d: d.iter_values().sum(),
|
|
84
|
+
... ).unwrap()
|
|
85
|
+
{'user': 30, 'admin': 100}
|
|
86
|
+
>>>
|
|
87
|
+
>>> data_files = {
|
|
88
|
+
... "file_a.txt": 100,
|
|
89
|
+
... "file_b.log": 20,
|
|
90
|
+
... "file_c.txt": 50,
|
|
91
|
+
... "file_d.log": 5,
|
|
92
|
+
... }
|
|
93
|
+
>>>
|
|
94
|
+
>>> def get_stats(sub_dict: pc.Dict[str, int]) -> dict[str, Any]:
|
|
95
|
+
... return {
|
|
96
|
+
... "count": sub_dict.iter_keys().count(),
|
|
97
|
+
... "total_size": sub_dict.iter_values().sum(),
|
|
98
|
+
... "max_size": sub_dict.iter_values().max(),
|
|
99
|
+
... "files": sub_dict.iter_keys().sort().into(list),
|
|
100
|
+
... }
|
|
101
|
+
>>>
|
|
102
|
+
>>> pc.Dict(data_files).group_by_key_agg(
|
|
103
|
+
... key_func=lambda k: k.split(".")[-1], agg_func=get_stats
|
|
104
|
+
... ).sort().unwrap()
|
|
105
|
+
{'log': {'count': 2, 'total_size': 25, 'max_size': 20, 'files': ['file_b.log', 'file_d.log']}, 'txt': {'count': 2, 'total_size': 150, 'max_size': 100, 'files': ['file_a.txt', 'file_c.txt']}}
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
"""
|
|
109
|
+
from ._main import Dict
|
|
110
|
+
|
|
111
|
+
def _group_by_key_agg(data: dict[K, V]) -> dict[G, R]:
|
|
112
|
+
def _key_func(kv: tuple[K, V]) -> G:
|
|
113
|
+
return key_func(kv[0])
|
|
114
|
+
|
|
115
|
+
def _agg_func(items: list[tuple[K, V]]) -> R:
|
|
116
|
+
return agg_func(Dict(dict(items)))
|
|
117
|
+
|
|
118
|
+
groups = cz.itertoolz.groupby(_key_func, data.items())
|
|
119
|
+
return cz.dicttoolz.valmap(_agg_func, groups)
|
|
120
|
+
|
|
121
|
+
return self._new(_group_by_key_agg)
|
|
122
|
+
|
|
123
|
+
def group_by_value_agg[G, R](
|
|
124
|
+
self,
|
|
125
|
+
value_func: Callable[[V], G],
|
|
126
|
+
agg_func: Callable[[Dict[K, V]], R],
|
|
127
|
+
) -> Dict[G, R]:
|
|
128
|
+
"""
|
|
129
|
+
Group by value function, then apply aggregation function to each sub-dict.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value_func: Function to determine the group for each value.
|
|
133
|
+
agg_func: Function to aggregate each sub-dictionary.
|
|
134
|
+
|
|
135
|
+
This avoids materializing intermediate `dict` objects if you only need
|
|
136
|
+
an aggregated result for each group.
|
|
137
|
+
```python
|
|
138
|
+
>>> import pyochain as pc
|
|
139
|
+
>>>
|
|
140
|
+
>>> data = {"math": "A", "physics": "B", "english": "A"}
|
|
141
|
+
>>> pc.Dict(data).group_by_value_agg(
|
|
142
|
+
... value_func=lambda grade: grade,
|
|
143
|
+
... agg_func=lambda d: d.iter_keys().count(),
|
|
144
|
+
... ).unwrap()
|
|
145
|
+
{'A': 2, 'B': 1}
|
|
146
|
+
>>> # Second example
|
|
147
|
+
>>> sales_data = {
|
|
148
|
+
... "store_1": "Electronics",
|
|
149
|
+
... "store_2": "Groceries",
|
|
150
|
+
... "store_3": "Electronics",
|
|
151
|
+
... "store_4": "Clothing",
|
|
152
|
+
... }
|
|
153
|
+
>>>
|
|
154
|
+
>>> # Obtain the first store for each category (after sorting store names)
|
|
155
|
+
>>> pc.Dict(sales_data).group_by_value_agg(
|
|
156
|
+
... value_func=lambda category: category,
|
|
157
|
+
... agg_func=lambda d: d.iter_keys().sort().first(),
|
|
158
|
+
... ).sort().unwrap()
|
|
159
|
+
{'Clothing': 'store_4', 'Electronics': 'store_1', 'Groceries': 'store_2'}
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
"""
|
|
163
|
+
from ._main import Dict
|
|
164
|
+
|
|
165
|
+
def _group_by_value_agg(data: dict[K, V]) -> dict[G, R]:
|
|
166
|
+
def _key_func(kv: tuple[K, V]) -> G:
|
|
167
|
+
return value_func(kv[1])
|
|
168
|
+
|
|
169
|
+
def _agg_func(items: list[tuple[K, V]]) -> R:
|
|
170
|
+
return agg_func(Dict(dict(items)))
|
|
171
|
+
|
|
172
|
+
groups = cz.itertoolz.groupby(_key_func, data.items())
|
|
173
|
+
return cz.dicttoolz.valmap(_agg_func, groups)
|
|
174
|
+
|
|
175
|
+
return self._new(_group_by_value_agg)
|
pyochain/_dict/_iter.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, 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 .._iter import Iter, Seq
|
|
12
|
+
from ._main import Dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IterDict[K, V](MappingWrapper[K, V]):
|
|
16
|
+
def itr[**P, R, U](
|
|
17
|
+
self: MappingWrapper[K, Iterable[U]],
|
|
18
|
+
func: Callable[Concatenate[Iter[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 an Iter.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
func: Function to apply to each value after wrapping it in an Iter.
|
|
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(Iter(data), *args, **kwargs))`
|
|
31
|
+
```python
|
|
32
|
+
>>> import pyochain as pc
|
|
33
|
+
>>> data = {
|
|
34
|
+
... "numbers1": [1, 2, 3],
|
|
35
|
+
... "numbers2": [4, 5, 6],
|
|
36
|
+
... }
|
|
37
|
+
>>> pc.Dict(data).itr(lambda v: v.repeat(5).flatten().sum()).unwrap()
|
|
38
|
+
{'numbers1': 30, 'numbers2': 75}
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
from .._iter import Iter
|
|
43
|
+
|
|
44
|
+
def _itr(data: Mapping[K, Iterable[U]]) -> dict[K, R]:
|
|
45
|
+
def _(v: Iterable[U]) -> R:
|
|
46
|
+
return func(Iter(iter(v)), *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return cz.dicttoolz.valmap(_, data)
|
|
49
|
+
|
|
50
|
+
return self._new(_itr)
|
|
51
|
+
|
|
52
|
+
def iter_keys(self) -> Iter[K]:
|
|
53
|
+
"""
|
|
54
|
+
Return an Iter of the dict's keys.
|
|
55
|
+
```python
|
|
56
|
+
>>> import pyochain as pc
|
|
57
|
+
>>> pc.Dict({1: 2}).iter_keys().into(list)
|
|
58
|
+
[1]
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
from .._iter import Iter
|
|
63
|
+
|
|
64
|
+
def _keys(data: dict[K, V]) -> Iter[K]:
|
|
65
|
+
return Iter(iter(data.keys()))
|
|
66
|
+
|
|
67
|
+
return self.into(_keys)
|
|
68
|
+
|
|
69
|
+
def iter_values(self) -> Iter[V]:
|
|
70
|
+
"""
|
|
71
|
+
Return an Iter of the dict's values.
|
|
72
|
+
```python
|
|
73
|
+
>>> import pyochain as pc
|
|
74
|
+
>>> pc.Dict({1: 2}).iter_values().into(list)
|
|
75
|
+
[2]
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
from .._iter import Iter
|
|
80
|
+
|
|
81
|
+
def _values(data: dict[K, V]) -> Iter[V]:
|
|
82
|
+
return Iter(iter(data.values()))
|
|
83
|
+
|
|
84
|
+
return self.into(_values)
|
|
85
|
+
|
|
86
|
+
def iter_items(self) -> Iter[tuple[K, V]]:
|
|
87
|
+
"""
|
|
88
|
+
Return an Iter of the dict's items.
|
|
89
|
+
```python
|
|
90
|
+
>>> import pyochain as pc
|
|
91
|
+
>>> pc.Dict({1: 2}).iter_items().into(list)
|
|
92
|
+
[(1, 2)]
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
"""
|
|
96
|
+
from .._iter import Iter
|
|
97
|
+
|
|
98
|
+
def _items(data: dict[K, V]) -> Iter[tuple[K, V]]:
|
|
99
|
+
return Iter(iter(data.items()))
|
|
100
|
+
|
|
101
|
+
return self.into(_items)
|
|
102
|
+
|
|
103
|
+
def to_arrays(self) -> Seq[list[Any]]:
|
|
104
|
+
"""
|
|
105
|
+
Convert the nested dictionary into a sequence of arrays.
|
|
106
|
+
|
|
107
|
+
The sequence represents all paths from root to leaves.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
>>> import pyochain as pc
|
|
111
|
+
>>> data = {
|
|
112
|
+
... "a": {"b": 1, "c": 2},
|
|
113
|
+
... "d": {"e": {"f": 3}},
|
|
114
|
+
... }
|
|
115
|
+
>>> pc.Dict(data).to_arrays().unwrap()
|
|
116
|
+
[['a', 'b', 1], ['a', 'c', 2], ['d', 'e', 'f', 3]]
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
from .._iter import Seq
|
|
121
|
+
|
|
122
|
+
def _to_arrays(d: Mapping[Any, Any]) -> list[list[Any]]:
|
|
123
|
+
"""from dictutils.pivot"""
|
|
124
|
+
match d:
|
|
125
|
+
case Mapping():
|
|
126
|
+
arr: list[Any] = []
|
|
127
|
+
for k, v in d.items():
|
|
128
|
+
for el in _to_arrays(v):
|
|
129
|
+
arr.append([k] + el)
|
|
130
|
+
return arr
|
|
131
|
+
|
|
132
|
+
case _:
|
|
133
|
+
return [[d]]
|
|
134
|
+
|
|
135
|
+
return Seq(self.into(_to_arrays))
|
pyochain/_dict/_joins.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
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 JoinsDict[K, V](MappingWrapper[K, V]):
|
|
15
|
+
def inner_join[W](self, other: Mapping[K, W]) -> Dict[K, tuple[V, W]]:
|
|
16
|
+
"""
|
|
17
|
+
Performs an inner join with another mapping based on keys.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
other: The mapping to join with.
|
|
21
|
+
|
|
22
|
+
Only keys present in both mappings are kept.
|
|
23
|
+
```python
|
|
24
|
+
>>> import pyochain as pc
|
|
25
|
+
>>> d1 = {"a": 1, "b": 2}
|
|
26
|
+
>>> d2 = {"b": 10, "c": 20}
|
|
27
|
+
>>> pc.Dict(d1).inner_join(d2).unwrap()
|
|
28
|
+
{'b': (2, 10)}
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def _inner_join(data: Mapping[K, V]) -> dict[K, tuple[V, W]]:
|
|
34
|
+
return {k: (v, other[k]) for k, v in data.items() if k in other}
|
|
35
|
+
|
|
36
|
+
return self._new(_inner_join)
|
|
37
|
+
|
|
38
|
+
def left_join[W](self, other: Mapping[K, W]) -> Dict[K, tuple[V, W | None]]:
|
|
39
|
+
"""
|
|
40
|
+
Performs a left join with another mapping based on keys.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
other: The mapping to join with.
|
|
44
|
+
|
|
45
|
+
All keys from the left dictionary (self) are kept.
|
|
46
|
+
```python
|
|
47
|
+
>>> import pyochain as pc
|
|
48
|
+
>>> d1 = {"a": 1, "b": 2}
|
|
49
|
+
>>> d2 = {"b": 10, "c": 20}
|
|
50
|
+
>>> pc.Dict(d1).left_join(d2).unwrap()
|
|
51
|
+
{'a': (1, None), 'b': (2, 10)}
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def _left_join(data: Mapping[K, V]) -> dict[K, tuple[V, W | None]]:
|
|
57
|
+
return {k: (v, other.get(k)) for k, v in data.items()}
|
|
58
|
+
|
|
59
|
+
return self._new(_left_join)
|
|
60
|
+
|
|
61
|
+
def diff(self, other: Mapping[K, V]) -> Dict[K, tuple[V | None, V | None]]:
|
|
62
|
+
"""
|
|
63
|
+
Returns a dict of the differences between this dict and another.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
other: The mapping to compare against.
|
|
67
|
+
|
|
68
|
+
The keys of the returned dict are the keys that are not shared or have different values.
|
|
69
|
+
The values are tuples containing the value from self and the value from other.
|
|
70
|
+
```python
|
|
71
|
+
>>> import pyochain as pc
|
|
72
|
+
>>> d1 = {"a": 1, "b": 2, "c": 3}
|
|
73
|
+
>>> d2 = {"b": 2, "c": 4, "d": 5}
|
|
74
|
+
>>> pc.Dict(d1).diff(d2).sort().unwrap()
|
|
75
|
+
{'a': (1, None), 'c': (3, 4), 'd': (None, 5)}
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def _diff(data: Mapping[K, V]) -> dict[K, tuple[V | None, V | None]]:
|
|
81
|
+
all_keys: set[K] = data.keys() | other.keys()
|
|
82
|
+
diffs: dict[K, tuple[V | None, V | None]] = {}
|
|
83
|
+
for key in all_keys:
|
|
84
|
+
self_val = data.get(key)
|
|
85
|
+
other_val = other.get(key)
|
|
86
|
+
if self_val != other_val:
|
|
87
|
+
diffs[key] = (self_val, other_val)
|
|
88
|
+
return diffs
|
|
89
|
+
|
|
90
|
+
return self._new(_diff)
|
|
91
|
+
|
|
92
|
+
def merge(self, *others: Mapping[K, V]) -> Dict[K, V]:
|
|
93
|
+
"""
|
|
94
|
+
Merge other dicts into this one.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
*others: One or more mappings to merge into the current dictionary.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
>>> import pyochain as pc
|
|
101
|
+
>>> pc.Dict({1: "one"}).merge({2: "two"}).unwrap()
|
|
102
|
+
{1: 'one', 2: 'two'}
|
|
103
|
+
>>> # Later dictionaries have precedence
|
|
104
|
+
>>> pc.Dict({1: 2, 3: 4}).merge({3: 3, 4: 4}).unwrap()
|
|
105
|
+
{1: 2, 3: 3, 4: 4}
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def _merge(data: Mapping[K, V]) -> dict[K, V]:
|
|
111
|
+
return cz.dicttoolz.merge(data, *others)
|
|
112
|
+
|
|
113
|
+
return self._new(_merge)
|
|
114
|
+
|
|
115
|
+
def merge_with(
|
|
116
|
+
self, *others: Mapping[K, V], func: Callable[[Iterable[V]], V]
|
|
117
|
+
) -> Dict[K, V]:
|
|
118
|
+
"""
|
|
119
|
+
Merge dicts using a function to combine values for duplicate keys.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
*others: One or more mappings to merge into the current dictionary.
|
|
123
|
+
func: Function to combine values for duplicate keys.
|
|
124
|
+
|
|
125
|
+
A key may occur in more than one dict, and all values mapped from the key will be passed to the function as a list, such as func([val1, val2, ...]).
|
|
126
|
+
```python
|
|
127
|
+
>>> import pyochain as pc
|
|
128
|
+
>>> pc.Dict({1: 1, 2: 2}).merge_with({1: 10, 2: 20}, func=sum).unwrap()
|
|
129
|
+
{1: 11, 2: 22}
|
|
130
|
+
>>> pc.Dict({1: 1, 2: 2}).merge_with({2: 20, 3: 30}, func=max).unwrap()
|
|
131
|
+
{1: 1, 2: 20, 3: 30}
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def _merge_with(data: Mapping[K, V]) -> dict[K, V]:
|
|
137
|
+
return cz.dicttoolz.merge_with(func, data, *others)
|
|
138
|
+
|
|
139
|
+
return self._new(_merge_with)
|
pyochain/_dict/_main.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .._core import SupportsKeysAndGetItem
|
|
7
|
+
from ._filters import FilterDict
|
|
8
|
+
from ._groups import GroupsDict
|
|
9
|
+
from ._iter import IterDict
|
|
10
|
+
from ._joins import JoinsDict
|
|
11
|
+
from ._maps import MapDict
|
|
12
|
+
from ._nested import NestedDict
|
|
13
|
+
from ._process import ProcessDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DictCommonMethods[K, V](
|
|
17
|
+
ProcessDict[K, V],
|
|
18
|
+
IterDict[K, V],
|
|
19
|
+
NestedDict[K, V],
|
|
20
|
+
MapDict[K, V],
|
|
21
|
+
JoinsDict[K, V],
|
|
22
|
+
FilterDict[K, V],
|
|
23
|
+
GroupsDict[K, V],
|
|
24
|
+
):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Dict[K, V](DictCommonMethods[K, V]):
|
|
29
|
+
"""
|
|
30
|
+
Wrapper for Python dictionaries with chainable methods.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__slots__ = ()
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def from_[G, I](
|
|
37
|
+
data: Mapping[G, I] | Iterable[tuple[G, I]] | SupportsKeysAndGetItem[G, I],
|
|
38
|
+
) -> Dict[G, I]:
|
|
39
|
+
"""
|
|
40
|
+
Create a Dict from a convertible value.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
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:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
>>> import pyochain as pc
|
|
52
|
+
>>> class MyMapping:
|
|
53
|
+
... def __init__(self):
|
|
54
|
+
... self._data = {1: "a", 2: "b", 3: "c"}
|
|
55
|
+
...
|
|
56
|
+
... def keys(self):
|
|
57
|
+
... return self._data.keys()
|
|
58
|
+
...
|
|
59
|
+
... def __getitem__(self, key):
|
|
60
|
+
... return self._data[key]
|
|
61
|
+
>>>
|
|
62
|
+
>>> pc.Dict.from_(MyMapping()).unwrap()
|
|
63
|
+
{1: 'a', 2: 'b', 3: 'c'}
|
|
64
|
+
>>> pc.Dict.from_([("d", "e"), ("f", "g")]).unwrap()
|
|
65
|
+
{'d': 'e', 'f': 'g'}
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
return Dict(dict(data))
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def from_object(obj: object) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Create a Dict from an object's __dict__ attribute.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
obj: The object whose `__dict__` attribute will be used to create the Dict.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
>>> import pyochain as pc
|
|
81
|
+
>>> class Person:
|
|
82
|
+
... def __init__(self, name: str, age: int):
|
|
83
|
+
... self.name = name
|
|
84
|
+
... self.age = age
|
|
85
|
+
>>> person = Person("Alice", 30)
|
|
86
|
+
>>> pc.Dict.from_object(person).unwrap()
|
|
87
|
+
{'name': 'Alice', 'age': 30}
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
return Dict(obj.__dict__)
|
|
92
|
+
|
|
93
|
+
def pivot(self, *indices: int) -> Dict[Any, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Pivot a nested dictionary by rearranging the key levels according to order.
|
|
96
|
+
|
|
97
|
+
Syntactic sugar for to_arrays().rearrange(*indices).to_records()
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
indices: Indices specifying the new order of key levels
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Pivoted dictionary with keys rearranged
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
```python
|
|
107
|
+
>>> import pyochain as pc
|
|
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}}
|
|
111
|
+
"""
|
|
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)
|