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.

@@ -0,0 +1,62 @@
1
+ from typing import Any
2
+
3
+
4
+ def dict_repr(
5
+ v: object,
6
+ depth: int = 0,
7
+ max_depth: int = 3,
8
+ max_items: int = 6,
9
+ max_str: int = 80,
10
+ indent: int = 2,
11
+ ) -> str:
12
+ pad = " " * (depth * indent)
13
+ if depth > max_depth:
14
+ return "…"
15
+ match v:
16
+ case dict():
17
+ items: list[tuple[str, Any]] = list(v.items()) # type: ignore
18
+ shown: list[tuple[str, Any]] = items[:max_items]
19
+ if (
20
+ all(
21
+ not isinstance(val, dict) and not isinstance(val, list)
22
+ for _, val in shown
23
+ )
24
+ and len(shown) <= 2
25
+ ):
26
+ body = ", ".join(
27
+ f"{k!r}: {dict_repr(val, depth + 1)}" for k, val in shown
28
+ )
29
+ if len(items) > max_items:
30
+ body += ", …"
31
+ return "{" + body + "}"
32
+ lines: list[str] = []
33
+ for k, val in shown:
34
+ lines.append(
35
+ f"{pad}{' ' * indent}{k!r}: {dict_repr(val, depth + 1, max_depth, max_items, max_str, indent)}"
36
+ )
37
+ if len(items) > max_items:
38
+ lines.append(f"{pad}{' ' * indent}…")
39
+ return "{\n" + ",\n".join(lines) + f"\n{pad}" + "}"
40
+
41
+ case list():
42
+ elems: list[Any] = v[:max_items] # type: ignore
43
+ if (
44
+ all(isinstance(x, (int, float, str, bool, type(None))) for x in elems)
45
+ and len(elems) <= 4
46
+ ):
47
+ body = ", ".join(dict_repr(x, depth + 1) for x in elems)
48
+ if len(v) > max_items: # type: ignore
49
+ body += ", …"
50
+ return "[" + body + "]"
51
+ lines = [
52
+ f"{pad}{' ' * indent}{dict_repr(x, depth + 1, max_depth, max_items, max_str, indent)}"
53
+ for x in elems
54
+ ]
55
+ if len(v) > max_items: # type: ignore
56
+ lines.append(f"{pad}{' ' * indent}…")
57
+ return "[\n" + ",\n".join(lines) + f"\n{pad}" + "]"
58
+
59
+ case str():
60
+ return repr(v if len(v) <= max_str else v[:max_str] + "…")
61
+ case _:
62
+ return repr(v)
@@ -0,0 +1,176 @@
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.apply(_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.apply(_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.apply(_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
+ >>>
147
+ >>> # --- Exemple 2: Agrégation plus complexe ---
148
+ >>> sales_data = {
149
+ ... "store_1": "Electronics",
150
+ ... "store_2": "Groceries",
151
+ ... "store_3": "Electronics",
152
+ ... "store_4": "Clothing",
153
+ ... }
154
+ >>>
155
+ >>> # Obtain the first store for each category (after sorting store names)
156
+ >>> pc.Dict(sales_data).group_by_value_agg(
157
+ ... value_func=lambda category: category,
158
+ ... agg_func=lambda d: d.iter_keys().sort().first(),
159
+ ... ).sort().unwrap()
160
+ {'Clothing': 'store_4', 'Electronics': 'store_1', 'Groceries': 'store_2'}
161
+
162
+ ```
163
+ """
164
+ from ._main import Dict
165
+
166
+ def _group_by_value_agg(data: dict[K, V]) -> dict[G, R]:
167
+ def _key_func(kv: tuple[K, V]) -> G:
168
+ return value_func(kv[1])
169
+
170
+ def _agg_func(items: list[tuple[K, V]]) -> R:
171
+ return agg_func(Dict(dict(items)))
172
+
173
+ groups = cz.itertoolz.groupby(_key_func, data.items())
174
+ return cz.dicttoolz.valmap(_agg_func, groups)
175
+
176
+ return self.apply(_group_by_value_agg)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable, Mapping
4
+ from typing import TYPE_CHECKING, Concatenate
5
+
6
+ import cytoolz as cz
7
+
8
+ from .._core import MappingWrapper
9
+
10
+ if TYPE_CHECKING:
11
+ from .._iter import Iter
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.from_(v), *args, **kwargs)
47
+
48
+ return cz.dicttoolz.valmap(_, data)
49
+
50
+ return self.apply(_itr)
51
+
52
+ def iter_keys(self) -> Iter[K]:
53
+ """
54
+ Return a 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
+ return Iter.from_(self.unwrap().keys())
65
+
66
+ def iter_values(self) -> Iter[V]:
67
+ """
68
+ Return an Iter of the dict's values.
69
+ ```python
70
+ >>> import pyochain as pc
71
+ >>> pc.Dict({1: 2}).iter_values().into(list)
72
+ [2]
73
+
74
+ ```
75
+ """
76
+ from .._iter import Iter
77
+
78
+ return Iter.from_(self.unwrap().values())
79
+
80
+ def iter_items(self) -> Iter[tuple[K, V]]:
81
+ """
82
+ Return a Iter of the dict's items.
83
+ ```python
84
+ >>> import pyochain as pc
85
+ >>> pc.Dict({1: 2}).iter_items().into(list)
86
+ [(1, 2)]
87
+
88
+ ```
89
+ """
90
+ from .._iter import Iter
91
+
92
+ return Iter.from_(self.unwrap().items())
@@ -0,0 +1,137 @@
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.apply(_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.apply(_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(
81
+ data: Mapping[K, V], other: Mapping[K, V]
82
+ ) -> dict[K, tuple[V | None, V | None]]:
83
+ all_keys: set[K] = data.keys() | other.keys()
84
+ diffs: dict[K, tuple[V | None, V | None]] = {}
85
+ for key in all_keys:
86
+ self_val = data.get(key)
87
+ other_val = other.get(key)
88
+ if self_val != other_val:
89
+ diffs[key] = (self_val, other_val)
90
+ return diffs
91
+
92
+ return self.apply(_diff, other)
93
+
94
+ def merge(self, *others: Mapping[K, V]) -> Dict[K, V]:
95
+ """
96
+ Merge other dicts into this one and return a new Dict.
97
+
98
+ Args:
99
+ *others: One or more mappings to merge into the current dictionary.
100
+
101
+ ```python
102
+ >>> import pyochain as pc
103
+ >>> pc.Dict({1: "one"}).merge({2: "two"}).unwrap()
104
+ {1: 'one', 2: 'two'}
105
+ >>> # Later dictionaries have precedence
106
+ >>> pc.Dict({1: 2, 3: 4}).merge({3: 3, 4: 4}).unwrap()
107
+ {1: 2, 3: 3, 4: 4}
108
+
109
+ ```
110
+ """
111
+ return self.apply(cz.dicttoolz.merge, *others)
112
+
113
+ def merge_with(
114
+ self, *others: Mapping[K, V], func: Callable[[Iterable[V]], V]
115
+ ) -> Dict[K, V]:
116
+ """
117
+ Merge dicts using a function to combine values for duplicate keys.
118
+
119
+ Args:
120
+ *others: One or more mappings to merge into the current dictionary.
121
+ func: Function to combine values for duplicate keys.
122
+
123
+ 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, ...]).
124
+ ```python
125
+ >>> import pyochain as pc
126
+ >>> pc.Dict({1: 1, 2: 2}).merge_with({1: 10, 2: 20}, func=sum).unwrap()
127
+ {1: 11, 2: 22}
128
+ >>> pc.Dict({1: 1, 2: 2}).merge_with({2: 20, 3: 30}, func=max).unwrap()
129
+ {1: 1, 2: 20, 3: 30}
130
+
131
+ ```
132
+ """
133
+
134
+ def _merge_with(data: Mapping[K, V]) -> dict[K, V]:
135
+ return cz.dicttoolz.merge_with(func, data, *others)
136
+
137
+ return self.apply(_merge_with)