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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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