pyrill 0.2.0b0__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,95 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyrill
3
+ Version: 0.2.0b0
4
+ Summary: Asynchronous streams for Python
5
+ Keywords: stream,flow,dataflow,reactive
6
+ Author: Alfred
7
+ Author-email: Alfred <alfred82santa@gmail.com>
8
+ License: LGPL-2.1-or-later
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.14
14
+ Project-URL: Repository, https://github.com/pyrill-framework/pyrill
15
+ Project-URL: Homepage, https://github.com/pyrill-framework/pyrill
16
+ Project-URL: Documentation, https://pyrill-framework.readthedocs.io/stable/
17
+ Project-URL: Issues, https://github.com/pyrill-framework/pyrill/issues
18
+ Description-Content-Type: text/markdown
19
+
20
+ # PyRill
21
+
22
+ Asynchronous streams for Python.
23
+
24
+ PyRill builds asynchronous data pipelines in plain Python. You connect small
25
+ elements (sources, stages and sinks) with `>>`, and the data flows through them one
26
+ frame at a time, on top of `asyncio` and with no dependencies.
27
+
28
+ ```python
29
+ import asyncio
30
+
31
+ from pyrill.accumulators import ListAcc
32
+ from pyrill.mappers import Map
33
+ from pyrill.sinks import Last
34
+ from pyrill.sources import IterableSource
35
+
36
+
37
+ async def main() -> None:
38
+ pipeline = (
39
+ IterableSource(source=["alpha", "beta", "gamma"])
40
+ >> Map(map_func=str.upper)
41
+ >> ListAcc()
42
+ >> Last()
43
+ )
44
+ print(await pipeline.get_frame())
45
+
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ ```text
51
+ ['ALPHA', 'BETA', 'GAMMA']
52
+ ```
53
+
54
+ ## Why PyRill
55
+
56
+ Programs that handle streams (files, sockets, API responses, queues) tend to grow
57
+ hand-written loops that mix reading, parsing, filtering, timing and error handling.
58
+ PyRill turns each of those concerns into a small, typed, reusable element that you
59
+ can test alone and connect with the others.
60
+
61
+ - **Pull-based.** Work happens only when a frame is needed, so a slow consumer slows
62
+ the whole pipeline, and frames do not pile up in memory unless you add a buffer.
63
+ - **Streaming.** A pipeline works on one frame at a time, so memory use depends on
64
+ the size of the frames, not on the length of the stream.
65
+ - **Batteries included.** Elements for files and asyncio streams, byte and text
66
+ re-chunking, safe UTF-8 decoding, CSV, JSON, branching (`Tee`), pairing (`Zip`),
67
+ merging (`Union`), timeouts and deadlines, pausing (`Gate`), buffering (`Queue`)
68
+ and more.
69
+ - **Extensible.** Write your own sources, stages and sinks, or package a chain of
70
+ stages as a single reusable stage.
71
+ - **Typed.** Elements are generic classes, so type checkers follow the frames through
72
+ a pipeline.
73
+
74
+ ## Installation
75
+
76
+ PyRill needs Python 3.14 or later and has no other dependencies.
77
+
78
+ ```console
79
+ $ pip install pyrill
80
+ ```
81
+
82
+ To work on PyRill itself, clone the repository, run `uv sync`, and run the tests with
83
+ `uv run pytest`.
84
+
85
+ ## Documentation
86
+
87
+ Full documentation, including a quickstart, the core concepts, task-oriented guides
88
+ and a catalog of every element, is at
89
+ <https://pyrill-framework.readthedocs.io/stable/>. The same pages are in the
90
+ [`docs/`](docs/source) folder.
91
+
92
+ ## Project
93
+
94
+ - Repository and issue tracker: <https://github.com/pyrill-framework/pyrill>
95
+ - License: LGPL-2.1-or-later
@@ -0,0 +1,76 @@
1
+ # PyRill
2
+
3
+ Asynchronous streams for Python.
4
+
5
+ PyRill builds asynchronous data pipelines in plain Python. You connect small
6
+ elements (sources, stages and sinks) with `>>`, and the data flows through them one
7
+ frame at a time, on top of `asyncio` and with no dependencies.
8
+
9
+ ```python
10
+ import asyncio
11
+
12
+ from pyrill.accumulators import ListAcc
13
+ from pyrill.mappers import Map
14
+ from pyrill.sinks import Last
15
+ from pyrill.sources import IterableSource
16
+
17
+
18
+ async def main() -> None:
19
+ pipeline = (
20
+ IterableSource(source=["alpha", "beta", "gamma"])
21
+ >> Map(map_func=str.upper)
22
+ >> ListAcc()
23
+ >> Last()
24
+ )
25
+ print(await pipeline.get_frame())
26
+
27
+
28
+ asyncio.run(main())
29
+ ```
30
+
31
+ ```text
32
+ ['ALPHA', 'BETA', 'GAMMA']
33
+ ```
34
+
35
+ ## Why PyRill
36
+
37
+ Programs that handle streams (files, sockets, API responses, queues) tend to grow
38
+ hand-written loops that mix reading, parsing, filtering, timing and error handling.
39
+ PyRill turns each of those concerns into a small, typed, reusable element that you
40
+ can test alone and connect with the others.
41
+
42
+ - **Pull-based.** Work happens only when a frame is needed, so a slow consumer slows
43
+ the whole pipeline, and frames do not pile up in memory unless you add a buffer.
44
+ - **Streaming.** A pipeline works on one frame at a time, so memory use depends on
45
+ the size of the frames, not on the length of the stream.
46
+ - **Batteries included.** Elements for files and asyncio streams, byte and text
47
+ re-chunking, safe UTF-8 decoding, CSV, JSON, branching (`Tee`), pairing (`Zip`),
48
+ merging (`Union`), timeouts and deadlines, pausing (`Gate`), buffering (`Queue`)
49
+ and more.
50
+ - **Extensible.** Write your own sources, stages and sinks, or package a chain of
51
+ stages as a single reusable stage.
52
+ - **Typed.** Elements are generic classes, so type checkers follow the frames through
53
+ a pipeline.
54
+
55
+ ## Installation
56
+
57
+ PyRill needs Python 3.14 or later and has no other dependencies.
58
+
59
+ ```console
60
+ $ pip install pyrill
61
+ ```
62
+
63
+ To work on PyRill itself, clone the repository, run `uv sync`, and run the tests with
64
+ `uv run pytest`.
65
+
66
+ ## Documentation
67
+
68
+ Full documentation, including a quickstart, the core concepts, task-oriented guides
69
+ and a catalog of every element, is at
70
+ <https://pyrill-framework.readthedocs.io/stable/>. The same pages are in the
71
+ [`docs/`](docs/source) folder.
72
+
73
+ ## Project
74
+
75
+ - Repository and issue tracker: <https://github.com/pyrill-framework/pyrill>
76
+ - License: LGPL-2.1-or-later
@@ -0,0 +1,142 @@
1
+ [project]
2
+ name = "pyrill"
3
+ version = "0.2.0b0"
4
+ description = "Asynchronous streams for Python"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = []
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.14",
12
+ "Typing :: Typed",
13
+ ]
14
+ keywords = [
15
+ "stream",
16
+ "flow",
17
+ "dataflow",
18
+ "reactive",
19
+ ]
20
+
21
+ [[project.authors]]
22
+ name = "Alfred"
23
+ email = "alfred82santa@gmail.com"
24
+
25
+ [project.license]
26
+ text = "LGPL-2.1-or-later"
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/pyrill-framework/pyrill"
30
+ Homepage = "https://github.com/pyrill-framework/pyrill"
31
+ Documentation = "https://pyrill-framework.readthedocs.io/stable/"
32
+ Issues = "https://github.com/pyrill-framework/pyrill/issues"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.3,<0.12.0"]
36
+ build-backend = "uv_build"
37
+
38
+ [tool.pytest]
39
+ asyncio_mode = "auto"
40
+
41
+ [tool.coverage.run]
42
+ omit = [".venv/*"]
43
+ branch = true
44
+ relative_files = false
45
+
46
+ [tool.coverage.report]
47
+ exclude_also = [
48
+ "def __repr__",
49
+ 'if self\.debug',
50
+ "raise AssertionError",
51
+ "raise NotImplementedError",
52
+ "if 0:",
53
+ "if __name__ == .__main__.:",
54
+ '@(abc\.)?abstractmethod',
55
+ "if TYPE_CHECKING",
56
+ "@overload",
57
+ ]
58
+
59
+ [tool.coverage.paths]
60
+ source = ["src/"]
61
+
62
+ [tool.poe]
63
+ envfile = [
64
+ ".env",
65
+ ".env.local",
66
+ ]
67
+ executor = "uv"
68
+
69
+ [tool.poe.tasks]
70
+ build = "uv build"
71
+ test = "pytest -v -s --junitxml=pytest.xml --cov src --cov-report term-missing --cov-report xml:coverage.xml --cov-fail-under=85 --exitfirst"
72
+ typecheck = "mypy src"
73
+ format = "ruff format src/**/*.py tests/**/*.py"
74
+ "format:check" = "ruff format src/**/*.py tests/**/*.py --check"
75
+ lint = "ruff check src/**/*.py tests/**/*.py"
76
+ "lint:fix" = "ruff check src/**/*.py tests/**/*.py --fix"
77
+
78
+ [tool.poe.tasks.clean]
79
+ cmd = """
80
+ rm -rf ./**/src/**/*.pyc
81
+ ./**/src/**/*.pyo
82
+ ./**/src/**/__pycache__
83
+ ./**/tests/**/*.pyc
84
+ ./**/tests/**/*.pyo
85
+ ./**/tests/**/__pycache__
86
+ .coverage
87
+ coverage.xml
88
+ pytest.xml
89
+ .*_cache
90
+ """
91
+ empty_glob = "null"
92
+
93
+ [tool.poe.tasks.validate]
94
+ help = "Execute all validations: typecheck, lint, and test"
95
+ sequence = [
96
+ "typecheck",
97
+ "format:check",
98
+ "lint",
99
+ "test",
100
+ ]
101
+
102
+ [tool.poe.tasks.prebuild]
103
+ help = "Pre-build checks: typecheck and lint"
104
+ sequence = [
105
+ "typecheck",
106
+ "lint",
107
+ ]
108
+
109
+ [tool.poe.tasks."clean:all"]
110
+ help = "Complete cleanup: cache, coverage, and temporary files"
111
+ sequence = [
112
+ "clean",
113
+ { cmd = """
114
+ rm -rf dist
115
+ build
116
+ .coverage
117
+ coverage.xml
118
+ pytest.xml
119
+ **/.*_cache
120
+ """ },
121
+ ]
122
+
123
+ [tool.poe.tasks.fix]
124
+ help = "Fix linting and formatting issues, then run tests"
125
+ sequence = [
126
+ "format",
127
+ "lint:fix",
128
+ ]
129
+
130
+ [tool.poe.tasks.docs]
131
+ help = "Build the documentation"
132
+ cmd = "sphinx-build -b html docs/source docs/build"
133
+
134
+ [dependency-groups]
135
+ dev = [
136
+ "mypy>=2.4.0",
137
+ "pytest>=9.1.1",
138
+ "pytest-asyncio>=1.4.0",
139
+ "pytest-cov>=7.1.0",
140
+ "ruff>=0.17.3",
141
+ ]
142
+ docs = ["sphinx>=9.2.0"]
@@ -0,0 +1,131 @@
1
+ [project]
2
+ name = "pyrill"
3
+ version = "0.2.0b0"
4
+ description = "Asynchronous streams for Python"
5
+ readme = "README.md"
6
+ authors = [{ name = "Alfred", email = "alfred82santa@gmail.com" }]
7
+ requires-python = ">=3.14"
8
+ dependencies = []
9
+ license = { text = "LGPL-2.1-or-later" }
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.14",
14
+ "Typing :: Typed"
15
+ ]
16
+ keywords = ["stream", "flow", "dataflow", "reactive"]
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/pyrill-framework/pyrill"
20
+ Homepage = "https://github.com/pyrill-framework/pyrill"
21
+ Documentation = "https://pyrill-framework.readthedocs.io/stable/"
22
+ Issues = "https://github.com/pyrill-framework/pyrill/issues"
23
+
24
+
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.11.3,<0.12.0"]
28
+ build-backend = "uv_build"
29
+
30
+
31
+ [tool.pytest]
32
+ asyncio_mode = "auto"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "mypy>=2.4.0",
37
+ "pytest>=9.1.1",
38
+ "pytest-asyncio>=1.4.0",
39
+ "pytest-cov>=7.1.0",
40
+ "ruff>=0.17.3",
41
+ ]
42
+ docs = [
43
+ "sphinx>=9.2.0",
44
+ ]
45
+
46
+ [tool.coverage.run]
47
+ omit = [".venv/*"]
48
+ branch = true
49
+ relative_files = false
50
+
51
+ [tool.coverage.report]
52
+ # Regexes for lines to exclude from consideration
53
+ exclude_also = [
54
+ # Dont complain about missing debug-only code:
55
+ "def __repr__",
56
+ "if self\\.debug",
57
+
58
+ # Don't complain if tests don't hit defensive assertion code:
59
+ "raise AssertionError",
60
+ "raise NotImplementedError",
61
+
62
+ # Don't complain if non-runnable code isn't run:
63
+ "if 0:",
64
+ "if __name__ == .__main__.:",
65
+
66
+ # Don't complain about abstract methods, they aren't run:
67
+ "@(abc\\.)?abstractmethod",
68
+
69
+ # Don't complain type checking imports, they aren't run:
70
+ "if TYPE_CHECKING",
71
+
72
+ # Don't complain overloads, they aren't run:
73
+ "@overload"
74
+ ]
75
+
76
+ [tool.coverage.paths]
77
+ source = ["src/"]
78
+
79
+ [tool.poe]
80
+ envfile = [".env", ".env.local"]
81
+ executor = "uv"
82
+
83
+ [tool.poe.tasks]
84
+ build = "uv build"
85
+ test = "pytest -v -s --junitxml=pytest.xml --cov src --cov-report term-missing --cov-report xml:coverage.xml --cov-fail-under=85 --exitfirst"
86
+ typecheck = "mypy src"
87
+ format = "ruff format src/**/*.py tests/**/*.py"
88
+ "format:check" = "ruff format src/**/*.py tests/**/*.py --check"
89
+ lint = "ruff check src/**/*.py tests/**/*.py"
90
+ "lint:fix" = "ruff check src/**/*.py tests/**/*.py --fix"
91
+
92
+ [tool.poe.tasks.clean]
93
+ cmd = """
94
+ rm -rf ./**/src/**/*.pyc
95
+ ./**/src/**/*.pyo
96
+ ./**/src/**/__pycache__
97
+ ./**/tests/**/*.pyc
98
+ ./**/tests/**/*.pyo
99
+ ./**/tests/**/__pycache__
100
+ .coverage
101
+ coverage.xml
102
+ pytest.xml
103
+ .*_cache
104
+ """
105
+ empty_glob = "null"
106
+
107
+ [tool.poe.tasks.validate]
108
+ help = "Execute all validations: typecheck, lint, and test"
109
+ sequence = ["typecheck", "format:check", "lint", "test"]
110
+
111
+ [tool.poe.tasks.prebuild]
112
+ help = "Pre-build checks: typecheck and lint"
113
+ sequence = ["typecheck", "lint"]
114
+
115
+ [tool.poe.tasks."clean:all"]
116
+ help = "Complete cleanup: cache, coverage, and temporary files"
117
+ sequence = ["clean", { cmd = """rm -rf dist
118
+ build
119
+ .coverage
120
+ coverage.xml
121
+ pytest.xml
122
+ **/.*_cache
123
+ """ }]
124
+
125
+ [tool.poe.tasks.fix]
126
+ help = "Fix linting and formatting issues, then run tests"
127
+ sequence = ["format", "lint:fix"]
128
+
129
+ [tool.poe.tasks.docs]
130
+ help = "Build the documentation"
131
+ cmd = "sphinx-build -b html docs/source docs/build"
File without changes
@@ -0,0 +1,190 @@
1
+ from collections.abc import Sized
2
+ from typing import Any
3
+
4
+ from .base import BaseStage
5
+
6
+ __all__ = ["Count", "ListAcc", "Size", "SumAcc"]
7
+
8
+
9
+ class SumAcc[AccData: (str, bytes, int, complex, float)](BaseStage[AccData, AccData]):
10
+ """
11
+ Stage that emits the running sum of the frames received so far.
12
+
13
+ After every frame the stage emits the accumulated value, so a source of ``n``
14
+ frames produces ``n`` outputs. The sum uses ``+``, so it also concatenates ``str``
15
+ and ``bytes`` frames. The accumulator is reset to ``initial_value`` on every mount
16
+ and cleared on unmount.
17
+
18
+ :param initial_value: Value the sum starts from. When ``None`` (default), the first
19
+ frame becomes the initial value.
20
+
21
+ Input and output::
22
+
23
+ input: 1, 2, 3
24
+ output: 1, 3, 6
25
+
26
+ Usage:
27
+
28
+ .. code-block:: python
29
+
30
+ from pyrill.accumulators import SumAcc
31
+ from pyrill.sources import IterableSource
32
+
33
+ source = IterableSource(source=[1, 2, 3])
34
+ stage = source >> SumAcc(initial_value=10)
35
+
36
+ await stage.mount()
37
+ frames = [frame async for frame in stage]
38
+ # [11, 13, 16]
39
+ """
40
+
41
+ def __init__(self, *args: Any, initial_value: AccData | None = None, **kwargs: Any):
42
+ super().__init__(*args, **kwargs)
43
+
44
+ self.initial_value: AccData | None = initial_value
45
+
46
+ self._accum: AccData | None = None
47
+
48
+ async def _mount(self) -> None:
49
+ self._accum = self.initial_value
50
+ await super()._mount()
51
+
52
+ async def _unmount(self) -> None:
53
+ self._accum = None
54
+ await super()._unmount()
55
+
56
+ async def _process_frame(self, frame: AccData) -> AccData:
57
+ if self._accum is None:
58
+ self._accum = frame
59
+ else:
60
+ self._accum += frame
61
+ return self._accum
62
+
63
+
64
+ class ListAcc[Source_co](BaseStage[Source_co, list[Source_co]]):
65
+ """
66
+ Stage that emits the list of all the frames received so far.
67
+
68
+ After every frame the stage emits a new list (a copy, so earlier outputs are not
69
+ modified later) holding every frame seen since the stage was mounted. The list is
70
+ reset on every mount.
71
+
72
+ Input and output::
73
+
74
+ input: "a", "b", "c"
75
+ output: ["a"], ["a", "b"], ["a", "b", "c"]
76
+
77
+ Usage:
78
+
79
+ .. code-block:: python
80
+
81
+ from pyrill.accumulators import ListAcc
82
+ from pyrill.sources import IterableSource
83
+
84
+ source = IterableSource(source=["a", "b", "c"])
85
+ stage = source >> ListAcc()
86
+
87
+ await stage.mount()
88
+ frames = [frame async for frame in stage]
89
+ # [["a"], ["a", "b"], ["a", "b", "c"]]
90
+ """
91
+
92
+ def __init__(self, *args: Any, **kwargs: Any):
93
+ super().__init__(*args, **kwargs)
94
+
95
+ self._accum: list[Source_co] = []
96
+
97
+ async def _mount(self) -> None:
98
+ self._accum = []
99
+ await super()._mount()
100
+
101
+ async def _process_frame(self, frame: Source_co) -> list[Source_co]:
102
+ self._accum.append(frame)
103
+
104
+ return self._accum.copy()
105
+
106
+
107
+ class Count(BaseStage[Any, int]):
108
+ """
109
+ Stage that emits how many frames it has received so far.
110
+
111
+ The frame content is ignored. After every frame the stage emits the number of frames
112
+ seen since it was mounted, starting at ``1``. The counter is reset on mount and on
113
+ unmount.
114
+
115
+ Input and output::
116
+
117
+ input: "a", "b", "c"
118
+ output: 1, 2, 3
119
+
120
+ Usage:
121
+
122
+ .. code-block:: python
123
+
124
+ from pyrill.accumulators import Count
125
+ from pyrill.sources import IterableSource
126
+
127
+ source = IterableSource(source=["a", "b", "c"])
128
+ stage = source >> Count()
129
+
130
+ await stage.mount()
131
+ frames = [frame async for frame in stage]
132
+ # [1, 2, 3]
133
+ """
134
+
135
+ _counter: int = 0
136
+
137
+ async def _mount(self) -> None:
138
+ self._counter = 0
139
+ await super()._mount()
140
+
141
+ async def _unmount(self) -> None:
142
+ self._counter = 0
143
+ await super()._unmount()
144
+
145
+ async def _process_frame(self, frame: Any) -> int:
146
+ self._counter += 1
147
+ return self._counter
148
+
149
+
150
+ class Size(BaseStage[Sized, int]):
151
+ """
152
+ Stage that emits the total ``len`` of the frames received so far.
153
+
154
+ Frames can be any sized object (``str``, ``bytes``, ``list``...). After every frame
155
+ the stage emits the sum of the lengths seen since it was mounted. The total is reset
156
+ on mount and on unmount.
157
+
158
+ Input and output::
159
+
160
+ input: "ab", "cde", "f"
161
+ output: 2, 5, 6
162
+
163
+ Usage:
164
+
165
+ .. code-block:: python
166
+
167
+ from pyrill.accumulators import Size
168
+ from pyrill.sources import IterableSource
169
+
170
+ source = IterableSource(source=["ab", "cde", "f"])
171
+ stage = source >> Size()
172
+
173
+ await stage.mount()
174
+ frames = [frame async for frame in stage]
175
+ # [2, 5, 6]
176
+ """
177
+
178
+ _acc: int = 0
179
+
180
+ async def _mount(self) -> None:
181
+ self._acc = 0
182
+ await super()._mount()
183
+
184
+ async def _unmount(self) -> None:
185
+ self._acc = 0
186
+ await super()._unmount()
187
+
188
+ async def _process_frame(self, frame: Sized) -> int:
189
+ self._acc += len(frame)
190
+ return self._acc