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,272 @@
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, TypeIs
6
+
7
+ import cytoolz as cz
8
+
9
+ from .._core import MappingWrapper
10
+
11
+ if TYPE_CHECKING:
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
43
+
44
+
45
+ class NestedDict[K, V](MappingWrapper[K, V]):
46
+ def struct[**P, R, U: dict[Any, Any]](
47
+ self: NestedDict[K, U],
48
+ func: Callable[Concatenate[Dict[K, U], P], R],
49
+ *args: P.args,
50
+ **kwargs: P.kwargs,
51
+ ) -> Dict[K, R]:
52
+ """
53
+ Apply a function to each value after wrapping it in a Dict.
54
+
55
+ Args:
56
+ func: Function to apply to each value after wrapping it in a Dict.
57
+ *args: Positional arguments to pass to the function.
58
+ **kwargs: Keyword arguments to pass to the function.
59
+
60
+ Syntactic sugar for `map_values(lambda data: func(pc.Dict(data), *args, **kwargs))`
61
+ ```python
62
+ >>> import pyochain as pc
63
+ >>> data = {
64
+ ... "person1": {"name": "Alice", "age": 30, "city": "New York"},
65
+ ... "person2": {"name": "Bob", "age": 25, "city": "Los Angeles"},
66
+ ... }
67
+ >>> pc.Dict(data).struct(lambda d: d.map_keys(str.upper).drop("AGE").unwrap())
68
+ ... # doctest: +NORMALIZE_WHITESPACE
69
+ {'person1': {'CITY': 'New York', 'NAME': 'Alice'},
70
+ 'person2': {'CITY': 'Los Angeles', 'NAME': 'Bob'}}
71
+
72
+ ```
73
+ """
74
+ from ._main import Dict
75
+
76
+ def _struct(data: Mapping[K, U]) -> dict[K, R]:
77
+ def _(v: dict[Any, Any]) -> R:
78
+ return func(Dict(v), *args, **kwargs)
79
+
80
+ return cz.dicttoolz.valmap(_, data)
81
+
82
+ return self._new(_struct)
83
+
84
+ def flatten(
85
+ self: NestedDict[str, Any], sep: str = ".", max_depth: int | None = None
86
+ ) -> Dict[str, Any]:
87
+ """
88
+ Flatten a nested dictionary, concatenating keys with the specified separator.
89
+
90
+ Args:
91
+ sep: Separator to use when concatenating keys
92
+ max_depth: Maximum depth to flatten. If None, flattens completely.
93
+ ```python
94
+ >>> import pyochain as pc
95
+ >>> data = {
96
+ ... "config": {"params": {"retries": 3, "timeout": 30}, "mode": "fast"},
97
+ ... "version": 1.0,
98
+ ... }
99
+ >>> pc.Dict(data).flatten().unwrap()
100
+ {'config.params.retries': 3, 'config.params.timeout': 30, 'config.mode': 'fast', 'version': 1.0}
101
+ >>> pc.Dict(data).flatten(sep="_").unwrap()
102
+ {'config_params_retries': 3, 'config_params_timeout': 30, 'config_mode': 'fast', 'version': 1.0}
103
+ >>> pc.Dict(data).flatten(max_depth=1).unwrap()
104
+ {'config.params': {'retries': 3, 'timeout': 30}, 'config.mode': 'fast', 'version': 1.0}
105
+
106
+ ```
107
+ """
108
+
109
+ def _flatten(
110
+ d: Mapping[Any, Any], parent_key: str = "", current_depth: int = 1
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
+
117
+ items: list[tuple[str, Any]] = []
118
+ for k, v in d.items():
119
+ new_key = parent_key + sep + k if parent_key else k
120
+ if _can_recurse(v):
121
+ items.extend(_flatten(v, new_key, current_depth + 1).items())
122
+ else:
123
+ items.append((new_key, v))
124
+ return dict(items)
125
+
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)
156
+
157
+ def with_nested_key(self, *keys: K, value: V) -> Dict[K, V]:
158
+ """
159
+ Set a nested key path and return a new Dict with new, potentially nested, key value pair.
160
+
161
+ Args:
162
+ *keys: Sequence of keys representing the nested path.
163
+ value: Value to set at the specified nested path.
164
+ ```python
165
+ >>> import pyochain as pc
166
+ >>> purchase = {
167
+ ... "name": "Alice",
168
+ ... "order": {"items": ["Apple", "Orange"], "costs": [0.50, 1.25]},
169
+ ... "credit card": "5555-1234-1234-1234",
170
+ ... }
171
+ >>> pc.Dict(purchase).with_nested_key(
172
+ ... "order", "costs", value=[0.25, 1.00]
173
+ ... ).unwrap()
174
+ {'name': 'Alice', 'order': {'items': ['Apple', 'Orange'], 'costs': [0.25, 1.0]}, 'credit card': '5555-1234-1234-1234'}
175
+
176
+ ```
177
+ """
178
+
179
+ def _with_nested_key(data: dict[K, V]) -> dict[K, V]:
180
+ return cz.dicttoolz.assoc_in(data, keys, value=value)
181
+
182
+ return self._new(_with_nested_key)
183
+
184
+ def pluck[U: str | int](self: NestedDict[U, Any], *keys: str) -> Dict[U, Any]:
185
+ """
186
+ Extract values from nested dictionaries using a sequence of keys.
187
+
188
+ Args:
189
+ *keys: Sequence of keys to extract values from the nested dictionaries.
190
+ ```python
191
+ >>> import pyochain as pc
192
+ >>> data = {
193
+ ... "person1": {"name": "Alice", "age": 30},
194
+ ... "person2": {"name": "Bob", "age": 25},
195
+ ... }
196
+ >>> pc.Dict(data).pluck("name").unwrap()
197
+ {'person1': 'Alice', 'person2': 'Bob'}
198
+
199
+ ```
200
+ """
201
+
202
+ getter = partial(cz.dicttoolz.get_in, keys)
203
+
204
+ def _pluck(data: Mapping[U, Any]) -> dict[U, Any]:
205
+ return cz.dicttoolz.valmap(getter, data)
206
+
207
+ return self._new(_pluck)
208
+
209
+ def get_in(self, *keys: K, default: Any = None) -> Any:
210
+ """
211
+ Retrieve a value from a nested dictionary structure.
212
+
213
+ Args:
214
+ *keys: Sequence of keys representing the nested path to retrieve the value.
215
+ default: Default value to return if the keys do not exist.
216
+
217
+ ```python
218
+ >>> import pyochain as pc
219
+ >>> data = {"a": {"b": {"c": 1}}}
220
+ >>> pc.Dict(data).get_in("a", "b", "c")
221
+ 1
222
+ >>> pc.Dict(data).get_in("a", "x", default="Not Found")
223
+ 'Not Found'
224
+
225
+ ```
226
+ """
227
+
228
+ def _get_in(data: Mapping[K, V]) -> Any:
229
+ return cz.dicttoolz.get_in(keys, data, default)
230
+
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)
@@ -0,0 +1,204 @@
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, SupportsRichComparison
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._new(_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
+
84
+ def _update_in(data: dict[K, V]) -> dict[K, V]:
85
+ return cz.dicttoolz.update_in(data, keys, func, default=default)
86
+
87
+ return self._new(_update_in)
88
+
89
+ def with_key(self, key: K, value: V) -> Dict[K, V]:
90
+ """
91
+ Return a new Dict with key set to value.
92
+
93
+ Args:
94
+ key: Key to set in the dictionary.
95
+ value: Value to associate with the specified key.
96
+
97
+ Does not modify the initial dictionary.
98
+ ```python
99
+ >>> import pyochain as pc
100
+ >>> pc.Dict({"x": 1}).with_key("x", 2).unwrap()
101
+ {'x': 2}
102
+ >>> pc.Dict({"x": 1}).with_key("y", 3).unwrap()
103
+ {'x': 1, 'y': 3}
104
+ >>> pc.Dict({}).with_key("x", 1).unwrap()
105
+ {'x': 1}
106
+
107
+ ```
108
+ """
109
+
110
+ def _with_key(data: dict[K, V]) -> dict[K, V]:
111
+ return cz.dicttoolz.assoc(data, key, value)
112
+
113
+ return self._new(_with_key)
114
+
115
+ def drop(self, *keys: K) -> Dict[K, V]:
116
+ """
117
+ Return a new Dict with given keys removed.
118
+
119
+ Args:
120
+ *keys: Sequence of keys to remove from the dictionary.
121
+
122
+ New dict has d[key] deleted for each supplied key.
123
+ ```python
124
+ >>> import pyochain as pc
125
+ >>> pc.Dict({"x": 1, "y": 2}).drop("y").unwrap()
126
+ {'x': 1}
127
+ >>> pc.Dict({"x": 1, "y": 2}).drop("y", "x").unwrap()
128
+ {}
129
+ >>> pc.Dict({"x": 1}).drop("y").unwrap() # Ignores missing keys
130
+ {'x': 1}
131
+ >>> pc.Dict({1: 2, 3: 4}).drop(1).unwrap()
132
+ {3: 4}
133
+
134
+ ```
135
+ """
136
+
137
+ def _drop(data: dict[K, V]) -> dict[K, V]:
138
+ return cz.dicttoolz.dissoc(data, *keys)
139
+
140
+ return self._new(_drop)
141
+
142
+ def rename(self, mapping: Mapping[K, K]) -> Dict[K, V]:
143
+ """
144
+ Return a new Dict with keys renamed according to the mapping.
145
+
146
+ Args:
147
+ mapping: A dictionary mapping old keys to new keys.
148
+
149
+ Keys not in the mapping are kept as is.
150
+ ```python
151
+ >>> import pyochain as pc
152
+ >>> d = {"a": 1, "b": 2, "c": 3}
153
+ >>> mapping = {"b": "beta", "c": "gamma"}
154
+ >>> pc.Dict(d).rename(mapping).unwrap()
155
+ {'a': 1, 'beta': 2, 'gamma': 3}
156
+
157
+ ```
158
+ """
159
+
160
+ def _rename(data: dict[K, V]) -> dict[K, V]:
161
+ return {mapping.get(k, k): v for k, v in data.items()}
162
+
163
+ return self._new(_rename)
164
+
165
+ def sort(self, reverse: bool = False) -> Dict[K, V]:
166
+ """
167
+ Sort the dictionary by its keys and return a new Dict.
168
+
169
+ Args:
170
+ reverse: Whether to sort in descending order. Defaults to False.
171
+
172
+ ```python
173
+ >>> import pyochain as pc
174
+ >>> pc.Dict({"b": 2, "a": 1}).sort().unwrap()
175
+ {'a': 1, 'b': 2}
176
+
177
+ ```
178
+ """
179
+
180
+ def _sort(data: dict[K, V]) -> dict[K, V]:
181
+ return dict(sorted(data.items(), reverse=reverse))
182
+
183
+ return self._new(_sort)
184
+
185
+ def sort_values[U: SupportsRichComparison[Any]](
186
+ self: ProcessDict[K, U], reverse: bool = False
187
+ ) -> Dict[K, U]:
188
+ """
189
+ Sort the dictionary by its values and return a new Dict.
190
+
191
+ Args:
192
+ reverse: Whether to sort in descending order. Defaults to False.
193
+ ```python
194
+ >>> import pyochain as pc
195
+ >>> pc.Dict({"a": 2, "b": 1}).sort_values().unwrap()
196
+ {'b': 1, 'a': 2}
197
+
198
+ ```
199
+ """
200
+
201
+ def _sort_values(data: dict[K, U]) -> dict[K, U]:
202
+ return dict(sorted(data.items(), key=lambda item: item[1], reverse=reverse))
203
+
204
+ return self._new(_sort_values)
@@ -0,0 +1,3 @@
1
+ from ._main import Iter, Seq
2
+
3
+ __all__ = ["Iter", "Seq"]