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.
@@ -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)
@@ -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))
@@ -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)
@@ -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()
@@ -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)