pyrill 0.2.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.
pyrill-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyrill
3
+ Version: 0.2.0
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
+ The [messages and monitoring guide](docs/source/guides/messages-and-monitoring.rst)
93
+ explains how to observe lifecycle changes, rejected frames and file activity, and
94
+ publish custom progress events through the pipeline's message bus.
95
+
96
+ ## Project
97
+
98
+ - Repository and issue tracker: <https://github.com/pyrill-framework/pyrill>
99
+ - License: LGPL-2.1-or-later
pyrill-0.2.0/README.md ADDED
@@ -0,0 +1,80 @@
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
+ The [messages and monitoring guide](docs/source/guides/messages-and-monitoring.rst)
74
+ explains how to observe lifecycle changes, rejected frames and file activity, and
75
+ publish custom progress events through the pipeline's message bus.
76
+
77
+ ## Project
78
+
79
+ - Repository and issue tracker: <https://github.com/pyrill-framework/pyrill>
80
+ - License: LGPL-2.1-or-later
@@ -0,0 +1,146 @@
1
+ [project]
2
+ name = "pyrill"
3
+ version = "0.2.0"
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
+ [tool.poe.tasks."docs:clean"]
135
+ help = "Clean the documentation build"
136
+ cmd = "rm -rf docs/build"
137
+
138
+ [dependency-groups]
139
+ dev = [
140
+ "mypy>=2.4.0",
141
+ "pytest>=9.1.1",
142
+ "pytest-asyncio>=1.4.0",
143
+ "pytest-cov>=7.1.0",
144
+ "ruff>=0.17.3",
145
+ ]
146
+ docs = ["sphinx>=9.2.0"]
@@ -0,0 +1,132 @@
1
+ [project]
2
+ name = "pyrill"
3
+ version = "0.2.0"
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
+ [build-system]
26
+ requires = ["uv_build>=0.11.3,<0.12.0"]
27
+ build-backend = "uv_build"
28
+
29
+
30
+ [tool.pytest]
31
+ asyncio_mode = "auto"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "mypy>=2.4.0",
36
+ "pytest>=9.1.1",
37
+ "pytest-asyncio>=1.4.0",
38
+ "pytest-cov>=7.1.0",
39
+ "ruff>=0.17.3",
40
+ ]
41
+ docs = ["sphinx>=9.2.0"]
42
+
43
+ [tool.coverage.run]
44
+ omit = [".venv/*"]
45
+ branch = true
46
+ relative_files = false
47
+
48
+ [tool.coverage.report]
49
+ # Regexes for lines to exclude from consideration
50
+ exclude_also = [
51
+ # Dont complain about missing debug-only code:
52
+ "def __repr__",
53
+ "if self\\.debug",
54
+
55
+ # Don't complain if tests don't hit defensive assertion code:
56
+ "raise AssertionError",
57
+ "raise NotImplementedError",
58
+
59
+ # Don't complain if non-runnable code isn't run:
60
+ "if 0:",
61
+ "if __name__ == .__main__.:",
62
+
63
+ # Don't complain about abstract methods, they aren't run:
64
+ "@(abc\\.)?abstractmethod",
65
+
66
+ # Don't complain type checking imports, they aren't run:
67
+ "if TYPE_CHECKING",
68
+
69
+ # Don't complain overloads, they aren't run:
70
+ "@overload",
71
+ ]
72
+
73
+ [tool.coverage.paths]
74
+ source = ["src/"]
75
+
76
+ [tool.poe]
77
+ envfile = [".env", ".env.local"]
78
+ executor = "uv"
79
+
80
+ [tool.poe.tasks]
81
+ build = "uv build"
82
+ test = "pytest -v -s --junitxml=pytest.xml --cov src --cov-report term-missing --cov-report xml:coverage.xml --cov-fail-under=85 --exitfirst"
83
+ typecheck = "mypy src"
84
+ format = "ruff format src/**/*.py tests/**/*.py"
85
+ "format:check" = "ruff format src/**/*.py tests/**/*.py --check"
86
+ lint = "ruff check src/**/*.py tests/**/*.py"
87
+ "lint:fix" = "ruff check src/**/*.py tests/**/*.py --fix"
88
+
89
+ [tool.poe.tasks.clean]
90
+ cmd = """
91
+ rm -rf ./**/src/**/*.pyc
92
+ ./**/src/**/*.pyo
93
+ ./**/src/**/__pycache__
94
+ ./**/tests/**/*.pyc
95
+ ./**/tests/**/*.pyo
96
+ ./**/tests/**/__pycache__
97
+ .coverage
98
+ coverage.xml
99
+ pytest.xml
100
+ .*_cache
101
+ """
102
+ empty_glob = "null"
103
+
104
+ [tool.poe.tasks.validate]
105
+ help = "Execute all validations: typecheck, lint, and test"
106
+ sequence = ["typecheck", "format:check", "lint", "test"]
107
+
108
+ [tool.poe.tasks.prebuild]
109
+ help = "Pre-build checks: typecheck and lint"
110
+ sequence = ["typecheck", "lint"]
111
+
112
+ [tool.poe.tasks."clean:all"]
113
+ help = "Complete cleanup: cache, coverage, and temporary files"
114
+ sequence = ["clean", { cmd = """rm -rf dist
115
+ build
116
+ .coverage
117
+ coverage.xml
118
+ pytest.xml
119
+ **/.*_cache
120
+ """ }]
121
+
122
+ [tool.poe.tasks.fix]
123
+ help = "Fix linting and formatting issues, then run tests"
124
+ sequence = ["format", "lint:fix"]
125
+
126
+ [tool.poe.tasks.docs]
127
+ help = "Build the documentation"
128
+ cmd = "sphinx-build -b html docs/source docs/build"
129
+
130
+ [tool.poe.tasks."docs:clean"]
131
+ help = "Clean the documentation build"
132
+ cmd = "rm -rf docs/build"
File without changes
@@ -0,0 +1,195 @@
1
+ from collections.abc import Sized
2
+ from typing import Any
3
+
4
+ from .base import BaseStage
5
+
6
+ __all__ = [
7
+ "Count",
8
+ "ListAcc",
9
+ "Size",
10
+ "SumAcc",
11
+ ]
12
+
13
+
14
+ class SumAcc[AccData: (str, bytes, int, complex, float)](BaseStage[AccData, AccData]):
15
+ """
16
+ Stage that emits the running sum of the frames received so far.
17
+
18
+ After every frame the stage emits the accumulated value, so a source of ``n``
19
+ frames produces ``n`` outputs. The sum uses ``+``, so it also concatenates ``str``
20
+ and ``bytes`` frames. The accumulator is reset to ``initial_value`` on every mount
21
+ and cleared on unmount.
22
+
23
+ :param initial_value: Value the sum starts from. When ``None`` (default), the first
24
+ frame becomes the initial value.
25
+
26
+ Input and output::
27
+
28
+ input: 1, 2, 3
29
+ output: 1, 3, 6
30
+
31
+ Usage:
32
+
33
+ .. code-block:: python
34
+
35
+ from pyrill.accumulators import SumAcc
36
+ from pyrill.sources import IterableSource
37
+
38
+ source = IterableSource(source=[1, 2, 3])
39
+ stage = source >> SumAcc(initial_value=10)
40
+
41
+ await stage.mount()
42
+ frames = [frame async for frame in stage]
43
+ # [11, 13, 16]
44
+ """
45
+
46
+ def __init__(self, *args: Any, initial_value: AccData | None = None, **kwargs: Any):
47
+ super().__init__(*args, **kwargs)
48
+
49
+ self.initial_value: AccData | None = initial_value
50
+
51
+ self._accum: AccData | None = None
52
+
53
+ async def _mount(self) -> None:
54
+ self._accum = self.initial_value
55
+ await super()._mount()
56
+
57
+ async def _unmount(self) -> None:
58
+ self._accum = None
59
+ await super()._unmount()
60
+
61
+ async def _process_frame(self, frame: AccData) -> AccData:
62
+ if self._accum is None:
63
+ self._accum = frame
64
+ else:
65
+ self._accum += frame
66
+ return self._accum
67
+
68
+
69
+ class ListAcc[Source_co](BaseStage[Source_co, list[Source_co]]):
70
+ """
71
+ Stage that emits the list of all the frames received so far.
72
+
73
+ After every frame the stage emits a new list (a copy, so earlier outputs are not
74
+ modified later) holding every frame seen since the stage was mounted. The list is
75
+ reset on every mount.
76
+
77
+ Input and output::
78
+
79
+ input: "a", "b", "c"
80
+ output: ["a"], ["a", "b"], ["a", "b", "c"]
81
+
82
+ Usage:
83
+
84
+ .. code-block:: python
85
+
86
+ from pyrill.accumulators import ListAcc
87
+ from pyrill.sources import IterableSource
88
+
89
+ source = IterableSource(source=["a", "b", "c"])
90
+ stage = source >> ListAcc()
91
+
92
+ await stage.mount()
93
+ frames = [frame async for frame in stage]
94
+ # [["a"], ["a", "b"], ["a", "b", "c"]]
95
+ """
96
+
97
+ def __init__(self, *args: Any, **kwargs: Any):
98
+ super().__init__(*args, **kwargs)
99
+
100
+ self._accum: list[Source_co] = []
101
+
102
+ async def _mount(self) -> None:
103
+ self._accum = []
104
+ await super()._mount()
105
+
106
+ async def _process_frame(self, frame: Source_co) -> list[Source_co]:
107
+ self._accum.append(frame)
108
+
109
+ return self._accum.copy()
110
+
111
+
112
+ class Count(BaseStage[Any, int]):
113
+ """
114
+ Stage that emits how many frames it has received so far.
115
+
116
+ The frame content is ignored. After every frame the stage emits the number of frames
117
+ seen since it was mounted, starting at ``1``. The counter is reset on mount and on
118
+ unmount.
119
+
120
+ Input and output::
121
+
122
+ input: "a", "b", "c"
123
+ output: 1, 2, 3
124
+
125
+ Usage:
126
+
127
+ .. code-block:: python
128
+
129
+ from pyrill.accumulators import Count
130
+ from pyrill.sources import IterableSource
131
+
132
+ source = IterableSource(source=["a", "b", "c"])
133
+ stage = source >> Count()
134
+
135
+ await stage.mount()
136
+ frames = [frame async for frame in stage]
137
+ # [1, 2, 3]
138
+ """
139
+
140
+ _counter: int = 0
141
+
142
+ async def _mount(self) -> None:
143
+ self._counter = 0
144
+ await super()._mount()
145
+
146
+ async def _unmount(self) -> None:
147
+ self._counter = 0
148
+ await super()._unmount()
149
+
150
+ async def _process_frame(self, frame: Any) -> int:
151
+ self._counter += 1
152
+ return self._counter
153
+
154
+
155
+ class Size(BaseStage[Sized, int]):
156
+ """
157
+ Stage that emits the total ``len`` of the frames received so far.
158
+
159
+ Frames can be any sized object (``str``, ``bytes``, ``list``...). After every frame
160
+ the stage emits the sum of the lengths seen since it was mounted. The total is reset
161
+ on mount and on unmount.
162
+
163
+ Input and output::
164
+
165
+ input: "ab", "cde", "f"
166
+ output: 2, 5, 6
167
+
168
+ Usage:
169
+
170
+ .. code-block:: python
171
+
172
+ from pyrill.accumulators import Size
173
+ from pyrill.sources import IterableSource
174
+
175
+ source = IterableSource(source=["ab", "cde", "f"])
176
+ stage = source >> Size()
177
+
178
+ await stage.mount()
179
+ frames = [frame async for frame in stage]
180
+ # [2, 5, 6]
181
+ """
182
+
183
+ _acc: int = 0
184
+
185
+ async def _mount(self) -> None:
186
+ self._acc = 0
187
+ await super()._mount()
188
+
189
+ async def _unmount(self) -> None:
190
+ self._acc = 0
191
+ await super()._unmount()
192
+
193
+ async def _process_frame(self, frame: Sized) -> int:
194
+ self._acc += len(frame)
195
+ return self._acc