pystreamlet 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.
- pystreamlet-0.1.0.dist-info/METADATA +387 -0
- pystreamlet-0.1.0.dist-info/RECORD +10 -0
- pystreamlet-0.1.0.dist-info/WHEEL +4 -0
- pystreamlet-0.1.0.dist-info/entry_points.txt +3 -0
- pystreamlet-0.1.0.dist-info/licenses/LICENSE +21 -0
- streamlet/__init__.py +30 -0
- streamlet/async_stream.py +514 -0
- streamlet/file_stream.py +177 -0
- streamlet/py.typed +0 -0
- streamlet/stream.py +446 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pystreamlet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fluent, lazy stream-processing library for Python, modeled after Java's Streams API
|
|
5
|
+
Keywords: stream,streams,lazy,itertools,functional,pipeline,async
|
|
6
|
+
Author: 0xthreadsafe
|
|
7
|
+
Author-email: 0xthreadsafe <a.ghasabeh@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Project-URL: Homepage, https://github.com/0xthreadsafe/streamlet
|
|
22
|
+
Project-URL: Repository, https://github.com/0xthreadsafe/streamlet
|
|
23
|
+
Project-URL: Changelog, https://github.com/0xthreadsafe/streamlet/blob/main/CHANGELOG.md
|
|
24
|
+
Project-URL: Issues, https://github.com/0xthreadsafe/streamlet/issues
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Streamlet
|
|
28
|
+
|
|
29
|
+
A fluent, **lazy** stream-processing library for Python, modeled after Java's Streams API.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from streamlet import Stream
|
|
33
|
+
|
|
34
|
+
(
|
|
35
|
+
Stream.from_iterable(range(100))
|
|
36
|
+
.filter(lambda n: n % 2 == 0)
|
|
37
|
+
.map(lambda n: n**2)
|
|
38
|
+
.take(5)
|
|
39
|
+
.to_list()
|
|
40
|
+
)
|
|
41
|
+
# [0, 4, 16, 36, 64]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The goal is to make `itertools`' power reachable through clean, chainable syntax.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pystreamlet # or: uv add pystreamlet
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You install `pystreamlet` but import `streamlet`, as above — PyPI reserves names confusable with
|
|
51
|
+
`streamlit`. Requires **Python 3.11+**, has no dependencies, and ships type information
|
|
52
|
+
(`py.typed`).
|
|
53
|
+
|
|
54
|
+
> **Status: 0.1.0, awaiting its first upload.** Everything below works and is tested; until the
|
|
55
|
+
> release lands on PyPI, install from source (see [Development](#development)). Before 1.0.0 the
|
|
56
|
+
> API may still change — see [versioning](#versioning).
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
`itertools` is powerful but reads inside out — you write the last step first:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# itertools: read from the middle outwards
|
|
64
|
+
from itertools import count, islice
|
|
65
|
+
|
|
66
|
+
list(islice((n**2 for n in count() if n % 2 == 0), 5))
|
|
67
|
+
|
|
68
|
+
# streamlet: read left to right, in the order it happens
|
|
69
|
+
(
|
|
70
|
+
Stream.iterate(0, lambda n: n + 1)
|
|
71
|
+
.filter(lambda n: n % 2 == 0)
|
|
72
|
+
.map(lambda n: n**2)
|
|
73
|
+
.take(5)
|
|
74
|
+
.to_list()
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Both are lazy and both handle infinite sources. The difference is that one describes the
|
|
79
|
+
pipeline in the order it runs.
|
|
80
|
+
|
|
81
|
+
### Side by side
|
|
82
|
+
|
|
83
|
+
**Take the first few results of an expensive step.** `islice` wraps what it slices, so the
|
|
84
|
+
bound you care about ends up furthest from the work it bounds:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from itertools import islice
|
|
88
|
+
|
|
89
|
+
list(islice(map(fetch, filter(is_recent, urls)), 5))
|
|
90
|
+
|
|
91
|
+
Stream(urls).filter(is_recent).map(fetch).take(5).to_list()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Group items by a key.** `itertools.groupby` only groups *adjacent* items, so it needs a sort
|
|
95
|
+
first and hands back iterators that expire as you advance:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from itertools import groupby
|
|
99
|
+
from operator import attrgetter
|
|
100
|
+
|
|
101
|
+
by_team = {
|
|
102
|
+
team: list(members)
|
|
103
|
+
for team, members in groupby(sorted(users, key=attrgetter("team")), key=attrgetter("team"))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
by_team = Stream(users).group_by(attrgetter("team"))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Drop repeats but keep order.** The `set` version loses order, and the `dict.fromkeys` trick
|
|
110
|
+
reads as a puzzle:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
list(dict.fromkeys(names))
|
|
114
|
+
|
|
115
|
+
Stream(names).distinct().to_list()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Chain a few steps over a file.** The itertools version needs a `with` block, a generator
|
|
119
|
+
expression and a slice, in three different directions:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
with open("app.log", encoding="utf-8") as handle:
|
|
123
|
+
first_errors = list(islice((line for line in handle if line.startswith("ERROR")), 10))
|
|
124
|
+
|
|
125
|
+
with Stream.from_file("app.log") as lines:
|
|
126
|
+
first_errors = lines.filter(lambda line: line.startswith("ERROR")).take(10).to_list()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Stop as soon as one item matches.** `next()` with a default over a generator expression says
|
|
130
|
+
the same thing, more quietly:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
next((user for user in users if user.is_admin), None)
|
|
134
|
+
|
|
135
|
+
Stream(users).filter(lambda user: user.is_admin).first()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
None of these are faster than the `itertools` version — they compile to the same generators.
|
|
139
|
+
They are shorter to read and, more usefully, they read in the order the work happens.
|
|
140
|
+
|
|
141
|
+
## Laziness
|
|
142
|
+
|
|
143
|
+
Nothing executes until a terminal op runs. Intermediate ops just build a pipeline:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
stream = Stream.from_iterable(count()).map(expensive) # expensive() has not been called
|
|
147
|
+
stream.take(3).to_list() # now it runs -- exactly 3 times
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This is what makes infinite sources safe:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
Stream.iterate(1, lambda n: n * 2).take_while(lambda n: n < 100).to_list()
|
|
154
|
+
# [1, 2, 4, 8, 16, 32, 64]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Single use
|
|
158
|
+
|
|
159
|
+
A stream is consumed once, like the iterator underneath it. Reusing one raises rather than
|
|
160
|
+
silently yielding nothing:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
stream = Stream.of(1, 2, 3)
|
|
164
|
+
stream.to_list() # [1, 2, 3]
|
|
165
|
+
stream.to_list() # StreamConsumedError
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## API
|
|
169
|
+
|
|
170
|
+
### Creating a stream
|
|
171
|
+
|
|
172
|
+
| | |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `Stream(iterable)` | wrap any iterable |
|
|
175
|
+
| `Stream.of(*items)` | from individual arguments |
|
|
176
|
+
| `Stream.from_iterable(iterable)` | from an existing iterable |
|
|
177
|
+
| `Stream.empty()` | no items |
|
|
178
|
+
| `Stream.iterate(seed, fn)` | infinite: `seed`, `fn(seed)`, `fn(fn(seed))`, … |
|
|
179
|
+
| `Stream.generate(fn)` | infinite: repeated calls to `fn()` |
|
|
180
|
+
| `Stream.concat(*iterables)` | join sources end to end |
|
|
181
|
+
| `Stream.from_file(path)` | lines of a file, handle managed |
|
|
182
|
+
| `Stream.from_handle(handle, close=True)` | a handle you already have, ownership stated |
|
|
183
|
+
|
|
184
|
+
### Intermediate ops — lazy, return a new `Stream`
|
|
185
|
+
|
|
186
|
+
| | |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `.map(fn)` | apply `fn` to every item |
|
|
189
|
+
| `.filter(predicate)` | keep matching items |
|
|
190
|
+
| `.flat_map(fn)` | map to iterables and flatten one level |
|
|
191
|
+
| `.distinct()` | drop repeats, keeping first-seen order |
|
|
192
|
+
| `.take(n)` / `.skip(n)` | first `n` / everything after `n` |
|
|
193
|
+
| `.take_while(p)` / `.drop_while(p)` | stop at, or skip until, the first failure |
|
|
194
|
+
| `.peek(action)` | run a side effect per item, yielding it unchanged |
|
|
195
|
+
| `.sorted(key=None, reverse=False)` | sort — **buffers the whole stream** |
|
|
196
|
+
| `.reverse()` | reverse order — **buffers the whole stream** |
|
|
197
|
+
|
|
198
|
+
### Terminal ops — consume the stream, return a value
|
|
199
|
+
|
|
200
|
+
| | |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `.to_list()` `.to_tuple()` `.to_set()` | materialise |
|
|
203
|
+
| `.to_dict(key, value=None)` | collect into a dict (last key wins) |
|
|
204
|
+
| `.join(separator="")` | concatenate a `Stream[str]` |
|
|
205
|
+
| `.group_by(key)` | `dict` of key → members |
|
|
206
|
+
| `.reduce(fn, initial)` | fold into a single value |
|
|
207
|
+
| `.sum()` `.count()` `.min(key=None)` `.max(key=None)` | aggregate |
|
|
208
|
+
| `.first()` | first item, or `None` |
|
|
209
|
+
| `.any(p)` `.all(p)` `.none(p)` | matching — all short-circuit |
|
|
210
|
+
| `.for_each(action)` | run an action per item |
|
|
211
|
+
|
|
212
|
+
### Pipe operator
|
|
213
|
+
|
|
214
|
+
Any function taking a `Stream` can act as a stage, so you can extend the library without
|
|
215
|
+
subclassing:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
def errors_only(stream):
|
|
219
|
+
return stream.filter(lambda line: line.startswith("ERROR"))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
Stream(lines) | errors_only | (lambda s: s.take(10)) | list
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Files
|
|
226
|
+
|
|
227
|
+
`Stream.from_file` streams a file's lines lazily and owns the handle:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
with Stream.from_file("app.log") as lines:
|
|
231
|
+
errors = lines.map(str.rstrip).filter(lambda line: line.startswith("ERROR")).to_list()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The file is closed on every exit path: exhausting the stream, stopping early with a `break` or
|
|
235
|
+
a partial read, an exception escaping iteration, or leaving the `with` block. `with` remains the
|
|
236
|
+
clearest way to express the intent, and it closes the file even if you never iterate at all.
|
|
237
|
+
|
|
238
|
+
Lines keep their trailing newline, matching `open()`. Pass `encoding`, `errors` or `newline`
|
|
239
|
+
through as needed.
|
|
240
|
+
|
|
241
|
+
### Handles you already have
|
|
242
|
+
|
|
243
|
+
`Stream(handle)` leaves the handle to whoever opened it — the right default, but it leaks if
|
|
244
|
+
nobody follows up. `Stream.from_handle` makes the hand-off explicit:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
Stream.from_handle(sock.makefile()) # the stream closes it
|
|
248
|
+
Stream.from_handle(sys.stdin, close=False) # you keep it
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
With `close=True` (the default) the resource is closed on the same paths `from_file` covers;
|
|
252
|
+
with `close=False` nothing is closed for you. It works for anything iterable with a `close()` —
|
|
253
|
+
socket files, `os.popen` pipes, `io.StringIO`, database cursors — and raises `TypeError` for an
|
|
254
|
+
iterable that has none. Streamlet never auto-detects handles in `Stream(...)`: silently closing
|
|
255
|
+
something you opened would break the whoever-opens-closes convention.
|
|
256
|
+
|
|
257
|
+
## Async
|
|
258
|
+
|
|
259
|
+
`AsyncStream[T]` is the same API over an async source, driven with `async for` and `await`:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
import asyncio
|
|
263
|
+
|
|
264
|
+
from streamlet import AsyncStream
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def fetch(url: str) -> str: ...
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def main() -> None:
|
|
271
|
+
titles = await (
|
|
272
|
+
AsyncStream.from_iterable(urls)
|
|
273
|
+
.filter(lambda u: u.startswith("https://"))
|
|
274
|
+
.map(fetch)
|
|
275
|
+
.take(10)
|
|
276
|
+
.to_list()
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Every op that takes a function accepts **either a plain function or a coroutine function**, so
|
|
281
|
+
`.map(str)` and `.map(fetch)` both work and both type correctly.
|
|
282
|
+
|
|
283
|
+
### Concurrency
|
|
284
|
+
|
|
285
|
+
`.map(fetch)` awaits one item at a time. `.map_concurrent(fetch)` runs several at once:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
results = await AsyncStream.from_iterable(urls).map_concurrent(fetch, limit=8).to_list()
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
At most `limit` calls are in flight, and items are pulled from the source only as slots free up —
|
|
292
|
+
so it stays lazy and works on infinite sources. Results come back in source order by default;
|
|
293
|
+
pass `ordered=False` to get each one as soon as it is ready.
|
|
294
|
+
|
|
295
|
+
If the mapper raises, the original exception propagates (not an `ExceptionGroup`) and every
|
|
296
|
+
in-flight call is cancelled. Abandoning the stream early — a `take`, a `break` — cancels them too.
|
|
297
|
+
|
|
298
|
+
A runnable fan-out demo lives in [`examples/concurrent_fan_out.py`](examples/concurrent_fan_out.py):
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
uv run python examples/concurrent_fan_out.py
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
sequential : 24 requests in 1.95s (1 at a time)
|
|
306
|
+
concurrent : 24 requests in 0.24s (up to 8 at once) -- 8.0x faster
|
|
307
|
+
first infra user: user-02 -- fetched 8/24 users, not all
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Sources can be sync or async: `AsyncStream.of(...)`, `.from_iterable(list)`,
|
|
311
|
+
`.from_async_iterable(agen)`, `.empty()`, `.iterate(seed, fn)`, `.generate(fn)`, and
|
|
312
|
+
`.concat(...)` — which mixes both kinds.
|
|
313
|
+
|
|
314
|
+
Terminal ops are coroutines, so they need `await`. Everything else matches `Stream`: the same
|
|
315
|
+
intermediate ops, the same single-use semantics, the same laziness.
|
|
316
|
+
|
|
317
|
+
### Cleanup
|
|
318
|
+
|
|
319
|
+
Each stage closes the one behind it, so a pipeline that stops early finalises its source right
|
|
320
|
+
away — a bounded `take`, a short-circuiting `first`/`any`/`all`, or an exception all unwind the
|
|
321
|
+
whole chain. That matters when the source holds something: a connection, a cursor, a handle.
|
|
322
|
+
|
|
323
|
+
One case Python gives no hook for is a bare `break` out of an `async for`, which leaves the
|
|
324
|
+
outermost stage suspended. Wrap the iterator when that matters:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from contextlib import aclosing
|
|
328
|
+
|
|
329
|
+
async with aclosing(stream.map(fetch).__aiter__()) as items:
|
|
330
|
+
async for item in items:
|
|
331
|
+
if done(item):
|
|
332
|
+
break
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Typing
|
|
336
|
+
|
|
337
|
+
`Stream[T]` is generic and ships a `py.typed` marker, so element types flow through a chain:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
Stream.of(1, 2, 3) # Stream[int]
|
|
341
|
+
.map(str) # Stream[str]
|
|
342
|
+
.to_list() # list[str]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Some ops are restricted by self-type and fail at type-check time, not runtime:
|
|
346
|
+
|
|
347
|
+
- `.sum()` only on numeric streams
|
|
348
|
+
- `.join()` only on `Stream[str]`
|
|
349
|
+
- `.to_set()`, `.distinct()`, `.group_by()` require hashable items
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
uv sync # install project + dev tools (needs Python 3.11+)
|
|
355
|
+
uv run pytest # tests
|
|
356
|
+
uv run ruff check . # lint
|
|
357
|
+
uv run mypy # type check (strict)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Run everything the way CI does:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Versioning
|
|
367
|
+
|
|
368
|
+
Streamlet follows [semantic versioning](https://semver.org/spec/v2.0.0.html). Before 1.0.0 a
|
|
369
|
+
breaking change bumps the minor version and everything else bumps the patch version. Changes
|
|
370
|
+
are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
371
|
+
|
|
372
|
+
Releases are cut from a tag: pushing `vX.Y.Z` builds the artifacts and checks the tag against the
|
|
373
|
+
version in `pyproject.toml`. Publishing the GitHub release for that tag uploads them to PyPI
|
|
374
|
+
through trusted publishing, so no token lives in this repository.
|
|
375
|
+
|
|
376
|
+
## Roadmap
|
|
377
|
+
|
|
378
|
+
- [x] Core `Stream[T]`, intermediate + terminal ops, laziness tests
|
|
379
|
+
- [x] `__or__` pipe chaining, constructors, `group_by`, edge cases
|
|
380
|
+
- [x] `Stream.from_file` — context-managed resource streams
|
|
381
|
+
- [x] `AsyncStream` — bounded concurrent `map`
|
|
382
|
+
- [x] Docstrings, README examples
|
|
383
|
+
- [ ] PyPI release
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
streamlet/__init__.py,sha256=nWV1AIAUuNb3pdALPQu3vMje44_-Q7pdnzEEFNB4WzQ,901
|
|
2
|
+
streamlet/async_stream.py,sha256=-4U0QGp-196F0KkRmrK0axLMS27rSDyX2R6PD0bVlCo,18566
|
|
3
|
+
streamlet/file_stream.py,sha256=KtkFH0dpvAmqXkOoMKWSnGrxCIUhsN19N6wdGM0fujQ,5532
|
|
4
|
+
streamlet/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
streamlet/stream.py,sha256=VAYQ3Vm4Jh7Yrf2VR0GE6xClFhMSH-d64zYk5dEUQuQ,15645
|
|
6
|
+
pystreamlet-0.1.0.dist-info/licenses/LICENSE,sha256=IZW3gS-eGHikKwMWk0GN7Z2HwPh8DPam9vxb1L7SHQE,1069
|
|
7
|
+
pystreamlet-0.1.0.dist-info/WHEEL,sha256=R1d3uUTbmXM1FHXH_itQashbrqrOSVj-hvBCpmkIIGE,81
|
|
8
|
+
pystreamlet-0.1.0.dist-info/entry_points.txt,sha256=cuCNK35Vx6qfGKF6eI7jZRGyISmDsIfHj_ozom1pt_A,46
|
|
9
|
+
pystreamlet-0.1.0.dist-info/METADATA,sha256=ngLgmgtstw8CcwpGnm5ka07gCVnxnjRt9XhSPFUHBug,12905
|
|
10
|
+
pystreamlet-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 0xthreadsafe
|
|
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.
|
streamlet/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Streamlet — a fluent, lazy stream-processing library for Python.
|
|
2
|
+
|
|
3
|
+
Start from :class:`~streamlet.stream.Stream` for synchronous pipelines and
|
|
4
|
+
:class:`~streamlet.async_stream.AsyncStream` for async ones::
|
|
5
|
+
|
|
6
|
+
from streamlet import Stream
|
|
7
|
+
|
|
8
|
+
Stream.from_iterable(range(100)).filter(lambda n: n % 2 == 0).take(5).to_list()
|
|
9
|
+
|
|
10
|
+
:class:`~streamlet.file_stream.ResourceStream` (and its ``FileStream``
|
|
11
|
+
specialisation) covers sources that hold a resource open.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from streamlet.async_stream import AsyncStream
|
|
15
|
+
from streamlet.file_stream import ClosableIterable, FileStream, ResourceStream
|
|
16
|
+
from streamlet.stream import Stream, StreamConsumedError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AsyncStream",
|
|
20
|
+
"ClosableIterable",
|
|
21
|
+
"FileStream",
|
|
22
|
+
"ResourceStream",
|
|
23
|
+
"Stream",
|
|
24
|
+
"StreamConsumedError",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
"""Entry point for the ``streamlet`` console script."""
|
|
30
|
+
print("Hello from streamlet!")
|