streamish 0.1.0__tar.gz
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-0.1.0/.github/workflows/ci.yml +43 -0
- streamish-0.1.0/.github/workflows/publish.yml +18 -0
- streamish-0.1.0/.gitignore +33 -0
- streamish-0.1.0/.pre-commit-config.yaml +23 -0
- streamish-0.1.0/.python-version +1 -0
- streamish-0.1.0/LICENSE +21 -0
- streamish-0.1.0/PKG-INFO +219 -0
- streamish-0.1.0/README.md +198 -0
- streamish-0.1.0/pyproject.toml +74 -0
- streamish-0.1.0/streamish/__init__.py +63 -0
- streamish-0.1.0/streamish/_util.py +38 -0
- streamish-0.1.0/streamish/ops/__init__.py +43 -0
- streamish-0.1.0/streamish/ops/combine.py +101 -0
- streamish-0.1.0/streamish/ops/filter.py +235 -0
- streamish-0.1.0/streamish/ops/group.py +183 -0
- streamish-0.1.0/streamish/ops/transform.py +322 -0
- streamish-0.1.0/streamish/stream.py +162 -0
- streamish-0.1.0/tests/__init__.py +1 -0
- streamish-0.1.0/tests/conftest.py +1 -0
- streamish-0.1.0/tests/test_ops_combine.py +65 -0
- streamish-0.1.0/tests/test_ops_filter.py +89 -0
- streamish-0.1.0/tests/test_ops_group.py +73 -0
- streamish-0.1.0/tests/test_ops_transform.py +144 -0
- streamish-0.1.0/tests/test_stream.py +33 -0
- streamish-0.1.0/tests/test_util.py +44 -0
- streamish-0.1.0/uv.lock +369 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
run: uv python install ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: uv run ruff check .
|
|
29
|
+
|
|
30
|
+
- name: Format check
|
|
31
|
+
run: uv run ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Type check
|
|
34
|
+
run: uv run pyright streamish/
|
|
35
|
+
|
|
36
|
+
- name: Test
|
|
37
|
+
run: uv run pytest --cov=streamish --cov-report=xml
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage
|
|
40
|
+
uses: codecov/codecov-action@v4
|
|
41
|
+
with:
|
|
42
|
+
files: coverage.xml
|
|
43
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
- uses: astral-sh/setup-uv@v4
|
|
17
|
+
- run: uv build
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
|
|
11
|
+
# IDE
|
|
12
|
+
.idea/
|
|
13
|
+
.vscode/
|
|
14
|
+
*.swp
|
|
15
|
+
|
|
16
|
+
# Testing/Coverage
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
|
|
21
|
+
# Type checkers
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.pyright/
|
|
24
|
+
|
|
25
|
+
# Environment/Secrets
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
|
|
32
|
+
# Logs
|
|
33
|
+
*.log
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.14.14
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: local
|
|
9
|
+
hooks:
|
|
10
|
+
- id: pyright
|
|
11
|
+
name: pyright
|
|
12
|
+
entry: uv run pyright
|
|
13
|
+
language: system
|
|
14
|
+
types: [python]
|
|
15
|
+
pass_filenames: false
|
|
16
|
+
args: [streamish/]
|
|
17
|
+
- id: pytest
|
|
18
|
+
name: pytest
|
|
19
|
+
entry: uv run pytest
|
|
20
|
+
language: system
|
|
21
|
+
types: [python]
|
|
22
|
+
pass_filenames: false
|
|
23
|
+
args: [-q]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
streamish-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Francisco
|
|
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.
|
streamish-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streamish
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Iterator and async iterator utilities for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/gabfssilva/streamish
|
|
6
|
+
Project-URL: Repository, https://github.com/gabfssilva/streamish
|
|
7
|
+
Author: Gabriel Francisco
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: async,functional,iterator,pipeline,stream
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# streamish
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/streamish/)
|
|
25
|
+
[](https://pypi.org/project/streamish/)
|
|
26
|
+
[](https://github.com/gabfssilva/streamish/actions)
|
|
27
|
+
[](https://opensource.org/licenses/MIT)
|
|
28
|
+
|
|
29
|
+
Iterator and async iterator utilities for Python 3.12+.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Hybrid API**: Fluent chains or standalone functions
|
|
34
|
+
- **Unified sync/async**: Same functions work with both iterators and async iterators
|
|
35
|
+
- **Type safe**: Full pyright strict mode support
|
|
36
|
+
- **Zero dependencies**: stdlib only
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install streamish
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import streamish as st
|
|
48
|
+
|
|
49
|
+
# Fluent API
|
|
50
|
+
result = list(
|
|
51
|
+
st.stream([1, 2, 3, 4, 5])
|
|
52
|
+
.map(lambda x: x * 2)
|
|
53
|
+
.filter(lambda x: x > 4)
|
|
54
|
+
.take(2)
|
|
55
|
+
)
|
|
56
|
+
# [6, 8]
|
|
57
|
+
|
|
58
|
+
# Standalone functions
|
|
59
|
+
result = list(st.take(2, st.filter(lambda x: x > 4, st.map(lambda x: x * 2, [1, 2, 3, 4, 5]))))
|
|
60
|
+
# [6, 8]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Async Support
|
|
64
|
+
|
|
65
|
+
Functions automatically detect async iterables and async functions:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import streamish as st
|
|
69
|
+
|
|
70
|
+
async def fetch(url: str) -> Response:
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
# Async source
|
|
74
|
+
async def urls():
|
|
75
|
+
yield "https://example.com/1"
|
|
76
|
+
yield "https://example.com/2"
|
|
77
|
+
|
|
78
|
+
# Automatically async
|
|
79
|
+
async for response in st.stream(urls()).map(fetch):
|
|
80
|
+
print(response)
|
|
81
|
+
|
|
82
|
+
# Concurrent execution with map_async
|
|
83
|
+
async for response in st.map_async(fetch, urls(), concurrency=10):
|
|
84
|
+
print(response)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Operations
|
|
88
|
+
|
|
89
|
+
### Transform
|
|
90
|
+
|
|
91
|
+
| Operation | Description |
|
|
92
|
+
|-----------|-------------|
|
|
93
|
+
| `map(fn, it)` | Apply function to each element |
|
|
94
|
+
| `filter(pred, it)` | Keep elements satisfying predicate |
|
|
95
|
+
| `flatten(it)` | Flatten one level of nesting |
|
|
96
|
+
| `flat_map(fn, it)` | Map then flatten |
|
|
97
|
+
| `enumerate(it, start=0)` | Add index to elements |
|
|
98
|
+
| `scan(fn, it, initial=x)` | Cumulative reduce, yielding intermediate values |
|
|
99
|
+
| `map_async(fn, it, concurrency=1)` | Concurrent async map, preserving order |
|
|
100
|
+
|
|
101
|
+
### Filter
|
|
102
|
+
|
|
103
|
+
| Operation | Description |
|
|
104
|
+
|-----------|-------------|
|
|
105
|
+
| `take(n, it)` | Take first n elements |
|
|
106
|
+
| `skip(n, it)` | Skip first n elements |
|
|
107
|
+
| `take_while(pred, it)` | Take while predicate is true |
|
|
108
|
+
| `skip_while(pred, it)` | Skip while predicate is true |
|
|
109
|
+
| `distinct(it)` | Remove duplicates |
|
|
110
|
+
| `distinct_by(key_fn, it)` | Remove duplicates by key |
|
|
111
|
+
|
|
112
|
+
### Group
|
|
113
|
+
|
|
114
|
+
| Operation | Description |
|
|
115
|
+
|-----------|-------------|
|
|
116
|
+
| `batch(size, it, timeout=None)` | Group into batches by size or timeout |
|
|
117
|
+
| `window(size, it, step=1)` | Sliding window |
|
|
118
|
+
| `partition(pred, it)` | Split into (matches, non_matches) |
|
|
119
|
+
|
|
120
|
+
### Combine
|
|
121
|
+
|
|
122
|
+
| Operation | Description |
|
|
123
|
+
|-----------|-------------|
|
|
124
|
+
| `zip(*iterables)` | Zip iterables together |
|
|
125
|
+
| `chain(*iterables)` | Chain iterables sequentially |
|
|
126
|
+
| `interleave(*iterables)` | Alternate elements round-robin |
|
|
127
|
+
| `merge(*async_iterables)` | Merge async iterables, emit as they arrive |
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
### Processing a file line by line
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import streamish as st
|
|
135
|
+
|
|
136
|
+
with open("data.txt") as f:
|
|
137
|
+
result = list(
|
|
138
|
+
st.stream(f)
|
|
139
|
+
.map(str.strip)
|
|
140
|
+
.filter(bool) # skip empty lines
|
|
141
|
+
.distinct()
|
|
142
|
+
.take(100)
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Batching API requests
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import streamish as st
|
|
150
|
+
|
|
151
|
+
async def send_batch(items: list[Item]) -> None:
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
async def process(items: AsyncIterable[Item]) -> None:
|
|
155
|
+
async for batch in st.stream(items).batch(100, timeout=5.0):
|
|
156
|
+
await send_batch(batch)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Concurrent HTTP requests
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import httpx
|
|
163
|
+
import streamish as st
|
|
164
|
+
|
|
165
|
+
async def fetch(client: httpx.AsyncClient, url: str) -> Response:
|
|
166
|
+
return await client.get(url)
|
|
167
|
+
|
|
168
|
+
async def main():
|
|
169
|
+
urls = ["https://example.com/1", "https://example.com/2", ...]
|
|
170
|
+
|
|
171
|
+
async with httpx.AsyncClient() as client:
|
|
172
|
+
async for response in st.map_async(
|
|
173
|
+
lambda url: fetch(client, url),
|
|
174
|
+
urls,
|
|
175
|
+
concurrency=10,
|
|
176
|
+
):
|
|
177
|
+
print(response.status_code)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Windowed statistics
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import streamish as st
|
|
184
|
+
|
|
185
|
+
# Moving average over last 5 values
|
|
186
|
+
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
187
|
+
|
|
188
|
+
averages = list(
|
|
189
|
+
st.stream(values)
|
|
190
|
+
.window(5)
|
|
191
|
+
.map(lambda w: sum(w) / len(w))
|
|
192
|
+
)
|
|
193
|
+
# [3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Merging async streams
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import streamish as st
|
|
200
|
+
|
|
201
|
+
async def stream_a():
|
|
202
|
+
for i in range(3):
|
|
203
|
+
await asyncio.sleep(0.1)
|
|
204
|
+
yield f"a{i}"
|
|
205
|
+
|
|
206
|
+
async def stream_b():
|
|
207
|
+
for i in range(3):
|
|
208
|
+
await asyncio.sleep(0.15)
|
|
209
|
+
yield f"b{i}"
|
|
210
|
+
|
|
211
|
+
# Items emitted as they arrive
|
|
212
|
+
async for item in st.merge(stream_a(), stream_b()):
|
|
213
|
+
print(item)
|
|
214
|
+
# a0, b0, a1, a2, b1, b2 (order depends on timing)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# streamish
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/streamish/)
|
|
4
|
+
[](https://pypi.org/project/streamish/)
|
|
5
|
+
[](https://github.com/gabfssilva/streamish/actions)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
Iterator and async iterator utilities for Python 3.12+.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Hybrid API**: Fluent chains or standalone functions
|
|
13
|
+
- **Unified sync/async**: Same functions work with both iterators and async iterators
|
|
14
|
+
- **Type safe**: Full pyright strict mode support
|
|
15
|
+
- **Zero dependencies**: stdlib only
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install streamish
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import streamish as st
|
|
27
|
+
|
|
28
|
+
# Fluent API
|
|
29
|
+
result = list(
|
|
30
|
+
st.stream([1, 2, 3, 4, 5])
|
|
31
|
+
.map(lambda x: x * 2)
|
|
32
|
+
.filter(lambda x: x > 4)
|
|
33
|
+
.take(2)
|
|
34
|
+
)
|
|
35
|
+
# [6, 8]
|
|
36
|
+
|
|
37
|
+
# Standalone functions
|
|
38
|
+
result = list(st.take(2, st.filter(lambda x: x > 4, st.map(lambda x: x * 2, [1, 2, 3, 4, 5]))))
|
|
39
|
+
# [6, 8]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Async Support
|
|
43
|
+
|
|
44
|
+
Functions automatically detect async iterables and async functions:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import streamish as st
|
|
48
|
+
|
|
49
|
+
async def fetch(url: str) -> Response:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
# Async source
|
|
53
|
+
async def urls():
|
|
54
|
+
yield "https://example.com/1"
|
|
55
|
+
yield "https://example.com/2"
|
|
56
|
+
|
|
57
|
+
# Automatically async
|
|
58
|
+
async for response in st.stream(urls()).map(fetch):
|
|
59
|
+
print(response)
|
|
60
|
+
|
|
61
|
+
# Concurrent execution with map_async
|
|
62
|
+
async for response in st.map_async(fetch, urls(), concurrency=10):
|
|
63
|
+
print(response)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Operations
|
|
67
|
+
|
|
68
|
+
### Transform
|
|
69
|
+
|
|
70
|
+
| Operation | Description |
|
|
71
|
+
|-----------|-------------|
|
|
72
|
+
| `map(fn, it)` | Apply function to each element |
|
|
73
|
+
| `filter(pred, it)` | Keep elements satisfying predicate |
|
|
74
|
+
| `flatten(it)` | Flatten one level of nesting |
|
|
75
|
+
| `flat_map(fn, it)` | Map then flatten |
|
|
76
|
+
| `enumerate(it, start=0)` | Add index to elements |
|
|
77
|
+
| `scan(fn, it, initial=x)` | Cumulative reduce, yielding intermediate values |
|
|
78
|
+
| `map_async(fn, it, concurrency=1)` | Concurrent async map, preserving order |
|
|
79
|
+
|
|
80
|
+
### Filter
|
|
81
|
+
|
|
82
|
+
| Operation | Description |
|
|
83
|
+
|-----------|-------------|
|
|
84
|
+
| `take(n, it)` | Take first n elements |
|
|
85
|
+
| `skip(n, it)` | Skip first n elements |
|
|
86
|
+
| `take_while(pred, it)` | Take while predicate is true |
|
|
87
|
+
| `skip_while(pred, it)` | Skip while predicate is true |
|
|
88
|
+
| `distinct(it)` | Remove duplicates |
|
|
89
|
+
| `distinct_by(key_fn, it)` | Remove duplicates by key |
|
|
90
|
+
|
|
91
|
+
### Group
|
|
92
|
+
|
|
93
|
+
| Operation | Description |
|
|
94
|
+
|-----------|-------------|
|
|
95
|
+
| `batch(size, it, timeout=None)` | Group into batches by size or timeout |
|
|
96
|
+
| `window(size, it, step=1)` | Sliding window |
|
|
97
|
+
| `partition(pred, it)` | Split into (matches, non_matches) |
|
|
98
|
+
|
|
99
|
+
### Combine
|
|
100
|
+
|
|
101
|
+
| Operation | Description |
|
|
102
|
+
|-----------|-------------|
|
|
103
|
+
| `zip(*iterables)` | Zip iterables together |
|
|
104
|
+
| `chain(*iterables)` | Chain iterables sequentially |
|
|
105
|
+
| `interleave(*iterables)` | Alternate elements round-robin |
|
|
106
|
+
| `merge(*async_iterables)` | Merge async iterables, emit as they arrive |
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
### Processing a file line by line
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import streamish as st
|
|
114
|
+
|
|
115
|
+
with open("data.txt") as f:
|
|
116
|
+
result = list(
|
|
117
|
+
st.stream(f)
|
|
118
|
+
.map(str.strip)
|
|
119
|
+
.filter(bool) # skip empty lines
|
|
120
|
+
.distinct()
|
|
121
|
+
.take(100)
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Batching API requests
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import streamish as st
|
|
129
|
+
|
|
130
|
+
async def send_batch(items: list[Item]) -> None:
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
async def process(items: AsyncIterable[Item]) -> None:
|
|
134
|
+
async for batch in st.stream(items).batch(100, timeout=5.0):
|
|
135
|
+
await send_batch(batch)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Concurrent HTTP requests
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import httpx
|
|
142
|
+
import streamish as st
|
|
143
|
+
|
|
144
|
+
async def fetch(client: httpx.AsyncClient, url: str) -> Response:
|
|
145
|
+
return await client.get(url)
|
|
146
|
+
|
|
147
|
+
async def main():
|
|
148
|
+
urls = ["https://example.com/1", "https://example.com/2", ...]
|
|
149
|
+
|
|
150
|
+
async with httpx.AsyncClient() as client:
|
|
151
|
+
async for response in st.map_async(
|
|
152
|
+
lambda url: fetch(client, url),
|
|
153
|
+
urls,
|
|
154
|
+
concurrency=10,
|
|
155
|
+
):
|
|
156
|
+
print(response.status_code)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Windowed statistics
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import streamish as st
|
|
163
|
+
|
|
164
|
+
# Moving average over last 5 values
|
|
165
|
+
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
166
|
+
|
|
167
|
+
averages = list(
|
|
168
|
+
st.stream(values)
|
|
169
|
+
.window(5)
|
|
170
|
+
.map(lambda w: sum(w) / len(w))
|
|
171
|
+
)
|
|
172
|
+
# [3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Merging async streams
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import streamish as st
|
|
179
|
+
|
|
180
|
+
async def stream_a():
|
|
181
|
+
for i in range(3):
|
|
182
|
+
await asyncio.sleep(0.1)
|
|
183
|
+
yield f"a{i}"
|
|
184
|
+
|
|
185
|
+
async def stream_b():
|
|
186
|
+
for i in range(3):
|
|
187
|
+
await asyncio.sleep(0.15)
|
|
188
|
+
yield f"b{i}"
|
|
189
|
+
|
|
190
|
+
# Items emitted as they arrive
|
|
191
|
+
async for item in st.merge(stream_a(), stream_b()):
|
|
192
|
+
print(item)
|
|
193
|
+
# a0, b0, a1, a2, b1, b2 (order depends on timing)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "streamish"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Iterator and async iterator utilities for Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [{ name = "Gabriel Francisco" }]
|
|
9
|
+
keywords = ["iterator", "async", "stream", "functional", "pipeline"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/gabfssilva/streamish"
|
|
24
|
+
Repository = "https://github.com/gabfssilva/streamish"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
33
|
+
|
|
34
|
+
[tool.pyright]
|
|
35
|
+
pythonVersion = "3.12"
|
|
36
|
+
typeCheckingMode = "strict"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 88
|
|
40
|
+
target-version = "py312"
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["ALL"]
|
|
44
|
+
ignore = [
|
|
45
|
+
"D", # docstrings (optional for library)
|
|
46
|
+
"COM812", # trailing comma (conflicts with formatter)
|
|
47
|
+
"ISC001", # implicit string concat (conflicts with formatter)
|
|
48
|
+
"A004", # shadowing builtins is intentional (map, filter, enumerate)
|
|
49
|
+
"EM101", # string literals in exceptions ok for simple errors
|
|
50
|
+
"TRY003", # long exception messages ok
|
|
51
|
+
"TRY300", # return in try ok for simple cases
|
|
52
|
+
"RUF022", # __all__ sorting not required
|
|
53
|
+
"SLOT000", # __slots__ false positive on generic class
|
|
54
|
+
"E501", # line length handled by formatter
|
|
55
|
+
"C901", # complexity ok for async streaming functions
|
|
56
|
+
"PLR0912", # branches ok for async streaming functions
|
|
57
|
+
"SIM113", # enumerate suggestion not always better
|
|
58
|
+
"PERF401", # list comprehension not possible with await
|
|
59
|
+
"UP028", # yield from not possible when retuning tuple
|
|
60
|
+
"ASYNC109", # timeout parameter naming is intentional
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
"tests/*" = ["S101", "PLR2004"] # assert, magic values ok in tests
|
|
65
|
+
|
|
66
|
+
[dependency-groups]
|
|
67
|
+
dev = [
|
|
68
|
+
"pytest>=8.0",
|
|
69
|
+
"pytest-asyncio>=0.24",
|
|
70
|
+
"pytest-cov>=4.0",
|
|
71
|
+
"pyright>=1.1",
|
|
72
|
+
"ruff>=0.14",
|
|
73
|
+
"pre-commit>=4.5.1",
|
|
74
|
+
]
|
|
@@ -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)
|