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 ADDED
@@ -0,0 +1,5 @@
1
+ from ._core import Wrapper
2
+ from ._dict import Dict, Expr, key
3
+ from ._iter import Iter, Seq
4
+
5
+ __all__ = ["Dict", "Iter", "Wrapper", "key", "Seq", "Expr"]
@@ -0,0 +1,21 @@
1
+ from ._main import CommonBase, IterWrapper, MappingWrapper, Pipeable, Wrapper
2
+ from ._protocols import (
3
+ Peeked,
4
+ SizedIterable,
5
+ SupportsAllComparisons,
6
+ SupportsKeysAndGetItem,
7
+ SupportsRichComparison,
8
+ )
9
+
10
+ __all__ = [
11
+ "MappingWrapper",
12
+ "CommonBase",
13
+ "IterWrapper",
14
+ "Wrapper",
15
+ "SupportsAllComparisons",
16
+ "SupportsRichComparison",
17
+ "SupportsKeysAndGetItem",
18
+ "Peeked",
19
+ "SizedIterable",
20
+ "Pipeable",
21
+ ]
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Collection, Iterable, Iterator
5
+ from typing import TYPE_CHECKING, Any, Concatenate, Self
6
+
7
+ if TYPE_CHECKING:
8
+ from .._dict import Dict
9
+ from .._iter import Iter, Seq
10
+
11
+
12
+ class Pipeable:
13
+ def pipe[**P, R](
14
+ self,
15
+ func: Callable[Concatenate[Self, P], R],
16
+ *args: P.args,
17
+ **kwargs: P.kwargs,
18
+ ) -> R:
19
+ """Pipe the instance in the function and return the result."""
20
+ return func(self, *args, **kwargs)
21
+
22
+
23
+ class CommonBase[T](ABC, Pipeable):
24
+ """
25
+ Base class for all wrappers.
26
+ You can subclass this to create your own wrapper types.
27
+ The pipe unwrap method must be implemented to allow piping functions that transform the underlying data type, whilst retaining the wrapper.
28
+ """
29
+
30
+ _data: T
31
+
32
+ __slots__ = ("_data",)
33
+
34
+ def __init__(self, data: T) -> None:
35
+ self._data = data
36
+
37
+ @abstractmethod
38
+ def apply[**P](
39
+ self,
40
+ func: Callable[Concatenate[T, P], Any],
41
+ *args: P.args,
42
+ **kwargs: P.kwargs,
43
+ ) -> Any:
44
+ raise NotImplementedError
45
+
46
+ def println(self, pretty: bool = True) -> Self:
47
+ """
48
+ Print the underlying data and return self for chaining.
49
+
50
+ Useful for debugging, simply insert `.println()` in the chain,
51
+ and then removing it will not affect the rest of the chain.
52
+ """
53
+ from pprint import pprint
54
+
55
+ if pretty:
56
+ pprint(self.unwrap(), sort_dicts=False)
57
+ else:
58
+ print(self.unwrap())
59
+ return self
60
+
61
+ def unwrap(self) -> T:
62
+ """
63
+ Return the underlying data.
64
+
65
+ This is a terminal operation.
66
+ """
67
+ return self._data
68
+
69
+ def into[**P, R](
70
+ self,
71
+ func: Callable[Concatenate[T, P], R],
72
+ *args: P.args,
73
+ **kwargs: P.kwargs,
74
+ ) -> R:
75
+ """
76
+ Pass the *unwrapped* underlying data into a function.
77
+
78
+ The result is not wrapped.
79
+ ```python
80
+ >>> import pyochain as pc
81
+ >>> pc.Iter.from_(range(5)).into(list)
82
+ [0, 1, 2, 3, 4]
83
+
84
+ ```
85
+ This is a core functionality that allows ending the chain whilst keeping the code style consistent.
86
+ """
87
+ return func(self.unwrap(), *args, **kwargs)
88
+
89
+
90
+ class IterWrapper[T](CommonBase[Iterable[T]]):
91
+ _data: Iterable[T]
92
+
93
+ def apply[**P, R](
94
+ self,
95
+ func: Callable[Concatenate[Iterable[T], P], Iterator[R]],
96
+ *args: P.args,
97
+ **kwargs: P.kwargs,
98
+ ) -> Iter[R]:
99
+ """
100
+ Apply a function to the underlying iterable and return an Iter of the result.
101
+ Allow to pass user defined functions that transform the iterable while retaining the Iter wrapper.
102
+ Args:
103
+ func: Function to apply to the underlying iterable.
104
+ *args: Positional arguments to pass to the function.
105
+ **kwargs: Keyword arguments to pass to the function.
106
+
107
+ Example:
108
+ ```python
109
+ >>> import pyochain as pc
110
+ >>> def double(data: Iterable[int]) -> Iterator[int]:
111
+ ... return (x * 2 for x in data)
112
+ >>> pc.Iter.from_([1, 2, 3]).apply(double).into(list)
113
+ [2, 4, 6]
114
+ """
115
+ from .._iter import Iter
116
+
117
+ return Iter(self.into(func, *args, **kwargs))
118
+
119
+ def collect(self, factory: Callable[[Iterable[T]], Collection[T]] = list) -> Seq[T]:
120
+ """
121
+ Collect the elements into a sequence.
122
+ Args:
123
+ factory: A callable that takes an iterable and returns a collection. Defaults to list.
124
+
125
+ Example:
126
+ ```python
127
+ >>> import pyochain as pc
128
+ >>> pc.Iter.from_(range(5)).collect().unwrap()
129
+ [0, 1, 2, 3, 4]
130
+
131
+ ```
132
+ """
133
+ from .._iter import Seq
134
+
135
+ return Seq(self.into(factory))
136
+
137
+
138
+ class MappingWrapper[K, V](CommonBase[dict[K, V]]):
139
+ _data: dict[K, V]
140
+
141
+ def apply[**P, KU, VU](
142
+ self,
143
+ func: Callable[Concatenate[dict[K, V], P], dict[KU, VU]],
144
+ *args: P.args,
145
+ **kwargs: P.kwargs,
146
+ ) -> Dict[KU, VU]:
147
+ """
148
+ Apply a function to the underlying dict and return a Dict of the result.
149
+ Allow to pass user defined functions that transform the dict while retaining the Dict wrapper.
150
+ Args:
151
+ func: Function to apply to the underlying dict.
152
+ *args: Positional arguments to pass to the function.
153
+ **kwargs: Keyword arguments to pass to the function.
154
+ Example:
155
+ ```python
156
+ >>> import pyochain as pc
157
+ >>> def invert_dict(d: dict[K, V]) -> dict[V, K]:
158
+ ... return {v: k for k, v in d.items()}
159
+ >>> pc.Dict({'a': 1, 'b': 2}).apply(invert_dict).unwrap()
160
+ {1: 'a', 2: 'b'}
161
+
162
+ ```
163
+ """
164
+ from .._dict import Dict
165
+
166
+ return Dict(self.into(func, *args, **kwargs))
167
+
168
+
169
+ class Wrapper[T](CommonBase[T]):
170
+ """
171
+ A generic Wrapper for any type.
172
+ The pipe into method is implemented to return a Wrapper of the result type.
173
+
174
+ This class is intended for use with other types/implementations that do not support the fluent/functional style.
175
+ This allow the use of a consistent code style across the code base.
176
+ """
177
+
178
+ def apply[**P, R](
179
+ self,
180
+ func: Callable[Concatenate[T, P], R],
181
+ *args: P.args,
182
+ **kwargs: P.kwargs,
183
+ ) -> Wrapper[R]:
184
+ return Wrapper(self.into(func, *args, **kwargs))
@@ -0,0 +1,43 @@
1
+ from collections.abc import Iterable, Iterator, Sized
2
+ from typing import NamedTuple, Protocol
3
+
4
+
5
+ class Peeked[T](NamedTuple):
6
+ value: T | tuple[T, ...]
7
+ sequence: Iterator[T]
8
+
9
+
10
+ class SupportsDunderLT[T](Protocol):
11
+ def __lt__(self, other: T, /) -> bool: ...
12
+
13
+
14
+ class SupportsDunderGT[T](Protocol):
15
+ def __gt__(self, other: T, /) -> bool: ...
16
+
17
+
18
+ class SupportsDunderLE[T](Protocol):
19
+ def __le__(self, other: T, /) -> bool: ...
20
+
21
+
22
+ class SupportsDunderGE[T](Protocol):
23
+ def __ge__(self, other: T, /) -> bool: ...
24
+
25
+
26
+ class SupportsKeysAndGetItem[K, V](Protocol):
27
+ def keys(self) -> Iterable[K]: ...
28
+ def __getitem__(self, key: K, /) -> V: ...
29
+
30
+
31
+ class SupportsAllComparisons[T](
32
+ SupportsDunderLT[T],
33
+ SupportsDunderGT[T],
34
+ SupportsDunderLE[T],
35
+ SupportsDunderGE[T],
36
+ Protocol,
37
+ ): ...
38
+
39
+
40
+ type SupportsRichComparison[T] = SupportsDunderLT[T] | SupportsDunderGT[T]
41
+
42
+
43
+ class SizedIterable[T](Sized, Iterable[T]): ...
@@ -0,0 +1,4 @@
1
+ from ._exprs import Expr, key
2
+ from ._main import Dict
3
+
4
+ __all__ = ["Dict", "key", "Expr"]
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from dataclasses import dataclass
5
+ from typing import Any, Self, TypeGuard
6
+
7
+ import cytoolz as cz
8
+
9
+ from .._core import Pipeable
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class Expr(Pipeable):
14
+ """
15
+ Represents an expression in the pipeline.
16
+
17
+ An Expr encapsulates a sequence of operations to be applied to keys on a python dict.
18
+
19
+ Each Expr instance maintains:
20
+ - A list of tokens representing the keys to access in the dict (the first being the input given to the `key` function),
21
+ - A tuple of operations to apply to the accessed data
22
+ - An alias for the expression (default to the last token).
23
+ """
24
+
25
+ __tokens__: list[str]
26
+ __ops__: tuple[Callable[[object], object], ...]
27
+ _alias: str
28
+
29
+ def __repr__(self) -> str:
30
+ parts: list[str] = []
31
+ s_parts: list[str] = []
32
+ for t in self.__tokens__:
33
+ parts.append(f"field({t!r})")
34
+ if s_parts:
35
+ s_parts.append(".")
36
+ s_parts.append(str(t))
37
+ symbolic = ".".join(parts) if parts else "<root>"
38
+ lowered = "".join(s_parts) or "<root>"
39
+ base = f"Expr({symbolic} -> {lowered})"
40
+ return f"{base}.alias({self._alias!r})"
41
+
42
+ def _to_expr(self, op: Callable[[Any], Any]) -> Self:
43
+ return self.__class__(
44
+ self.__tokens__,
45
+ self.__ops__ + (op,),
46
+ self._alias,
47
+ )
48
+
49
+ def key(self, name: str) -> Self:
50
+ """
51
+ Add a key to the expression.
52
+
53
+ Allow to access nested keys in a dict.
54
+
55
+ Args:
56
+ name: The key to access.
57
+ Example:
58
+ ```python
59
+ >>> import pyochain as pc
60
+ >>> expr = pc.key("a").key("b").key("c")
61
+ >>> expr.__tokens__
62
+ ['a', 'b', 'c']
63
+ >>> data = {"a": {"b": {"c": 42}}}
64
+ >>> pc.Dict(data).select(expr).unwrap()
65
+ {'c': 42}
66
+
67
+ ```
68
+ """
69
+ return self.__class__(
70
+ self.__tokens__ + [name],
71
+ self.__ops__,
72
+ name,
73
+ )
74
+
75
+ def alias(self, name: str) -> Self:
76
+ return self.__class__(self.__tokens__, self.__ops__, name)
77
+
78
+ @property
79
+ def name(self) -> str:
80
+ return self._alias
81
+
82
+ def apply(self, fn: Callable[[Any], Any]) -> Self:
83
+ """
84
+ Applies the given function fn to the data within the current Expr instance
85
+ """
86
+
87
+ def _apply(data: Any) -> Any:
88
+ return fn(data)
89
+
90
+ return self._to_expr(_apply)
91
+
92
+
93
+ def key(name: str) -> Expr:
94
+ """Create an Expr that accesses the given key."""
95
+ return Expr([name], (), name)
96
+
97
+
98
+ def _expr_identity(obj: Any) -> TypeGuard[Expr]:
99
+ return hasattr(obj, "__tokens__")
100
+
101
+
102
+ type IntoExpr = Expr | str
103
+
104
+
105
+ def compute_exprs(
106
+ exprs: Iterable[IntoExpr], data_in: dict[str, Any], data_out: dict[str, Any]
107
+ ) -> dict[str, Any]:
108
+ for e in exprs:
109
+ if not _expr_identity(e):
110
+ e = key(e) # type: ignore
111
+ current: object = cz.dicttoolz.get_in(e.__tokens__, data_in)
112
+ for op in e.__ops__:
113
+ current = op(current)
114
+ data_out[e.name] = current
115
+ return data_out
@@ -0,0 +1,273 @@
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, TypeGuard
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
+ class FilterDict[K, V](MappingWrapper[K, V]):
16
+ def filter_keys(self, predicate: Callable[[K], bool]) -> Dict[K, V]:
17
+ """
18
+ Return a new Dict containing keys that satisfy predicate.
19
+
20
+ Args:
21
+ predicate: Function to determine if a key should be included.
22
+ Example:
23
+ ```python
24
+ >>> import pyochain as pc
25
+ >>> d = {1: 2, 2: 3, 3: 4, 4: 5}
26
+ >>> pc.Dict(d).filter_keys(lambda x: x % 2 == 0).unwrap()
27
+ {2: 3, 4: 5}
28
+
29
+ ```
30
+ """
31
+ return self.apply(partial(cz.dicttoolz.keyfilter, predicate))
32
+
33
+ def filter_values(self, predicate: Callable[[V], bool]) -> Dict[K, V]:
34
+ """
35
+ Return a new Dict containing items whose values satisfy predicate.
36
+
37
+ Args:
38
+ predicate: Function to determine if a value should be included.
39
+ Example:
40
+ ```python
41
+ >>> import pyochain as pc
42
+ >>> d = {1: 2, 2: 3, 3: 4, 4: 5}
43
+ >>> pc.Dict(d).filter_values(lambda x: x % 2 == 0).unwrap()
44
+ {1: 2, 3: 4}
45
+ >>> pc.Dict(d).filter_values(lambda x: not x > 3).unwrap()
46
+ {1: 2, 2: 3}
47
+
48
+ ```
49
+ """
50
+ return self.apply(partial(cz.dicttoolz.valfilter, predicate))
51
+
52
+ def filter_items(
53
+ self,
54
+ predicate: Callable[[tuple[K, V]], bool],
55
+ ) -> Dict[K, V]:
56
+ """
57
+ Filter items by predicate applied to (key, value) tuples.
58
+
59
+ Args:
60
+ predicate: Function to determine if a (key, value) pair should be included.
61
+ Example:
62
+ ```python
63
+ >>> import pyochain as pc
64
+ >>> def isvalid(item):
65
+ ... k, v = item
66
+ ... return k % 2 == 0 and v < 4
67
+ >>> d = pc.Dict({1: 2, 2: 3, 3: 4, 4: 5})
68
+ >>>
69
+ >>> d.filter_items(isvalid).unwrap()
70
+ {2: 3}
71
+ >>> d.filter_items(lambda kv: not isvalid(kv)).unwrap()
72
+ {1: 2, 3: 4, 4: 5}
73
+
74
+ ```
75
+ """
76
+ return self.apply(partial(cz.dicttoolz.itemfilter, predicate))
77
+
78
+ def filter_kv(
79
+ self,
80
+ predicate: Callable[[K, V], bool],
81
+ ) -> Dict[K, V]:
82
+ """
83
+ Filter items by predicate applied to unpacked (key, value) tuples.
84
+
85
+ Args:
86
+ predicate: Function to determine if a key-value pair should be included.
87
+ Example:
88
+ ```python
89
+ >>> import pyochain as pc
90
+ >>> def isvalid(key, value):
91
+ ... return key % 2 == 0 and value < 4
92
+ >>> d = pc.Dict({1: 2, 2: 3, 3: 4, 4: 5})
93
+ >>>
94
+ >>> d.filter_kv(isvalid).unwrap()
95
+ {2: 3}
96
+ >>> d.filter_kv(lambda k, v: not isvalid(k, v)).unwrap()
97
+ {1: 2, 3: 4, 4: 5}
98
+
99
+ ```
100
+ """
101
+
102
+ def _filter_kv(data: dict[K, V]) -> dict[K, V]:
103
+ def _(kv: tuple[K, V]) -> bool:
104
+ return predicate(kv[0], kv[1])
105
+
106
+ return cz.dicttoolz.itemfilter(_, data)
107
+
108
+ return self.apply(_filter_kv)
109
+
110
+ def filter_attr[U](self, attr: str, dtype: type[U] = object) -> Dict[K, U]:
111
+ """
112
+ Filter values that have a given attribute.
113
+
114
+ This does not enforce type checking at runtime for performance considerations.
115
+ Args:
116
+ attr: Attribute name to check for.
117
+ dtype: Optional expected type of the attribute for type hinting.
118
+ Example:
119
+ ```python
120
+ >>> import pyochain as pc
121
+ >>> pc.Dict({"a": "hello", "b": "world", "c": 2, "d": 5}).filter_attr(
122
+ ... "capitalize", str
123
+ ... ).unwrap()
124
+ {'a': 'hello', 'b': 'world'}
125
+
126
+ ```
127
+ """
128
+
129
+ def _filter_attr(data: dict[K, V]) -> dict[K, U]:
130
+ def has_attr(x: V) -> TypeGuard[U]:
131
+ return hasattr(x, attr)
132
+
133
+ return cz.dicttoolz.valfilter(has_attr, data)
134
+
135
+ return self.apply(_filter_attr)
136
+
137
+ def filter_type[R](self, typ: type[R]) -> Dict[K, R]:
138
+ """
139
+ Filter values by type.
140
+
141
+ Args:
142
+ typ: Type to filter values by.
143
+ Example:
144
+ ```python
145
+ >>> import pyochain as pc
146
+ >>> data = {"a": "one", "b": "two", "c": 3, "d": 4}
147
+ >>> pc.Dict(data).filter_type(str).unwrap()
148
+ {'a': 'one', 'b': 'two'}
149
+
150
+ ```
151
+ """
152
+
153
+ def _filter_type(data: dict[K, V]) -> dict[K, R]:
154
+ def _(x: V) -> TypeGuard[R]:
155
+ return isinstance(x, typ)
156
+
157
+ return cz.dicttoolz.valfilter(_, data)
158
+
159
+ return self.apply(_filter_type)
160
+
161
+ def filter_callable(self) -> Dict[K, Callable[..., Any]]:
162
+ """
163
+ Filter values that are callable.
164
+ ```python
165
+ >>> import pyochain as pc
166
+ >>> def foo():
167
+ ... pass
168
+ >>> data = {1: "one", 2: "two", 3: foo, 4: print}
169
+ >>> pc.Dict(data).filter_callable().map_values(lambda x: x.__name__).unwrap()
170
+ {3: 'foo', 4: 'print'}
171
+
172
+ ```
173
+ """
174
+
175
+ def _filter_callable(data: dict[K, V]) -> dict[K, Callable[..., Any]]:
176
+ def _(x: V) -> TypeGuard[Callable[..., Any]]:
177
+ return callable(x)
178
+
179
+ return cz.dicttoolz.valfilter(_, data)
180
+
181
+ return self.apply(_filter_callable)
182
+
183
+ def filter_subclass[U: type[Any], R](
184
+ self: FilterDict[K, U], parent: type[R], keep_parent: bool = True
185
+ ) -> Dict[K, type[R]]:
186
+ """
187
+ Filter values that are subclasses of a given parent class.
188
+
189
+ Args:
190
+ parent: Parent class to check against.
191
+ keep_parent: Whether to include the parent class itself. Defaults to True.
192
+
193
+ ```python
194
+ >>> import pyochain as pc
195
+ >>> class A:
196
+ ... pass
197
+ >>> class B(A):
198
+ ... pass
199
+ >>> class C:
200
+ ... pass
201
+ >>> def name(cls: type[Any]) -> str:
202
+ ... return cls.__name__
203
+ >>> data = pc.Dict({"first": A, "second": B, "third": C})
204
+ >>> data.filter_subclass(A).map_values(name).unwrap()
205
+ {'first': 'A', 'second': 'B'}
206
+ >>> data.filter_subclass(A, keep_parent=False).map_values(name).unwrap()
207
+ {'second': 'B'}
208
+
209
+ ```
210
+ """
211
+
212
+ def _filter_subclass(data: dict[K, U]) -> dict[K, type[R]]:
213
+ def _(x: type[Any]) -> TypeGuard[type[R]]:
214
+ if keep_parent:
215
+ return issubclass(x, parent)
216
+ else:
217
+ return issubclass(x, parent) and x is not parent
218
+
219
+ return cz.dicttoolz.valfilter(_, data)
220
+
221
+ return self.apply(_filter_subclass)
222
+
223
+ def intersect_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
224
+ """
225
+ Return a new Dict keeping only keys present in self and all others.
226
+
227
+ Args:
228
+ *others: Other mappings to intersect keys with.
229
+
230
+ ```python
231
+ >>> import pyochain as pc
232
+ >>> d1 = {"a": 1, "b": 2, "c": 3}
233
+ >>> d2 = {"b": 10, "c": 20}
234
+ >>> d3 = {"c": 30}
235
+ >>> pc.Dict(d1).intersect_keys(d2, d3).unwrap()
236
+ {'c': 3}
237
+
238
+ ```
239
+ """
240
+
241
+ def _intersect_keys(data: dict[K, V]) -> dict[K, V]:
242
+ self_keys = set(data.keys())
243
+ for other in others:
244
+ self_keys.intersection_update(other.keys())
245
+ return {k: data[k] for k in self_keys}
246
+
247
+ return self.apply(_intersect_keys)
248
+
249
+ def diff_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
250
+ """
251
+ Return a new Dict keeping only keys present in self but not in others.
252
+
253
+ Args:
254
+ *others: Other mappings to exclude keys from.
255
+
256
+ ```python
257
+ >>> import pyochain as pc
258
+ >>> d1 = {"a": 1, "b": 2, "c": 3}
259
+ >>> d2 = {"b": 10, "d": 40}
260
+ >>> d3 = {"c": 30}
261
+ >>> pc.Dict(d1).diff_keys(d2, d3).unwrap()
262
+ {'a': 1}
263
+
264
+ ```
265
+ """
266
+
267
+ def _diff_keys(data: dict[K, V]) -> dict[K, V]:
268
+ self_keys = set(data.keys())
269
+ for other in others:
270
+ self_keys.difference_update(other.keys())
271
+ return {k: data[k] for k in self_keys}
272
+
273
+ return self.apply(_diff_keys)