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 +5 -0
- pyochain/_core/__init__.py +21 -0
- pyochain/_core/_main.py +184 -0
- pyochain/_core/_protocols.py +43 -0
- pyochain/_dict/__init__.py +4 -0
- pyochain/_dict/_exprs.py +115 -0
- pyochain/_dict/_filters.py +273 -0
- pyochain/_dict/_funcs.py +62 -0
- pyochain/_dict/_groups.py +176 -0
- pyochain/_dict/_iter.py +92 -0
- pyochain/_dict/_joins.py +137 -0
- pyochain/_dict/_main.py +307 -0
- pyochain/_dict/_nested.py +218 -0
- pyochain/_dict/_process.py +171 -0
- pyochain/_iter/__init__.py +3 -0
- pyochain/_iter/_aggregations.py +323 -0
- pyochain/_iter/_booleans.py +224 -0
- pyochain/_iter/_constructors.py +155 -0
- pyochain/_iter/_eager.py +195 -0
- pyochain/_iter/_filters.py +503 -0
- pyochain/_iter/_groups.py +264 -0
- pyochain/_iter/_joins.py +407 -0
- pyochain/_iter/_lists.py +306 -0
- pyochain/_iter/_main.py +224 -0
- pyochain/_iter/_maps.py +358 -0
- pyochain/_iter/_partitions.py +148 -0
- pyochain/_iter/_process.py +384 -0
- pyochain/_iter/_rolling.py +247 -0
- pyochain/_iter/_tuples.py +221 -0
- pyochain/py.typed +0 -0
- pyochain-0.5.0.dist-info/METADATA +295 -0
- pyochain-0.5.0.dist-info/RECORD +33 -0
- pyochain-0.5.0.dist-info/WHEEL +4 -0
pyochain/__init__.py
ADDED
|
@@ -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
|
+
]
|
pyochain/_core/_main.py
ADDED
|
@@ -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]): ...
|
pyochain/_dict/_exprs.py
ADDED
|
@@ -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)
|