pyrio 1.0.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.
- pyrio/__init__.py +2 -0
- pyrio/decorator.py +55 -0
- pyrio/exception.py +10 -0
- pyrio/iterator.py +135 -0
- pyrio/itertools_mixin.py +223 -0
- pyrio/optional.py +62 -0
- pyrio/stream.py +305 -0
- pyrio-1.0.0.dist-info/LICENSE +21 -0
- pyrio-1.0.0.dist-info/METADATA +25 -0
- pyrio-1.0.0.dist-info/RECORD +11 -0
- pyrio-1.0.0.dist-info/WHEEL +4 -0
pyrio/__init__.py
ADDED
pyrio/decorator.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from pyrio.exception import IllegalStateError
|
|
4
|
+
|
|
5
|
+
TERMINAL_FUNCTIONS = [
|
|
6
|
+
"for_each",
|
|
7
|
+
"reduce",
|
|
8
|
+
"count",
|
|
9
|
+
"sum",
|
|
10
|
+
"find_first",
|
|
11
|
+
"find_any",
|
|
12
|
+
"any_match",
|
|
13
|
+
"all_match",
|
|
14
|
+
"none_match",
|
|
15
|
+
"min",
|
|
16
|
+
"max",
|
|
17
|
+
"compare_with",
|
|
18
|
+
"to_list",
|
|
19
|
+
"to_tuple",
|
|
20
|
+
"to_set",
|
|
21
|
+
"to_dict",
|
|
22
|
+
"group_by",
|
|
23
|
+
"take_nth",
|
|
24
|
+
"all_equal",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pre_call(function_decorator):
|
|
29
|
+
def decorator(cls):
|
|
30
|
+
for name, obj in vars(cls).items():
|
|
31
|
+
if callable(obj):
|
|
32
|
+
setattr(cls, name, function_decorator(obj))
|
|
33
|
+
return cls
|
|
34
|
+
|
|
35
|
+
return decorator
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def handle_consumed(func):
|
|
39
|
+
@wraps(func)
|
|
40
|
+
def wrapper(*args, **kw):
|
|
41
|
+
from pyrio import Stream
|
|
42
|
+
|
|
43
|
+
if not args or isinstance(args[0], Stream) is False:
|
|
44
|
+
return func(*args, **kw)
|
|
45
|
+
|
|
46
|
+
is_consumed = getattr(args[0], "_is_consumed", None)
|
|
47
|
+
if is_consumed:
|
|
48
|
+
raise IllegalStateError("Stream object already consumed")
|
|
49
|
+
|
|
50
|
+
result = func(*args, **kw)
|
|
51
|
+
if is_consumed is False and func.__name__ in TERMINAL_FUNCTIONS:
|
|
52
|
+
args[0]._is_consumed = True
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
return wrapper
|
pyrio/exception.py
ADDED
pyrio/iterator.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
class Iterator:
|
|
2
|
+
@staticmethod
|
|
3
|
+
def concat(*streams):
|
|
4
|
+
for iterable in streams:
|
|
5
|
+
yield from iterable
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def filter(iterable, predicate):
|
|
9
|
+
for i in iterable:
|
|
10
|
+
if predicate(i):
|
|
11
|
+
yield i
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def map(iterable, mapper):
|
|
15
|
+
for i in iterable:
|
|
16
|
+
yield mapper(i)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def filter_map(iterable, mapper, falsy=False):
|
|
20
|
+
for i in iterable:
|
|
21
|
+
if (not falsy and i is not None) or (falsy and i):
|
|
22
|
+
yield mapper(i)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def flat_map(iterable, mapper):
|
|
26
|
+
for i in iterable:
|
|
27
|
+
for j in mapper(i):
|
|
28
|
+
yield j
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def flatten(iterable):
|
|
32
|
+
from collections.abc import Iterable
|
|
33
|
+
|
|
34
|
+
for i in iterable:
|
|
35
|
+
if isinstance(i, str) or not isinstance(i, Iterable):
|
|
36
|
+
yield i
|
|
37
|
+
else:
|
|
38
|
+
yield from Iterator.flatten(i)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def reduce(iterable, accumulator, identity=None):
|
|
42
|
+
if len(iterable) == 0:
|
|
43
|
+
return identity
|
|
44
|
+
|
|
45
|
+
idx = 0
|
|
46
|
+
if identity is None:
|
|
47
|
+
identity = iterable[0]
|
|
48
|
+
idx = 1
|
|
49
|
+
|
|
50
|
+
for i in iterable[idx:]:
|
|
51
|
+
identity = accumulator(identity, i)
|
|
52
|
+
return identity
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def for_each(iterable, operation):
|
|
56
|
+
for i in iterable:
|
|
57
|
+
operation(i)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def peek(iterable, operation):
|
|
61
|
+
for i in iterable:
|
|
62
|
+
operation(i)
|
|
63
|
+
yield i
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def iterate(seed, operation):
|
|
67
|
+
while True:
|
|
68
|
+
yield seed
|
|
69
|
+
seed = operation(seed)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def generate(supplier):
|
|
73
|
+
while True:
|
|
74
|
+
yield supplier()
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def distinct(iterable):
|
|
78
|
+
elements = set()
|
|
79
|
+
for i in iterable:
|
|
80
|
+
if i not in elements:
|
|
81
|
+
elements.add(i)
|
|
82
|
+
yield i
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def skip(iterable, count):
|
|
86
|
+
for i in iterable:
|
|
87
|
+
if count > 0:
|
|
88
|
+
count -= 1
|
|
89
|
+
else:
|
|
90
|
+
yield i
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def limit(iterable, count):
|
|
94
|
+
for i in iterable:
|
|
95
|
+
if count == 0:
|
|
96
|
+
break
|
|
97
|
+
yield i
|
|
98
|
+
count -= 1
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def tail(iterable, count):
|
|
102
|
+
import collections
|
|
103
|
+
|
|
104
|
+
for i in collections.deque(iterable, maxlen=count):
|
|
105
|
+
yield i
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def take_while(iterable, predicate):
|
|
109
|
+
for i in iterable:
|
|
110
|
+
if not predicate(i):
|
|
111
|
+
break
|
|
112
|
+
yield i
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def drop_while(iterable, predicate):
|
|
116
|
+
iterator = iter(iterable)
|
|
117
|
+
for x in iterator:
|
|
118
|
+
if not predicate(x):
|
|
119
|
+
yield x
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
for x in iterator:
|
|
123
|
+
yield x
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def sorted(iterable, comparator=None, reverse=False):
|
|
127
|
+
for i in sorted(iterable, key=comparator, reverse=reverse):
|
|
128
|
+
yield i
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def compare_with(iterable, other_iterable, comparator=None):
|
|
132
|
+
for i, j in zip(iterable, other_iterable):
|
|
133
|
+
if (comparator and not comparator(i, j)) or i != j:
|
|
134
|
+
return False
|
|
135
|
+
return True
|
pyrio/itertools_mixin.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from collections.abc import Iterable, Sized
|
|
2
|
+
import itertools as it
|
|
3
|
+
import operator
|
|
4
|
+
|
|
5
|
+
from pyrio.optional import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ItertoolsMixin:
|
|
9
|
+
_iterable: Iterable | Sized
|
|
10
|
+
|
|
11
|
+
def use(self, it_function, *args, **kwargs):
|
|
12
|
+
"""Provides integration with itertools methods"""
|
|
13
|
+
import inspect
|
|
14
|
+
|
|
15
|
+
if args:
|
|
16
|
+
raise ValueError("Use keyword arguments only")
|
|
17
|
+
|
|
18
|
+
if self._handle_no_signature_functions(it_function, **kwargs):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
signature = inspect.signature(it_function).parameters
|
|
22
|
+
if self._handle_no_kwargs_functions(signature, it_function, **kwargs):
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
return self._handle_default_signature_functions(signature, it_function, **kwargs)
|
|
26
|
+
|
|
27
|
+
def _handle_no_signature_functions(self, it_function, **kwargs):
|
|
28
|
+
NO_SIGNATURE_FUNCTIONS = ["chain", "islice", "product", "repeat", "zip_longest"]
|
|
29
|
+
|
|
30
|
+
if it_function.__name__ not in NO_SIGNATURE_FUNCTIONS:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
if it_function.__name__ in ("product", "zip_longest"):
|
|
34
|
+
if isinstance(self._iterable, range):
|
|
35
|
+
self._iterable = it_function(self._iterable, **kwargs)
|
|
36
|
+
else:
|
|
37
|
+
self._iterable = it_function(*self._iterable, **kwargs)
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
# functions like 'chain' don't expect key-word arguments
|
|
41
|
+
self._iterable = it_function(self._iterable, *kwargs.values())
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def _handle_no_kwargs_functions(self, signature, it_function, **kwargs):
|
|
45
|
+
NO_KWARGS_FUNCTIONS = ["dropwhile", "filterfalse", "starmap", "takewhile", "tee"]
|
|
46
|
+
|
|
47
|
+
# handle functions that take only iterable as arg
|
|
48
|
+
if len(signature.keys()) == 1 and "iterable" in signature:
|
|
49
|
+
self._iterable = it_function(self._iterable)
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
# handle functions that take no kwargs
|
|
53
|
+
if it_function.__name__ in NO_KWARGS_FUNCTIONS:
|
|
54
|
+
if it_function.__name__ == "tee":
|
|
55
|
+
self._iterable = it_function(self._iterable, *kwargs.values())
|
|
56
|
+
else:
|
|
57
|
+
self._iterable = it_function(*kwargs.values(), self._iterable)
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def _handle_default_signature_functions(self, signature, it_function, **kwargs):
|
|
62
|
+
if "iterable" in signature:
|
|
63
|
+
kwargs["iterable"] = self._iterable
|
|
64
|
+
elif "data" in signature:
|
|
65
|
+
kwargs["data"] = self._iterable
|
|
66
|
+
self._iterable = it_function(**kwargs)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
# ### 'recipes' ###
|
|
70
|
+
# https://docs.python.org/3/library/itertools.html#itertools-recipes
|
|
71
|
+
def tabulate(self, mapper, start=0):
|
|
72
|
+
""" "Returns function(0), function(1), ..."""
|
|
73
|
+
self._iterable = map(mapper, it.count(start))
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def repeat_func(self, operation, times=None):
|
|
77
|
+
"""Repeats calls to func with specified arguments"""
|
|
78
|
+
self._iterable = it.starmap(operation, it.repeat(self._iterable, times=times))
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def ncycles(self, count=0):
|
|
82
|
+
"""Returns the stream elements n times"""
|
|
83
|
+
self._iterable = it.chain.from_iterable(it.repeat(tuple(self._iterable), count))
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def consume(self, n=None):
|
|
87
|
+
"""Advances the iterator n-steps ahead. If n is None, consumes stream entirely"""
|
|
88
|
+
import collections
|
|
89
|
+
|
|
90
|
+
if n is None:
|
|
91
|
+
self._iterable = collections.deque(self._iterable, maxlen=0)
|
|
92
|
+
return self
|
|
93
|
+
if n < 0:
|
|
94
|
+
raise ValueError("Consume boundary cannot be negative")
|
|
95
|
+
self._iterable = it.islice(self._iterable, n, len(self._iterable))
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def take_nth(self, idx, default=None):
|
|
99
|
+
"""Returns Optional with the nth element of the stream or a default value"""
|
|
100
|
+
if idx < 0:
|
|
101
|
+
idx = len(self._iterable) + idx
|
|
102
|
+
return Optional.of_nullable(next(it.islice(self._iterable, idx, None), default))
|
|
103
|
+
|
|
104
|
+
def all_equal(self, key=None):
|
|
105
|
+
"""Returns True if all elements of the stream are equal to each other"""
|
|
106
|
+
return len(list(it.islice(it.groupby(self._iterable, key), 2))) <= 1
|
|
107
|
+
|
|
108
|
+
def view(self, start=0, stop=None, step=None):
|
|
109
|
+
"""Provides access to a selected part of the stream"""
|
|
110
|
+
if start < 0:
|
|
111
|
+
start = len(self._iterable) + start
|
|
112
|
+
|
|
113
|
+
if stop and stop < 0:
|
|
114
|
+
stop = len(self._iterable) + stop
|
|
115
|
+
|
|
116
|
+
if step and step < 0:
|
|
117
|
+
raise ValueError("Step must be a positive integer or None")
|
|
118
|
+
|
|
119
|
+
self._iterable = it.islice(self._iterable, start, stop, step)
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
# ### unique ###
|
|
123
|
+
def unique(self, key=None, reverse=False):
|
|
124
|
+
"""Yields unique elements in sorted order. Supports unhashable inputs"""
|
|
125
|
+
self._iterable = self._unique(sorted(self._iterable, key=key, reverse=reverse), key=key)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _unique(iterable, key=None):
|
|
130
|
+
return map(next, map(operator.itemgetter(1), it.groupby(iterable, key)))
|
|
131
|
+
|
|
132
|
+
def unique_just_seen(self, key=None):
|
|
133
|
+
"""Yields unique elements, preserving order. Remembers only the element just seen"""
|
|
134
|
+
self._iterable = map(next, map(operator.itemgetter(1), it.groupby(self._iterable, key)))
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def unique_ever_seen(self, key=None):
|
|
138
|
+
"""Yields unique elements, preserving order. Remembers all elements ever seen"""
|
|
139
|
+
self._iterable = self._unique_ever_seen(self._iterable, key)
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _unique_ever_seen(iterable, key=None):
|
|
144
|
+
seen = set()
|
|
145
|
+
for element in iterable:
|
|
146
|
+
k = key(element) if key else element
|
|
147
|
+
if k not in seen:
|
|
148
|
+
seen.add(k)
|
|
149
|
+
yield element
|
|
150
|
+
|
|
151
|
+
# ### ###
|
|
152
|
+
def sliding_window(self, n):
|
|
153
|
+
"""Collects data into overlapping fixed-length chunks or blocks"""
|
|
154
|
+
if n < 0:
|
|
155
|
+
raise ValueError("Window size cannot be negative")
|
|
156
|
+
self._iterable = self._sliding_window(self._iterable, n)
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _sliding_window(iterable, n):
|
|
161
|
+
import collections
|
|
162
|
+
|
|
163
|
+
window = collections.deque(it.islice(iterable, n - 1), maxlen=n)
|
|
164
|
+
for x in it.islice(iterable, n - 1, len(iterable)):
|
|
165
|
+
window.append(x)
|
|
166
|
+
yield tuple(window)
|
|
167
|
+
|
|
168
|
+
def grouper(self, n, *, incomplete="fill", fill_value=None):
|
|
169
|
+
"""Collects data into non-overlapping fixed-length chunks or blocks"""
|
|
170
|
+
self._iterable = self._grouper(n, incomplete, fill_value)
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def _grouper(self, n, incomplete="fill", fill_value=None):
|
|
174
|
+
iterators = [iter(self._iterable)] * n
|
|
175
|
+
match incomplete:
|
|
176
|
+
case "fill":
|
|
177
|
+
return it.zip_longest(*iterators, fillvalue=fill_value)
|
|
178
|
+
case "strict":
|
|
179
|
+
return zip(*iterators, strict=True)
|
|
180
|
+
case "ignore":
|
|
181
|
+
return zip(*iterators)
|
|
182
|
+
case _:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Invalid incomplete flag '{incomplete}', expected: 'fill', 'strict', or 'ignore'"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def round_robin(self):
|
|
188
|
+
"""Visits input iterables in a cycle until each is exhausted"""
|
|
189
|
+
self._iterable = self._round_robin(self._iterable)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _round_robin(iterable):
|
|
194
|
+
# Algorithm credited to George Sakkis
|
|
195
|
+
iterators = map(iter, iterable)
|
|
196
|
+
for num_active in range(len(iterable), 0, -1):
|
|
197
|
+
iterators = it.cycle(it.islice(iterators, num_active))
|
|
198
|
+
yield from map(next, iterators)
|
|
199
|
+
|
|
200
|
+
def partition(self, predicate):
|
|
201
|
+
"""Partitions entries into false entries and true entries.
|
|
202
|
+
Returns a stream of two nested generators"""
|
|
203
|
+
true_iter, false_iter = it.tee(self._iterable)
|
|
204
|
+
self._iterable = filter(predicate, true_iter), it.filterfalse(predicate, false_iter)
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def subslices(self):
|
|
208
|
+
"""Returns all contiguous non-empty sub-slices"""
|
|
209
|
+
slices = it.starmap(slice, it.combinations(range(len(self._iterable) + 1), 2))
|
|
210
|
+
self._iterable = map(operator.getitem, it.repeat(self._iterable), slices) # noqa
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def find_indices(self, value, start=0, stop=None):
|
|
214
|
+
"""Returns indices where a value occurs in a sequence or iterable"""
|
|
215
|
+
self._iterable = self._find_indices(self._iterable, value, start, stop)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _find_indices(iterable, value, start=0, stop=None):
|
|
220
|
+
iterator = it.islice(iterable, start, stop)
|
|
221
|
+
for i, element in enumerate(iterator, start):
|
|
222
|
+
if element is value or element == value:
|
|
223
|
+
yield i
|
pyrio/optional.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pyrio.exception import NoSuchElementError, NullPointerError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Optional:
|
|
5
|
+
def __init__(self, element):
|
|
6
|
+
self._element = element
|
|
7
|
+
|
|
8
|
+
def __str__(self):
|
|
9
|
+
return f"Optional[{self._element}]"
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def empty():
|
|
13
|
+
"""Creates empty Optional"""
|
|
14
|
+
return Optional(None)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def of(element):
|
|
18
|
+
"""Creates Optional describing given non-null value"""
|
|
19
|
+
if element is None:
|
|
20
|
+
raise NullPointerError("Value cannot be None")
|
|
21
|
+
return Optional(element)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def of_nullable(element):
|
|
25
|
+
"""Returns an Optional describing the given value, if non-null, otherwise returns an empty Optional"""
|
|
26
|
+
return Optional(element)
|
|
27
|
+
|
|
28
|
+
def get(self):
|
|
29
|
+
"""If a value is present, returns the value, otherwise raises an Exception"""
|
|
30
|
+
if self.is_empty():
|
|
31
|
+
raise NoSuchElementError("Optional is empty")
|
|
32
|
+
return self._element
|
|
33
|
+
|
|
34
|
+
def is_present(self):
|
|
35
|
+
"""Returns bool whether a value is present"""
|
|
36
|
+
return not self.is_empty()
|
|
37
|
+
|
|
38
|
+
def is_empty(self):
|
|
39
|
+
"""Returns bool whether the Optional is empty"""
|
|
40
|
+
return self._element is None
|
|
41
|
+
|
|
42
|
+
def if_present(self, action):
|
|
43
|
+
"""Performs given action with the value if the Optional is not empty"""
|
|
44
|
+
if self.is_present():
|
|
45
|
+
action(self.get())
|
|
46
|
+
|
|
47
|
+
def if_present_or_else(self, action, empty_action):
|
|
48
|
+
"""Performs given action with the value if the Optional is not empty, otherwise calls fallback 'empty_action'"""
|
|
49
|
+
if self.is_present():
|
|
50
|
+
action(self.get())
|
|
51
|
+
else:
|
|
52
|
+
empty_action()
|
|
53
|
+
|
|
54
|
+
def or_else(self, value):
|
|
55
|
+
"""Returns the value if present, or a provided argument otherwise.
|
|
56
|
+
Safe alternative to get() method."""
|
|
57
|
+
return self._element if self.is_present() else value
|
|
58
|
+
|
|
59
|
+
def or_else_get(self, supplier):
|
|
60
|
+
"""Returns the value if present, or calls a 'supplier' function otherwise.
|
|
61
|
+
Safe alternative to get() method."""
|
|
62
|
+
return self._element if self.is_present() else supplier()
|
pyrio/stream.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from pyrio.decorator import pre_call, handle_consumed
|
|
2
|
+
from pyrio.exception import IllegalStateError
|
|
3
|
+
from pyrio.iterator import Iterator
|
|
4
|
+
from pyrio.itertools_mixin import ItertoolsMixin
|
|
5
|
+
from pyrio.optional import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pre_call(handle_consumed)
|
|
9
|
+
class Stream(ItertoolsMixin):
|
|
10
|
+
def __init__(self, iterable):
|
|
11
|
+
"""Creates Stream from a collection"""
|
|
12
|
+
self._iterable = iterable
|
|
13
|
+
self._is_consumed = False
|
|
14
|
+
|
|
15
|
+
def __iter__(self):
|
|
16
|
+
return iter(self._iterable)
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def of(cls, *iterable):
|
|
20
|
+
"""Creates Stream from args"""
|
|
21
|
+
return cls(iterable)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def empty(cls):
|
|
25
|
+
"""Creates empty Stream"""
|
|
26
|
+
return cls([])
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def iterate(cls, seed, operation):
|
|
30
|
+
"""Creates infinite ordered Stream"""
|
|
31
|
+
return cls(Iterator.iterate(seed, operation))
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def generate(cls, supplier):
|
|
35
|
+
"""Creates infinite unordered Stream with values generated by given supplier function"""
|
|
36
|
+
return cls(Iterator.generate(supplier))
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def constant(cls, element):
|
|
40
|
+
"""Creates infinite Stream with given value"""
|
|
41
|
+
return cls.generate(lambda: element)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def concat(*streams):
|
|
45
|
+
"""Concatenates several streams together or add new streams to the current one"""
|
|
46
|
+
return Stream(Iterator.concat(*streams))
|
|
47
|
+
|
|
48
|
+
def prepend(self, *iterable):
|
|
49
|
+
"""Prepends iterable to current stream"""
|
|
50
|
+
self._iterable = Iterator.concat(iterable, self._iterable)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def filter(self, predicate):
|
|
54
|
+
"""Filters values in stream based on given predicate function"""
|
|
55
|
+
self._iterable = Iterator.filter(self._iterable, predicate)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def map(self, mapper):
|
|
59
|
+
"""Returns a stream consisting of the results of applying the given function to the elements of this stream"""
|
|
60
|
+
self._iterable = Iterator.map(self._iterable, mapper)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def filter_map(self, mapper, *, falsy=False):
|
|
64
|
+
"""Filters out all None or falsy values and applies mapper function to the elements of the stream"""
|
|
65
|
+
self._iterable = Iterator.filter_map(self._iterable, mapper, falsy)
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def flat_map(self, mapper):
|
|
69
|
+
"""Maps each element of the stream and yields the elements of the produced iterators"""
|
|
70
|
+
self._iterable = Iterator.flat_map(self._iterable, mapper)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def flatten(self):
|
|
74
|
+
"""Converts a Stream of multidimensional collection into a one-dimensional"""
|
|
75
|
+
self._iterable = Iterator.flatten(self._iterable)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def for_each(self, operation):
|
|
79
|
+
"""Performs an action for each element of this stream"""
|
|
80
|
+
return Iterator.for_each(self._iterable, operation)
|
|
81
|
+
|
|
82
|
+
def peek(self, operation):
|
|
83
|
+
"""Performs the provided operation on each element of the stream without consuming it"""
|
|
84
|
+
self._iterable = Iterator.peek(self._iterable, operation)
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def reduce(self, accumulator, identity=None):
|
|
88
|
+
"""Reduces the elements to a single one, by repeatedly applying a reducing operation.
|
|
89
|
+
Returns Optional with the result, if any, or None"""
|
|
90
|
+
return Optional.of_nullable(Iterator.reduce(self._iterable, accumulator, identity))
|
|
91
|
+
|
|
92
|
+
def distinct(self):
|
|
93
|
+
"""Returns a stream with the distinct elements of the current one"""
|
|
94
|
+
self._iterable = Iterator.distinct(self._iterable)
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def count(self):
|
|
98
|
+
"""Returns the count of elements in the stream"""
|
|
99
|
+
return len(tuple(self._iterable))
|
|
100
|
+
|
|
101
|
+
def sum(self):
|
|
102
|
+
"""Sums the elements of the stream"""
|
|
103
|
+
if len(self._iterable) == 0:
|
|
104
|
+
return 0
|
|
105
|
+
if any(isinstance(x, (int | float | None)) for x in self._iterable) is False:
|
|
106
|
+
raise ValueError("Cannot apply sum on non-number elements")
|
|
107
|
+
return sum(self._iterable)
|
|
108
|
+
|
|
109
|
+
def skip(self, count):
|
|
110
|
+
"""Discards the first n elements of the stream and returns a new stream with the remaining ones"""
|
|
111
|
+
if count < 0:
|
|
112
|
+
raise ValueError("Skip count cannot be negative")
|
|
113
|
+
self._iterable = Iterator.skip(self._iterable, count)
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def limit(self, count):
|
|
117
|
+
"""Returns a stream with the first n elements, or fewer if the underlying iterator ends sooner"""
|
|
118
|
+
if count < 0:
|
|
119
|
+
raise ValueError("Limit count cannot be negative")
|
|
120
|
+
self._iterable = Iterator.limit(self._iterable, count)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def head(self, count):
|
|
124
|
+
"""Alias for 'limit'"""
|
|
125
|
+
if count < 0:
|
|
126
|
+
raise ValueError("Head count cannot be negative")
|
|
127
|
+
self._iterable = Iterator.limit(self._iterable, count)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def tail(self, count):
|
|
131
|
+
"""Returns a stream with the last n elements, or fewer if the underlying iterator ends sooner"""
|
|
132
|
+
if count < 0:
|
|
133
|
+
raise ValueError("Tail count cannot be negative")
|
|
134
|
+
self._iterable = Iterator.tail(self._iterable, count)
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def take_while(self, predicate):
|
|
138
|
+
"""Returns a stream that yields elements based on a predicate"""
|
|
139
|
+
self._iterable = Iterator.take_while(self._iterable, predicate)
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def drop_while(self, predicate):
|
|
143
|
+
"""Returns a stream that skips elements based on a predicate and yields the remaining ones"""
|
|
144
|
+
self._iterable = Iterator.drop_while(self._iterable, predicate)
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def find_first(self, predicate=None):
|
|
148
|
+
"""Searches for an element of the stream that satisfies a predicate.
|
|
149
|
+
Returns an Optional with the first found value, if any, or None"""
|
|
150
|
+
return Optional.of_nullable(next(filter(predicate, self._iterable), None))
|
|
151
|
+
|
|
152
|
+
def find_any(self, predicate=None):
|
|
153
|
+
"""Searches for an element of the stream that satisfies a predicate.
|
|
154
|
+
Returns an Optional with some of the found values, if any, or None"""
|
|
155
|
+
import random
|
|
156
|
+
|
|
157
|
+
if predicate:
|
|
158
|
+
self.filter(predicate)
|
|
159
|
+
try:
|
|
160
|
+
return Optional.of(random.choice(list(self._iterable)))
|
|
161
|
+
except IndexError:
|
|
162
|
+
return Optional.of_nullable(None)
|
|
163
|
+
|
|
164
|
+
def any_match(self, predicate):
|
|
165
|
+
"""Returns whether any elements of the stream match the given predicate"""
|
|
166
|
+
return any(predicate(i) for i in self._iterable)
|
|
167
|
+
|
|
168
|
+
def all_match(self, predicate):
|
|
169
|
+
"""Returns whether all elements of the stream match the given predicate"""
|
|
170
|
+
return all(predicate(i) for i in self._iterable)
|
|
171
|
+
|
|
172
|
+
def none_match(self, predicate):
|
|
173
|
+
"""Returns whether no elements of the stream match the given predicate"""
|
|
174
|
+
return not all(predicate(i) for i in self._iterable)
|
|
175
|
+
|
|
176
|
+
def min(self, comparator=None, default=None):
|
|
177
|
+
"""Returns the minimum element of the stream according to the given comparator"""
|
|
178
|
+
return Optional.of_nullable(min(self._iterable, key=comparator, default=default))
|
|
179
|
+
|
|
180
|
+
def max(self, comparator=None, default=None):
|
|
181
|
+
"""Returns the maximum element of the stream according to the given comparator"""
|
|
182
|
+
return Optional.of_nullable(max(self._iterable, key=comparator, default=default))
|
|
183
|
+
|
|
184
|
+
def compare_with(self, other, comparator=None):
|
|
185
|
+
"""Compares current stream with another one based on a given comparator"""
|
|
186
|
+
return Iterator.compare_with(self._iterable, other, comparator)
|
|
187
|
+
|
|
188
|
+
def sorted(self, comparator=None, *, reverse=False):
|
|
189
|
+
"""Sorts the elements of the current stream according to natural order or based on the given comparator.
|
|
190
|
+
If 'reverse' flag is True, the elements are sorted in descending order"""
|
|
191
|
+
self._iterable = Iterator.sorted(self._iterable, comparator, reverse)
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
# ### collectors ###
|
|
195
|
+
def collect(self, collection_type, dict_collector=None, dict_merger=None):
|
|
196
|
+
"""Returns a collections from the stream.
|
|
197
|
+
|
|
198
|
+
In case of dict:
|
|
199
|
+
The 'dict_collector' function receives an element from the stream and returns a (key, value) pair
|
|
200
|
+
specifying how the dict should be constructed.
|
|
201
|
+
|
|
202
|
+
The 'dict_merger' functions indicates in the case of a collision (duplicate keys), which entry should be kept.
|
|
203
|
+
E.g. lambda old, new: new"""
|
|
204
|
+
import builtins
|
|
205
|
+
|
|
206
|
+
match collection_type:
|
|
207
|
+
case builtins.tuple:
|
|
208
|
+
return self.to_tuple()
|
|
209
|
+
case builtins.list:
|
|
210
|
+
return self.to_list()
|
|
211
|
+
case builtins.set:
|
|
212
|
+
return self.to_set()
|
|
213
|
+
case builtins.dict:
|
|
214
|
+
if dict_collector is None:
|
|
215
|
+
raise ValueError("Missing dict_collector")
|
|
216
|
+
return self.to_dict(dict_collector, dict_merger)
|
|
217
|
+
case _:
|
|
218
|
+
raise ValueError("Invalid collection type")
|
|
219
|
+
|
|
220
|
+
def to_list(self):
|
|
221
|
+
"""Returns a list of the elements of the current stream"""
|
|
222
|
+
return list(self._iterable)
|
|
223
|
+
|
|
224
|
+
def to_tuple(self):
|
|
225
|
+
"""Returns a tuple of the elements of the current stream"""
|
|
226
|
+
return tuple(self._iterable)
|
|
227
|
+
|
|
228
|
+
def to_set(self):
|
|
229
|
+
"""Returns a set of the elements of the current stream"""
|
|
230
|
+
return set(self._iterable)
|
|
231
|
+
|
|
232
|
+
def to_dict(self, collector, merger=None):
|
|
233
|
+
"""Returns a dict of the elements of the current stream.
|
|
234
|
+
|
|
235
|
+
The 'collector' function receives an element from the stream and returns a (key, value) pair
|
|
236
|
+
specifying how the dict should be constructed.
|
|
237
|
+
|
|
238
|
+
The 'merger' functions indicates in the case of a collision (duplicate keys), which entry should be kept.
|
|
239
|
+
E.g. lambda old, new: new"""
|
|
240
|
+
result = {}
|
|
241
|
+
for k, v in (collector(i) for i in self._iterable):
|
|
242
|
+
if k in result:
|
|
243
|
+
if merger is None:
|
|
244
|
+
raise IllegalStateError(f"Key '{k}' already exists")
|
|
245
|
+
v = merger(result[k], v)
|
|
246
|
+
result[k] = v
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
def group_by(self, classifier=None, collector=None):
|
|
250
|
+
if collector is None:
|
|
251
|
+
return {key: list(group) for key, group in self._group_by(classifier)}
|
|
252
|
+
|
|
253
|
+
result = {}
|
|
254
|
+
for key, group in self._group_by(classifier):
|
|
255
|
+
key, group = collector(key, list(group))
|
|
256
|
+
if hasattr(group, "__iter__"):
|
|
257
|
+
if key not in result:
|
|
258
|
+
result[key] = []
|
|
259
|
+
result[key] += group
|
|
260
|
+
else:
|
|
261
|
+
result[key] = group
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
def _group_by(self, classifier=None):
|
|
265
|
+
# https://docs.python.org/3/library/itertools.html#itertools.groupby
|
|
266
|
+
classifier = (lambda x: x) if classifier is None else classifier
|
|
267
|
+
iterator = iter(self._iterable)
|
|
268
|
+
exhausted = False
|
|
269
|
+
|
|
270
|
+
def _grouper(target_key):
|
|
271
|
+
nonlocal curr_value, curr_key, exhausted
|
|
272
|
+
yield curr_value
|
|
273
|
+
for curr_value in iterator:
|
|
274
|
+
curr_key = classifier(curr_value)
|
|
275
|
+
if curr_key != target_key:
|
|
276
|
+
return
|
|
277
|
+
yield curr_value
|
|
278
|
+
exhausted = True
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
curr_value = next(iterator)
|
|
282
|
+
except StopIteration:
|
|
283
|
+
return
|
|
284
|
+
curr_key = classifier(curr_value)
|
|
285
|
+
|
|
286
|
+
while not exhausted:
|
|
287
|
+
target_key = curr_key
|
|
288
|
+
curr_group = _grouper(target_key)
|
|
289
|
+
yield curr_key, curr_group
|
|
290
|
+
if curr_key == target_key:
|
|
291
|
+
for _ in curr_group:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def quantify(self, predicate=bool):
|
|
295
|
+
"""Count how many of the elements are Truthy or evaluate to True based on a given predicate"""
|
|
296
|
+
return sum(self.map(predicate))
|
|
297
|
+
|
|
298
|
+
# NB: give access to handle_consumed decorator to toggle flag
|
|
299
|
+
def take_nth(self, idx, default=None):
|
|
300
|
+
"""Returns Optional with the nth element of the stream or a default value"""
|
|
301
|
+
return super().take_nth(idx, default)
|
|
302
|
+
|
|
303
|
+
def all_equal(self, key=None):
|
|
304
|
+
"""Returns True if all elements of the stream are equal to each other"""
|
|
305
|
+
return super().all_equal(key)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kaloyan Ivanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyrio
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Java-inspired Streams Api
|
|
5
|
+
Author: kaliv0
|
|
6
|
+
Author-email: kaloyan.ivanov88@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://github.com/kaliv0/pyrio/blob/main/assets/Pyrio.jpg?raw=true" width="400" alt="Pyrio">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
# pyrio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
[](https://github.com/kaliv0/pyrio/actions/workflows/ci.yml)
|
|
21
|
+
[](https://codecov.io/gh/kaliv0/pyrio)
|
|
22
|
+
[](https://pypi.org/project/pyrio/)
|
|
23
|
+
|
|
24
|
+
<br>Java-inspired Streams Api
|
|
25
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyrio/__init__.py,sha256=HkXoOFVMiDnGl7AdnEgN2q4WReQhR2SmMDXt2w72D-I,80
|
|
2
|
+
pyrio/decorator.py,sha256=tKGcY1s1zopEqHK21m-ndLUhcXSc9ahUaNN53uXj6U8,1182
|
|
3
|
+
pyrio/exception.py,sha256=UXFw_3F1jrvPY0dD-UDFvcpogxzM_N2CNhsNoJq2N-U,139
|
|
4
|
+
pyrio/iterator.py,sha256=aAXQUjq5z80ZFkIRxmYHpld8X4JT7Cgk2gDZexkfDkU,3284
|
|
5
|
+
pyrio/itertools_mixin.py,sha256=qOPojN_hg3uGih1EaylkIHX_Epxc3Qg8sRdKBNt_Mzo,8633
|
|
6
|
+
pyrio/optional.py,sha256=a277B0b7DLsKinnJMvjqmYm3oKAryGhJDHyyVOKsDCY,2091
|
|
7
|
+
pyrio/stream.py,sha256=PLOyn6KSpDfq2VoazxsrWOCGv22EaBNDS3HGW73B-0c,12025
|
|
8
|
+
pyrio-1.0.0.dist-info/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
9
|
+
pyrio-1.0.0.dist-info/METADATA,sha256=PV5VNxjzVxZHj8-u2kuy7iSTshKXA9t0njy0B4cluiE,935
|
|
10
|
+
pyrio-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
11
|
+
pyrio-1.0.0.dist-info/RECORD,,
|