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 ADDED
@@ -0,0 +1,2 @@
1
+ from .stream import Stream as Stream
2
+ from .optional import Optional as Optional
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
@@ -0,0 +1,10 @@
1
+ class IllegalStateError(Exception):
2
+ pass
3
+
4
+
5
+ class NullPointerError(Exception):
6
+ pass
7
+
8
+
9
+ class NoSuchElementError(Exception):
10
+ pass
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
@@ -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
+ ![Python 3.x](https://img.shields.io/badge/python-3.12-blue?style=flat-square&logo=Python&logoColor=white)
20
+ [![tests](https://img.shields.io/github/actions/workflow/status/kaliv0/pyrio/ci.yml)](https://github.com/kaliv0/pyrio/actions/workflows/ci.yml)
21
+ [![codecov](https://codecov.io/gh/kaliv0/pyrio/graph/badge.svg?token=7EEG43BL33)](https://codecov.io/gh/kaliv0/pyrio)
22
+ [![PyPI](https://img.shields.io/pypi/v/pyrio.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any