pymizu 0.0.1.dev0__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.
pymizu/__init__.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Single-producer single-consumer channels for Python.
|
|
2
|
+
|
|
3
|
+
A bounded ring buffer with optional timeouts on receives and sentinel
|
|
4
|
+
return values for full, timeout, and closed states. The transport is
|
|
5
|
+
in-process: both ends of a channel live in the calling Python process,
|
|
6
|
+
which suits the channel to prototyping producer and consumer designs
|
|
7
|
+
and to testing channel-based logic.
|
|
8
|
+
|
|
9
|
+
This interface is experimental and may change in a future release.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import math
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from collections import deque
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__version__ = "0.0.1.dev0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CLOSED",
|
|
25
|
+
"FULL",
|
|
26
|
+
"TIMEOUT",
|
|
27
|
+
"InprocChannel",
|
|
28
|
+
"Sentinel",
|
|
29
|
+
"__version__",
|
|
30
|
+
"inproc_channel",
|
|
31
|
+
"is_sentinel",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Sentinel:
|
|
36
|
+
"""A channel terminal state as an ordinary value, not an exception.
|
|
37
|
+
|
|
38
|
+
The singletons FULL, TIMEOUT, and CLOSED are interned: identity is
|
|
39
|
+
the test, and is_sentinel() is provenance — True only for the
|
|
40
|
+
exact objects the channel verbs return. A sentinel forwarded as a
|
|
41
|
+
payload arrives as an ordinary copy.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__slots__ = ("_name",)
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str) -> None:
|
|
47
|
+
self._name = name
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"<pymizu.{self._name}>"
|
|
51
|
+
|
|
52
|
+
def __copy__(self) -> Sentinel:
|
|
53
|
+
return Sentinel(self._name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
FULL = Sentinel("FULL")
|
|
57
|
+
"""The buffer is at capacity (InprocChannel.send())."""
|
|
58
|
+
|
|
59
|
+
TIMEOUT = Sentinel("TIMEOUT")
|
|
60
|
+
"""No value arrived within timeout (InprocChannel.recv())."""
|
|
61
|
+
|
|
62
|
+
CLOSED = Sentinel("CLOSED")
|
|
63
|
+
"""The channel is closed, and drained for InprocChannel.recv()."""
|
|
64
|
+
|
|
65
|
+
_SENTINELS = (FULL, TIMEOUT, CLOSED)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_sentinel(x: Any) -> bool:
|
|
69
|
+
"""Test for a pymizu sentinel.
|
|
70
|
+
|
|
71
|
+
Identity comparison against the interned sentinel singletons —
|
|
72
|
+
True only for the exact objects the channel verbs themselves
|
|
73
|
+
return. A sentinel forwarded over a channel arrives as an ordinary
|
|
74
|
+
copy and fails this test, so code that relays untrusted values can
|
|
75
|
+
distinguish its own terminal states from look-alike payloads.
|
|
76
|
+
"""
|
|
77
|
+
return any(x is s for s in _SENTINELS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InprocChannel:
|
|
81
|
+
"""A single-producer single-consumer channel: a bounded ring buffer.
|
|
82
|
+
|
|
83
|
+
Sends and receives are O(1). Both ends of the channel live in the
|
|
84
|
+
calling process; the buffer is lock-guarded, so a producer thread
|
|
85
|
+
and a consumer thread may exchange values without an external
|
|
86
|
+
lock.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
__slots__ = ("_buf", "_capacity", "_closed", "_cond")
|
|
90
|
+
|
|
91
|
+
def __init__(self, capacity: int) -> None:
|
|
92
|
+
if isinstance(capacity, bool) or not isinstance(capacity, int):
|
|
93
|
+
raise TypeError("pymizu: capacity must be a positive integer")
|
|
94
|
+
if capacity < 1:
|
|
95
|
+
raise ValueError("pymizu: capacity must be a positive integer")
|
|
96
|
+
self._buf: deque[Any] = deque()
|
|
97
|
+
self._capacity = capacity
|
|
98
|
+
self._closed = False
|
|
99
|
+
self._cond = threading.Condition()
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
with self._cond:
|
|
103
|
+
state = "closed" if self._closed else "open"
|
|
104
|
+
return (
|
|
105
|
+
f"<pymizu.InprocChannel: {state}, "
|
|
106
|
+
f"{len(self._buf)}/{self._capacity} buffered>"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def send(self, x: Any) -> bool | Sentinel:
|
|
110
|
+
"""Append x to the buffer.
|
|
111
|
+
|
|
112
|
+
Sends never block: a send on a full channel returns FULL
|
|
113
|
+
immediately, and a send on a closed channel returns CLOSED.
|
|
114
|
+
Returns True on success.
|
|
115
|
+
"""
|
|
116
|
+
with self._cond:
|
|
117
|
+
if self._closed:
|
|
118
|
+
return CLOSED
|
|
119
|
+
if len(self._buf) == self._capacity:
|
|
120
|
+
return FULL
|
|
121
|
+
self._buf.append(x)
|
|
122
|
+
self._cond.notify()
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
def recv(self, timeout: float | None = None) -> Any:
|
|
126
|
+
"""Remove and return the oldest buffered value.
|
|
127
|
+
|
|
128
|
+
Waits up to timeout seconds for one to arrive: None (the
|
|
129
|
+
default) and math.inf wait indefinitely, and 0 polls. Returns
|
|
130
|
+
TIMEOUT when the wait expires. A closed channel drains its
|
|
131
|
+
buffered values before recv() reports CLOSED.
|
|
132
|
+
|
|
133
|
+
The wait is on a monotonic clock: system clock adjustments do
|
|
134
|
+
not affect it.
|
|
135
|
+
"""
|
|
136
|
+
deadline = _recv_deadline(timeout)
|
|
137
|
+
with self._cond:
|
|
138
|
+
while not self._buf:
|
|
139
|
+
if self._closed:
|
|
140
|
+
return CLOSED
|
|
141
|
+
if deadline is None:
|
|
142
|
+
self._cond.wait()
|
|
143
|
+
continue
|
|
144
|
+
remaining = deadline - time.monotonic()
|
|
145
|
+
if remaining <= 0:
|
|
146
|
+
return TIMEOUT
|
|
147
|
+
self._cond.wait(remaining)
|
|
148
|
+
x = self._buf.popleft()
|
|
149
|
+
if is_sentinel(x):
|
|
150
|
+
# A forwarded sentinel crosses as an ordinary copy, so a
|
|
151
|
+
# returned singleton always tags a terminal state.
|
|
152
|
+
return copy.copy(x)
|
|
153
|
+
return x
|
|
154
|
+
|
|
155
|
+
def close(self) -> bool:
|
|
156
|
+
"""Orderly shutdown.
|
|
157
|
+
|
|
158
|
+
Values buffered before the close remain receivable; further
|
|
159
|
+
sends return CLOSED, and a receive on a drained channel
|
|
160
|
+
returns the same. Closing an already-closed channel is a
|
|
161
|
+
no-op. Returns True.
|
|
162
|
+
"""
|
|
163
|
+
with self._cond:
|
|
164
|
+
self._closed = True
|
|
165
|
+
self._cond.notify_all()
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _recv_deadline(timeout: float | None) -> float | None:
|
|
170
|
+
if timeout is None or timeout == math.inf:
|
|
171
|
+
return None
|
|
172
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
|
|
173
|
+
raise TypeError("pymizu: timeout must be a non-negative number")
|
|
174
|
+
if math.isnan(timeout) or timeout < 0:
|
|
175
|
+
raise ValueError("pymizu: timeout must be a non-negative number")
|
|
176
|
+
return time.monotonic() + timeout
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def inproc_channel(capacity: int) -> InprocChannel:
|
|
180
|
+
"""Create a single-producer single-consumer channel.
|
|
181
|
+
|
|
182
|
+
capacity is a positive integer: the number of values the channel
|
|
183
|
+
buffers before send() returns the FULL sentinel. Returns an
|
|
184
|
+
InprocChannel.
|
|
185
|
+
"""
|
|
186
|
+
return InprocChannel(capacity)
|
pymizu/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PEP 561 marker: this package ships inline type information.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymizu
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: Single-producer single-consumer channels
|
|
5
|
+
Author: pymizu authors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shikokuchuo/mizu-py
|
|
8
|
+
Project-URL: Issues, https://github.com/shikokuchuo/mizu-py/issues
|
|
9
|
+
Keywords: channel,concurrency,spsc
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# pymizu
|
|
21
|
+
|
|
22
|
+
Single-producer single-consumer channels for Python: a bounded ring
|
|
23
|
+
buffer with optional timeouts on receives and sentinel return values
|
|
24
|
+
for full, timeout, and closed states. The transport is in-process.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install pymizu
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import pymizu
|
|
36
|
+
|
|
37
|
+
ch = pymizu.inproc_channel(2)
|
|
38
|
+
|
|
39
|
+
ch.send("a")
|
|
40
|
+
ch.send(42)
|
|
41
|
+
ch.send("b") # full: returns the pymizu.FULL sentinel
|
|
42
|
+
|
|
43
|
+
ch.recv()
|
|
44
|
+
# 'a'
|
|
45
|
+
ch.recv()
|
|
46
|
+
# 42
|
|
47
|
+
ch.recv(timeout=0) # empty: do not wait
|
|
48
|
+
# <pymizu.TIMEOUT>
|
|
49
|
+
|
|
50
|
+
ch.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`timeout` is in seconds on a monotonic clock: `None` (the default) and
|
|
54
|
+
`math.inf` wait indefinitely, and `0` polls. The buffer is
|
|
55
|
+
lock-guarded, so a producer thread and a consumer thread may exchange
|
|
56
|
+
values without an external lock.
|
|
57
|
+
|
|
58
|
+
Terminal states surface as sentinel singletons, not exceptions:
|
|
59
|
+
`pymizu.FULL`, `pymizu.TIMEOUT`, and `pymizu.CLOSED`.
|
|
60
|
+
`pymizu.is_sentinel(x)` is an identity test for use where payloads are
|
|
61
|
+
untrusted.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pymizu/__init__.py,sha256=n7C7JZdDbA8Ezs_P4pJ1LIrm2Cjl7ioKzcAt1QzyymQ,6045
|
|
2
|
+
pymizu/py.typed,sha256=UYkntRy6-so-avkIET2nEnwoqE8LDrAyAGoImVTRPoc,62
|
|
3
|
+
pymizu-0.0.1.dev0.dist-info/licenses/LICENSE,sha256=xRD4WE0rFyYSQ12E7feo0kE59W53K3aLmy-mzfXRmoo,1073
|
|
4
|
+
pymizu-0.0.1.dev0.dist-info/METADATA,sha256=lneaTshOR8lDssFP_r7cqk2pGfhGuKdiZpXxVLXR0As,1637
|
|
5
|
+
pymizu-0.0.1.dev0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
pymizu-0.0.1.dev0.dist-info/top_level.txt,sha256=IY3CQRUHe0BxB3vwcAzZPYJliioOVgsMLLcCQW1Tj8Q,7
|
|
7
|
+
pymizu-0.0.1.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pymizu authors
|
|
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 @@
|
|
|
1
|
+
pymizu
|