streamish 0.1.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.
- streamish/__init__.py +63 -0
- streamish/_util.py +38 -0
- streamish/ops/__init__.py +43 -0
- streamish/ops/combine.py +101 -0
- streamish/ops/filter.py +235 -0
- streamish/ops/group.py +183 -0
- streamish/ops/transform.py +322 -0
- streamish/stream.py +162 -0
- streamish-0.1.0.dist-info/METADATA +219 -0
- streamish-0.1.0.dist-info/RECORD +12 -0
- streamish-0.1.0.dist-info/WHEEL +4 -0
- streamish-0.1.0.dist-info/licenses/LICENSE +21 -0
streamish/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Streamish - Iterator and async iterator utilities."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterable, Iterable
|
|
4
|
+
|
|
5
|
+
from streamish.ops import (
|
|
6
|
+
batch,
|
|
7
|
+
chain,
|
|
8
|
+
chain_async,
|
|
9
|
+
distinct,
|
|
10
|
+
distinct_by,
|
|
11
|
+
enumerate,
|
|
12
|
+
filter,
|
|
13
|
+
flat_map,
|
|
14
|
+
flatten,
|
|
15
|
+
interleave,
|
|
16
|
+
map,
|
|
17
|
+
map_async,
|
|
18
|
+
merge,
|
|
19
|
+
partition,
|
|
20
|
+
partition_async,
|
|
21
|
+
scan,
|
|
22
|
+
skip,
|
|
23
|
+
skip_while,
|
|
24
|
+
take,
|
|
25
|
+
take_while,
|
|
26
|
+
window,
|
|
27
|
+
zip,
|
|
28
|
+
zip_async,
|
|
29
|
+
)
|
|
30
|
+
from streamish.stream import Stream
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"stream",
|
|
34
|
+
"Stream",
|
|
35
|
+
"map",
|
|
36
|
+
"map_async",
|
|
37
|
+
"filter",
|
|
38
|
+
"take",
|
|
39
|
+
"skip",
|
|
40
|
+
"take_while",
|
|
41
|
+
"skip_while",
|
|
42
|
+
"distinct",
|
|
43
|
+
"distinct_by",
|
|
44
|
+
"flatten",
|
|
45
|
+
"flat_map",
|
|
46
|
+
"enumerate",
|
|
47
|
+
"scan",
|
|
48
|
+
"batch",
|
|
49
|
+
"window",
|
|
50
|
+
"partition",
|
|
51
|
+
"partition_async",
|
|
52
|
+
"zip",
|
|
53
|
+
"zip_async",
|
|
54
|
+
"chain",
|
|
55
|
+
"chain_async",
|
|
56
|
+
"interleave",
|
|
57
|
+
"merge",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def stream[T](source: Iterable[T] | AsyncIterable[T]) -> Stream[T]:
|
|
62
|
+
"""Create a Stream from an iterable or async iterable."""
|
|
63
|
+
return Stream(source)
|
streamish/_util.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Internal utility helpers."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import (
|
|
5
|
+
AsyncGenerator,
|
|
6
|
+
AsyncIterable,
|
|
7
|
+
AsyncIterator,
|
|
8
|
+
Callable,
|
|
9
|
+
Iterable,
|
|
10
|
+
Iterator,
|
|
11
|
+
)
|
|
12
|
+
from typing import Any, TypeGuard
|
|
13
|
+
|
|
14
|
+
__all__ = ["is_async_iterable", "is_awaitable", "ensure_async_iterator"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_async_iterable[T](
|
|
18
|
+
obj: Iterable[T] | AsyncIterable[T],
|
|
19
|
+
) -> TypeGuard[AsyncIterable[T]]:
|
|
20
|
+
"""Check if object is an async iterable."""
|
|
21
|
+
return hasattr(obj, "__aiter__")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_awaitable(fn: Callable[..., Any]) -> bool:
|
|
25
|
+
"""Check if function is async (returns awaitable)."""
|
|
26
|
+
return inspect.iscoroutinefunction(fn)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def ensure_async_iterator[T](
|
|
30
|
+
it: Iterator[T] | AsyncIterator[T],
|
|
31
|
+
) -> AsyncGenerator[T]:
|
|
32
|
+
"""Convert sync iterator to async iterator if needed."""
|
|
33
|
+
if isinstance(it, AsyncIterator):
|
|
34
|
+
async for item in it:
|
|
35
|
+
yield item
|
|
36
|
+
else:
|
|
37
|
+
for item in it:
|
|
38
|
+
yield item
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stream operations."""
|
|
2
|
+
|
|
3
|
+
from streamish.ops.combine import chain, chain_async, interleave, merge, zip_async
|
|
4
|
+
from streamish.ops.combine import zip_ as zip
|
|
5
|
+
from streamish.ops.filter import (
|
|
6
|
+
distinct,
|
|
7
|
+
distinct_by,
|
|
8
|
+
skip,
|
|
9
|
+
skip_while,
|
|
10
|
+
take,
|
|
11
|
+
take_while,
|
|
12
|
+
)
|
|
13
|
+
from streamish.ops.group import batch, partition, partition_async, window
|
|
14
|
+
from streamish.ops.transform import enumerate_ as enumerate
|
|
15
|
+
from streamish.ops.transform import filter_ as filter
|
|
16
|
+
from streamish.ops.transform import flat_map, flatten, map_async, scan
|
|
17
|
+
from streamish.ops.transform import map_ as map
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"map",
|
|
21
|
+
"filter",
|
|
22
|
+
"take",
|
|
23
|
+
"skip",
|
|
24
|
+
"take_while",
|
|
25
|
+
"skip_while",
|
|
26
|
+
"distinct",
|
|
27
|
+
"distinct_by",
|
|
28
|
+
"enumerate",
|
|
29
|
+
"scan",
|
|
30
|
+
"flatten",
|
|
31
|
+
"flat_map",
|
|
32
|
+
"batch",
|
|
33
|
+
"window",
|
|
34
|
+
"partition",
|
|
35
|
+
"partition_async",
|
|
36
|
+
"zip",
|
|
37
|
+
"zip_async",
|
|
38
|
+
"chain",
|
|
39
|
+
"chain_async",
|
|
40
|
+
"interleave",
|
|
41
|
+
"merge",
|
|
42
|
+
"map_async",
|
|
43
|
+
]
|
streamish/ops/combine.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Combine operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
|
|
5
|
+
|
|
6
|
+
from streamish._util import ensure_async_iterator, is_async_iterable
|
|
7
|
+
|
|
8
|
+
__all__ = ["zip_", "zip_async", "chain", "chain_async", "interleave", "merge"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def zip_[T](*iterables: Iterable[T]) -> Iterator[tuple[T, ...]]:
|
|
12
|
+
"""Zip iterables together."""
|
|
13
|
+
iters = [iter(it) for it in iterables]
|
|
14
|
+
while True:
|
|
15
|
+
result: list[T] = []
|
|
16
|
+
for it in iters:
|
|
17
|
+
try:
|
|
18
|
+
result.append(next(it))
|
|
19
|
+
except StopIteration:
|
|
20
|
+
return
|
|
21
|
+
yield tuple(result)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def zip_async[T](
|
|
25
|
+
*iterables: AsyncIterable[T] | Iterable[T],
|
|
26
|
+
) -> AsyncIterator[tuple[T, ...]]:
|
|
27
|
+
"""Zip async iterables together."""
|
|
28
|
+
aiters: list[AsyncIterator[T]] = [
|
|
29
|
+
it.__aiter__() if is_async_iterable(it) else ensure_async_iterator(iter(it)) # type: ignore[union-attr, arg-type]
|
|
30
|
+
for it in iterables
|
|
31
|
+
]
|
|
32
|
+
while True:
|
|
33
|
+
try:
|
|
34
|
+
results: list[T] = []
|
|
35
|
+
for ait in aiters:
|
|
36
|
+
results.append(await ait.__anext__())
|
|
37
|
+
yield tuple(results)
|
|
38
|
+
except StopAsyncIteration:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def chain[T](*iterables: Iterable[T]) -> Iterator[T]:
|
|
43
|
+
"""Chain iterables together."""
|
|
44
|
+
for it in iterables:
|
|
45
|
+
yield from it
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def chain_async[T](
|
|
49
|
+
*iterables: AsyncIterable[T] | Iterable[T],
|
|
50
|
+
) -> AsyncIterator[T]:
|
|
51
|
+
"""Chain async iterables together."""
|
|
52
|
+
for it in iterables:
|
|
53
|
+
if is_async_iterable(it):
|
|
54
|
+
async for item in it: # type: ignore[union-attr]
|
|
55
|
+
yield item
|
|
56
|
+
else:
|
|
57
|
+
for item in it: # type: ignore[union-attr]
|
|
58
|
+
yield item
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def interleave[T](*iterables: Iterable[T]) -> Iterator[T]:
|
|
62
|
+
"""Alternate elements from iterables (round-robin)."""
|
|
63
|
+
iters: list[Iterator[T]] = [iter(it) for it in iterables]
|
|
64
|
+
while iters:
|
|
65
|
+
next_iters: list[Iterator[T]] = []
|
|
66
|
+
for it in iters:
|
|
67
|
+
try:
|
|
68
|
+
yield next(it)
|
|
69
|
+
next_iters.append(it)
|
|
70
|
+
except StopIteration:
|
|
71
|
+
pass
|
|
72
|
+
iters = next_iters
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def merge[T](*iterables: AsyncIterable[T]) -> AsyncIterator[T]:
|
|
76
|
+
"""Merge async iterables - emit as items arrive."""
|
|
77
|
+
pending: set[asyncio.Task[tuple[int, T | None, bool]]] = set()
|
|
78
|
+
aiters: dict[int, AsyncIterator[T]] = {}
|
|
79
|
+
|
|
80
|
+
for i, it in enumerate(iterables):
|
|
81
|
+
aiters[i] = it.__aiter__()
|
|
82
|
+
task = asyncio.create_task(_fetch_next(i, aiters[i]))
|
|
83
|
+
pending.add(task)
|
|
84
|
+
|
|
85
|
+
while pending:
|
|
86
|
+
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
|
87
|
+
for task in done:
|
|
88
|
+
idx, value, exhausted = task.result()
|
|
89
|
+
if exhausted:
|
|
90
|
+
continue
|
|
91
|
+
yield value # type: ignore[misc]
|
|
92
|
+
new_task = asyncio.create_task(_fetch_next(idx, aiters[idx]))
|
|
93
|
+
pending.add(new_task)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _fetch_next[T](idx: int, ait: AsyncIterator[T]) -> tuple[int, T | None, bool]:
|
|
97
|
+
try:
|
|
98
|
+
value = await ait.__anext__()
|
|
99
|
+
return (idx, value, False)
|
|
100
|
+
except StopAsyncIteration:
|
|
101
|
+
return (idx, None, True)
|
streamish/ops/filter.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Filter operations."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import (
|
|
4
|
+
AsyncIterable,
|
|
5
|
+
AsyncIterator,
|
|
6
|
+
Callable,
|
|
7
|
+
Hashable,
|
|
8
|
+
Iterable,
|
|
9
|
+
Iterator,
|
|
10
|
+
)
|
|
11
|
+
from typing import overload
|
|
12
|
+
|
|
13
|
+
from streamish._util import is_async_iterable
|
|
14
|
+
|
|
15
|
+
__all__ = ["take", "skip", "take_while", "skip_while", "distinct", "distinct_by"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def take[T](n: int, it: Iterable[T]) -> Iterator[T]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def take[T](n: int, it: AsyncIterable[T]) -> AsyncIterator[T]: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def take[T](
|
|
27
|
+
n: int, it: Iterable[T] | AsyncIterable[T]
|
|
28
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
29
|
+
"""Take first n elements."""
|
|
30
|
+
if is_async_iterable(it):
|
|
31
|
+
return _take_async(n, it) # type: ignore[arg-type]
|
|
32
|
+
return _take_sync(n, it) # type: ignore[arg-type, return-value]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _take_sync[T](n: int, it: Iterable[T]) -> Iterator[T]:
|
|
36
|
+
count = 0
|
|
37
|
+
for item in it:
|
|
38
|
+
if count >= n:
|
|
39
|
+
break
|
|
40
|
+
yield item
|
|
41
|
+
count += 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _take_async[T](n: int, it: AsyncIterable[T]) -> AsyncIterator[T]:
|
|
45
|
+
count = 0
|
|
46
|
+
async for item in it:
|
|
47
|
+
if count >= n:
|
|
48
|
+
break
|
|
49
|
+
yield item
|
|
50
|
+
count += 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def skip[T](n: int, it: Iterable[T]) -> Iterator[T]: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def skip[T](n: int, it: AsyncIterable[T]) -> AsyncIterator[T]: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def skip[T](
|
|
62
|
+
n: int, it: Iterable[T] | AsyncIterable[T]
|
|
63
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
64
|
+
"""Skip first n elements."""
|
|
65
|
+
if is_async_iterable(it):
|
|
66
|
+
return _skip_async(n, it) # type: ignore[arg-type]
|
|
67
|
+
return _skip_sync(n, it) # type: ignore[arg-type, return-value]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _skip_sync[T](n: int, it: Iterable[T]) -> Iterator[T]:
|
|
71
|
+
count = 0
|
|
72
|
+
for item in it:
|
|
73
|
+
if count < n:
|
|
74
|
+
count += 1
|
|
75
|
+
continue
|
|
76
|
+
yield item
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _skip_async[T](n: int, it: AsyncIterable[T]) -> AsyncIterator[T]:
|
|
80
|
+
count = 0
|
|
81
|
+
async for item in it:
|
|
82
|
+
if count < n:
|
|
83
|
+
count += 1
|
|
84
|
+
continue
|
|
85
|
+
yield item
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@overload
|
|
89
|
+
def take_while[T](pred: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]: ...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def take_while[T](
|
|
94
|
+
pred: Callable[[T], bool], it: AsyncIterable[T]
|
|
95
|
+
) -> AsyncIterator[T]: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def take_while[T](
|
|
99
|
+
pred: Callable[[T], bool], it: Iterable[T] | AsyncIterable[T]
|
|
100
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
101
|
+
"""Take elements while predicate is true."""
|
|
102
|
+
if is_async_iterable(it):
|
|
103
|
+
return _take_while_async(pred, it) # type: ignore[arg-type]
|
|
104
|
+
return _take_while_sync(pred, it) # type: ignore[arg-type, return-value]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _take_while_sync[T](pred: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]:
|
|
108
|
+
for item in it:
|
|
109
|
+
if not pred(item):
|
|
110
|
+
break
|
|
111
|
+
yield item
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _take_while_async[T](
|
|
115
|
+
pred: Callable[[T], bool], it: AsyncIterable[T]
|
|
116
|
+
) -> AsyncIterator[T]:
|
|
117
|
+
async for item in it:
|
|
118
|
+
if not pred(item):
|
|
119
|
+
break
|
|
120
|
+
yield item
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def skip_while[T](pred: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]: ...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
def skip_while[T](
|
|
129
|
+
pred: Callable[[T], bool], it: AsyncIterable[T]
|
|
130
|
+
) -> AsyncIterator[T]: ...
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def skip_while[T](
|
|
134
|
+
pred: Callable[[T], bool], it: Iterable[T] | AsyncIterable[T]
|
|
135
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
136
|
+
"""Skip elements while predicate is true."""
|
|
137
|
+
if is_async_iterable(it):
|
|
138
|
+
return _skip_while_async(pred, it) # type: ignore[arg-type]
|
|
139
|
+
return _skip_while_sync(pred, it) # type: ignore[arg-type, return-value]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _skip_while_sync[T](pred: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]:
|
|
143
|
+
skipping = True
|
|
144
|
+
for item in it:
|
|
145
|
+
if skipping and pred(item):
|
|
146
|
+
continue
|
|
147
|
+
skipping = False
|
|
148
|
+
yield item
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _skip_while_async[T](
|
|
152
|
+
pred: Callable[[T], bool], it: AsyncIterable[T]
|
|
153
|
+
) -> AsyncIterator[T]:
|
|
154
|
+
skipping = True
|
|
155
|
+
async for item in it:
|
|
156
|
+
if skipping and pred(item):
|
|
157
|
+
continue
|
|
158
|
+
skipping = False
|
|
159
|
+
yield item
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
def distinct[T: Hashable](it: Iterable[T]) -> Iterator[T]: ...
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@overload
|
|
167
|
+
def distinct[T: Hashable](it: AsyncIterable[T]) -> AsyncIterator[T]: ...
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def distinct[T: Hashable](
|
|
171
|
+
it: Iterable[T] | AsyncIterable[T],
|
|
172
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
173
|
+
"""Remove duplicates."""
|
|
174
|
+
if is_async_iterable(it):
|
|
175
|
+
return _distinct_async(it) # type: ignore[arg-type]
|
|
176
|
+
return _distinct_sync(it) # type: ignore[arg-type, return-value]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _distinct_sync[T: Hashable](it: Iterable[T]) -> Iterator[T]:
|
|
180
|
+
seen: set[T] = set()
|
|
181
|
+
for item in it:
|
|
182
|
+
if item not in seen:
|
|
183
|
+
seen.add(item)
|
|
184
|
+
yield item
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def _distinct_async[T: Hashable](it: AsyncIterable[T]) -> AsyncIterator[T]:
|
|
188
|
+
seen: set[T] = set()
|
|
189
|
+
async for item in it:
|
|
190
|
+
if item not in seen:
|
|
191
|
+
seen.add(item)
|
|
192
|
+
yield item
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@overload
|
|
196
|
+
def distinct_by[T, K: Hashable](
|
|
197
|
+
key_fn: Callable[[T], K], it: Iterable[T]
|
|
198
|
+
) -> Iterator[T]: ...
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@overload
|
|
202
|
+
def distinct_by[T, K: Hashable](
|
|
203
|
+
key_fn: Callable[[T], K], it: AsyncIterable[T]
|
|
204
|
+
) -> AsyncIterator[T]: ...
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def distinct_by[T, K: Hashable](
|
|
208
|
+
key_fn: Callable[[T], K], it: Iterable[T] | AsyncIterable[T]
|
|
209
|
+
) -> Iterator[T] | AsyncIterator[T]:
|
|
210
|
+
"""Remove duplicates by key function."""
|
|
211
|
+
if is_async_iterable(it):
|
|
212
|
+
return _distinct_by_async(key_fn, it) # type: ignore[arg-type]
|
|
213
|
+
return _distinct_by_sync(key_fn, it) # type: ignore[arg-type, return-value]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _distinct_by_sync[T, K: Hashable](
|
|
217
|
+
key_fn: Callable[[T], K], it: Iterable[T]
|
|
218
|
+
) -> Iterator[T]:
|
|
219
|
+
seen: set[K] = set()
|
|
220
|
+
for item in it:
|
|
221
|
+
key = key_fn(item)
|
|
222
|
+
if key not in seen:
|
|
223
|
+
seen.add(key)
|
|
224
|
+
yield item
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def _distinct_by_async[T, K: Hashable](
|
|
228
|
+
key_fn: Callable[[T], K], it: AsyncIterable[T]
|
|
229
|
+
) -> AsyncIterator[T]:
|
|
230
|
+
seen: set[K] = set()
|
|
231
|
+
async for item in it:
|
|
232
|
+
key = key_fn(item)
|
|
233
|
+
if key not in seen:
|
|
234
|
+
seen.add(key)
|
|
235
|
+
yield item
|
streamish/ops/group.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Group operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections import deque
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator, Callable, Iterable, Iterator
|
|
6
|
+
from typing import overload
|
|
7
|
+
|
|
8
|
+
from streamish._util import ensure_async_iterator, is_async_iterable
|
|
9
|
+
|
|
10
|
+
__all__ = ["batch", "window", "partition", "partition_async"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def batch[T](
|
|
15
|
+
size: int, it: Iterable[T], *, timeout: float | None = None
|
|
16
|
+
) -> Iterator[list[T]]: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def batch[T](
|
|
21
|
+
size: int, it: AsyncIterable[T], *, timeout: float | None = None
|
|
22
|
+
) -> AsyncIterator[list[T]]: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def batch[T](
|
|
26
|
+
size: int,
|
|
27
|
+
it: Iterable[T] | AsyncIterable[T],
|
|
28
|
+
*,
|
|
29
|
+
timeout: float | None = None,
|
|
30
|
+
) -> Iterator[list[T]] | AsyncIterator[list[T]]:
|
|
31
|
+
"""Group elements into batches by size or timeout."""
|
|
32
|
+
if size <= 0:
|
|
33
|
+
raise ValueError("size must be positive")
|
|
34
|
+
if is_async_iterable(it) or timeout is not None:
|
|
35
|
+
return _batch_async(size, it, timeout) # type: ignore[arg-type]
|
|
36
|
+
return _batch_sync(size, it) # type: ignore[arg-type, return-value]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _batch_sync[T](size: int, it: Iterable[T]) -> Iterator[list[T]]:
|
|
40
|
+
current: list[T] = []
|
|
41
|
+
for item in it:
|
|
42
|
+
current.append(item)
|
|
43
|
+
if len(current) >= size:
|
|
44
|
+
yield current
|
|
45
|
+
current = []
|
|
46
|
+
if current:
|
|
47
|
+
yield current
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _batch_async[T](
|
|
51
|
+
size: int,
|
|
52
|
+
it: Iterable[T] | AsyncIterable[T],
|
|
53
|
+
timeout: float | None,
|
|
54
|
+
) -> AsyncIterator[list[T]]:
|
|
55
|
+
current: list[T] = []
|
|
56
|
+
|
|
57
|
+
ait: AsyncIterator[T]
|
|
58
|
+
if is_async_iterable(it):
|
|
59
|
+
ait = it.__aiter__() # type: ignore[union-attr]
|
|
60
|
+
else:
|
|
61
|
+
sync_it: Iterable[T] = it # type: ignore[assignment]
|
|
62
|
+
ait = ensure_async_iterator(iter(sync_it))
|
|
63
|
+
|
|
64
|
+
if timeout is None:
|
|
65
|
+
async for item in ait:
|
|
66
|
+
current.append(item)
|
|
67
|
+
if len(current) >= size:
|
|
68
|
+
yield current
|
|
69
|
+
current = []
|
|
70
|
+
if current:
|
|
71
|
+
yield current
|
|
72
|
+
else:
|
|
73
|
+
# Use a task-based approach to avoid cancelling the iterator
|
|
74
|
+
async def get_next() -> T:
|
|
75
|
+
return await ait.__anext__()
|
|
76
|
+
|
|
77
|
+
pending_task: asyncio.Task[T] | None = None
|
|
78
|
+
while True:
|
|
79
|
+
try:
|
|
80
|
+
if pending_task is None:
|
|
81
|
+
pending_task = asyncio.create_task(get_next())
|
|
82
|
+
item = await asyncio.wait_for(
|
|
83
|
+
asyncio.shield(pending_task), timeout=timeout
|
|
84
|
+
)
|
|
85
|
+
pending_task = None # Task completed, clear it
|
|
86
|
+
current.append(item)
|
|
87
|
+
if len(current) >= size:
|
|
88
|
+
yield current
|
|
89
|
+
current = []
|
|
90
|
+
except TimeoutError:
|
|
91
|
+
if current:
|
|
92
|
+
yield current
|
|
93
|
+
current = []
|
|
94
|
+
# pending_task is still running, will be awaited next iteration
|
|
95
|
+
except StopAsyncIteration:
|
|
96
|
+
if current:
|
|
97
|
+
yield current
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@overload
|
|
102
|
+
def window[T](size: int, it: Iterable[T], *, step: int = 1) -> Iterator[list[T]]: ...
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@overload
|
|
106
|
+
def window[T](
|
|
107
|
+
size: int, it: AsyncIterable[T], *, step: int = 1
|
|
108
|
+
) -> AsyncIterator[list[T]]: ...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def window[T](
|
|
112
|
+
size: int, it: Iterable[T] | AsyncIterable[T], *, step: int = 1
|
|
113
|
+
) -> Iterator[list[T]] | AsyncIterator[list[T]]:
|
|
114
|
+
"""Sliding window over elements."""
|
|
115
|
+
if size <= 0:
|
|
116
|
+
raise ValueError("size must be positive")
|
|
117
|
+
if step <= 0:
|
|
118
|
+
raise ValueError("step must be positive")
|
|
119
|
+
if is_async_iterable(it):
|
|
120
|
+
return _window_async(size, it, step) # type: ignore[arg-type]
|
|
121
|
+
return _window_sync(size, it, step) # type: ignore[arg-type, return-value]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _window_sync[T](size: int, it: Iterable[T], step: int) -> Iterator[list[T]]:
|
|
125
|
+
buf: deque[T] = deque(maxlen=size)
|
|
126
|
+
skip = 0
|
|
127
|
+
for item in it:
|
|
128
|
+
if skip > 0:
|
|
129
|
+
skip -= 1
|
|
130
|
+
buf.append(item)
|
|
131
|
+
continue
|
|
132
|
+
buf.append(item)
|
|
133
|
+
if len(buf) == size:
|
|
134
|
+
yield list(buf)
|
|
135
|
+
skip = step - 1
|
|
136
|
+
for _ in range(min(step, size)):
|
|
137
|
+
if buf:
|
|
138
|
+
buf.popleft()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _window_async[T](
|
|
142
|
+
size: int, it: AsyncIterable[T], step: int
|
|
143
|
+
) -> AsyncIterator[list[T]]:
|
|
144
|
+
buf: deque[T] = deque(maxlen=size)
|
|
145
|
+
skip = 0
|
|
146
|
+
async for item in it:
|
|
147
|
+
if skip > 0:
|
|
148
|
+
skip -= 1
|
|
149
|
+
buf.append(item)
|
|
150
|
+
continue
|
|
151
|
+
buf.append(item)
|
|
152
|
+
if len(buf) == size:
|
|
153
|
+
yield list(buf)
|
|
154
|
+
skip = step - 1
|
|
155
|
+
for _ in range(min(step, size)):
|
|
156
|
+
if buf:
|
|
157
|
+
buf.popleft()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def partition[T](pred: Callable[[T], bool], it: Iterable[T]) -> tuple[list[T], list[T]]:
|
|
161
|
+
"""Split into (matches, non_matches). Terminal operation."""
|
|
162
|
+
matches: list[T] = []
|
|
163
|
+
non_matches: list[T] = []
|
|
164
|
+
for item in it:
|
|
165
|
+
if pred(item):
|
|
166
|
+
matches.append(item)
|
|
167
|
+
else:
|
|
168
|
+
non_matches.append(item)
|
|
169
|
+
return matches, non_matches
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def partition_async[T](
|
|
173
|
+
pred: Callable[[T], bool], it: AsyncIterable[T]
|
|
174
|
+
) -> tuple[list[T], list[T]]:
|
|
175
|
+
"""Split into (matches, non_matches). Terminal operation (async)."""
|
|
176
|
+
matches: list[T] = []
|
|
177
|
+
non_matches: list[T] = []
|
|
178
|
+
async for item in it:
|
|
179
|
+
if pred(item):
|
|
180
|
+
matches.append(item)
|
|
181
|
+
else:
|
|
182
|
+
non_matches.append(item)
|
|
183
|
+
return matches, non_matches
|