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.
@@ -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
@@ -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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/streamish)](https://pypi.org/project/streamish/)
25
+ [![Python](https://img.shields.io/pypi/pyversions/streamish)](https://pypi.org/project/streamish/)
26
+ [![CI](https://github.com/gabfssilva/streamish/actions/workflows/ci.yml/badge.svg)](https://github.com/gabfssilva/streamish/actions)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/streamish)](https://pypi.org/project/streamish/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/streamish)](https://pypi.org/project/streamish/)
5
+ [![CI](https://github.com/gabfssilva/streamish/actions/workflows/ci.yml/badge.svg)](https://github.com/gabfssilva/streamish/actions)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)