bitspark-bitwire 0.1.0__tar.gz → 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.
- {bitspark_bitwire-0.1.0/src/bitspark_bitwire.egg-info → bitspark_bitwire-0.2.0}/PKG-INFO +12 -8
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/README.md +11 -7
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/pyproject.toml +1 -1
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0/src/bitspark_bitwire.egg-info}/PKG-INFO +12 -8
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitwire/__init__.py +20 -10
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/tests/test_consumer.py +65 -13
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/tests/typecheck.py +5 -2
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/LICENSE +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/MANIFEST.in +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/NOTICE +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/check.py +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/requirements-dev.txt +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/setup.cfg +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitspark_bitwire.egg-info/SOURCES.txt +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitspark_bitwire.egg-info/dependency_links.txt +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitspark_bitwire.egg-info/top_level.txt +0 -0
- {bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitwire/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bitspark-bitwire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The shared relative-path Wire contract for Python
|
|
5
5
|
Author: Bitspark
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -26,7 +26,7 @@ The package contains typed declarations and supporting values; endpoint runtimes
|
|
|
26
26
|
carriers, codecs and routing helpers belong to implementations.
|
|
27
27
|
|
|
28
28
|
```python
|
|
29
|
-
from bitwire import Message, Receiver, ReturnAddress, Wire
|
|
29
|
+
from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def call(endpoint: Wire, replies: Wire) -> None:
|
|
@@ -39,18 +39,22 @@ def call(endpoint: Wire, replies: Wire) -> None:
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def listen(endpoint:
|
|
42
|
+
def listen(endpoint: Endpoint):
|
|
43
43
|
def receive(path, message):
|
|
44
44
|
print(path, message.frame)
|
|
45
45
|
|
|
46
|
-
return endpoint.receive(
|
|
46
|
+
return endpoint.receive(Receiver(message=receive))
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`Wire` is a structural `Protocol`; implementations do not need to inherit
|
|
50
|
-
it. `send` accepts or refuses synchronously; the implementation schedules
|
|
51
|
-
|
|
49
|
+
`Wire` is a send-only structural `Protocol`; implementations do not need to inherit
|
|
50
|
+
from it. `send` accepts or refuses synchronously; the implementation schedules
|
|
51
|
+
delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
|
|
52
|
+
attachment and closure authority separate from send access. One receive attachment
|
|
53
|
+
is allowed at a time; duplicates are refused. Receivers may return an awaitable.
|
|
54
|
+
`receive` returns an idempotent detach function that cannot remove a replacement.
|
|
55
|
+
Path matching belongs to routing compositions, not to these interfaces.
|
|
52
56
|
Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
|
|
53
|
-
Receiver paths are relative to the
|
|
57
|
+
Receiver paths are relative to the attached endpoint.
|
|
54
58
|
|
|
55
59
|
`ReturnAddress` uses object identity, including when its Wire cannot be compared
|
|
56
60
|
or hashed. Preserve this object during routing. The return address is local
|
|
@@ -6,7 +6,7 @@ The package contains typed declarations and supporting values; endpoint runtimes
|
|
|
6
6
|
carriers, codecs and routing helpers belong to implementations.
|
|
7
7
|
|
|
8
8
|
```python
|
|
9
|
-
from bitwire import Message, Receiver, ReturnAddress, Wire
|
|
9
|
+
from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def call(endpoint: Wire, replies: Wire) -> None:
|
|
@@ -19,18 +19,22 @@ def call(endpoint: Wire, replies: Wire) -> None:
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def listen(endpoint:
|
|
22
|
+
def listen(endpoint: Endpoint):
|
|
23
23
|
def receive(path, message):
|
|
24
24
|
print(path, message.frame)
|
|
25
25
|
|
|
26
|
-
return endpoint.receive(
|
|
26
|
+
return endpoint.receive(Receiver(message=receive))
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`Wire` is a structural `Protocol`; implementations do not need to inherit
|
|
30
|
-
it. `send` accepts or refuses synchronously; the implementation schedules
|
|
31
|
-
|
|
29
|
+
`Wire` is a send-only structural `Protocol`; implementations do not need to inherit
|
|
30
|
+
from it. `send` accepts or refuses synchronously; the implementation schedules
|
|
31
|
+
delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
|
|
32
|
+
attachment and closure authority separate from send access. One receive attachment
|
|
33
|
+
is allowed at a time; duplicates are refused. Receivers may return an awaitable.
|
|
34
|
+
`receive` returns an idempotent detach function that cannot remove a replacement.
|
|
35
|
+
Path matching belongs to routing compositions, not to these interfaces.
|
|
32
36
|
Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
|
|
33
|
-
Receiver paths are relative to the
|
|
37
|
+
Receiver paths are relative to the attached endpoint.
|
|
34
38
|
|
|
35
39
|
`ReturnAddress` uses object identity, including when its Wire cannot be compared
|
|
36
40
|
or hashed. Preserve this object during routing. The return address is local
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bitspark-bitwire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The shared relative-path Wire contract for Python
|
|
5
5
|
Author: Bitspark
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -26,7 +26,7 @@ The package contains typed declarations and supporting values; endpoint runtimes
|
|
|
26
26
|
carriers, codecs and routing helpers belong to implementations.
|
|
27
27
|
|
|
28
28
|
```python
|
|
29
|
-
from bitwire import Message, Receiver, ReturnAddress, Wire
|
|
29
|
+
from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def call(endpoint: Wire, replies: Wire) -> None:
|
|
@@ -39,18 +39,22 @@ def call(endpoint: Wire, replies: Wire) -> None:
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def listen(endpoint:
|
|
42
|
+
def listen(endpoint: Endpoint):
|
|
43
43
|
def receive(path, message):
|
|
44
44
|
print(path, message.frame)
|
|
45
45
|
|
|
46
|
-
return endpoint.receive(
|
|
46
|
+
return endpoint.receive(Receiver(message=receive))
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`Wire` is a structural `Protocol`; implementations do not need to inherit
|
|
50
|
-
it. `send` accepts or refuses synchronously; the implementation schedules
|
|
51
|
-
|
|
49
|
+
`Wire` is a send-only structural `Protocol`; implementations do not need to inherit
|
|
50
|
+
from it. `send` accepts or refuses synchronously; the implementation schedules
|
|
51
|
+
delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
|
|
52
|
+
attachment and closure authority separate from send access. One receive attachment
|
|
53
|
+
is allowed at a time; duplicates are refused. Receivers may return an awaitable.
|
|
54
|
+
`receive` returns an idempotent detach function that cannot remove a replacement.
|
|
55
|
+
Path matching belongs to routing compositions, not to these interfaces.
|
|
52
56
|
Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
|
|
53
|
-
Receiver paths are relative to the
|
|
57
|
+
Receiver paths are relative to the attached endpoint.
|
|
54
58
|
|
|
55
59
|
`ReturnAddress` uses object identity, including when its Wire cannot be compared
|
|
56
60
|
or hashed. Preserve this object during routing. The return address is local
|
|
@@ -25,6 +25,7 @@ __all__ = [
|
|
|
25
25
|
"Message",
|
|
26
26
|
"Receiver",
|
|
27
27
|
"Wire",
|
|
28
|
+
"Endpoint",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
# Opaque Unicode-scalar segments: [] != [""] != ["a/b"] != ["a", "b"].
|
|
@@ -111,30 +112,39 @@ class Message:
|
|
|
111
112
|
|
|
112
113
|
@dataclass(frozen=True)
|
|
113
114
|
class Receiver:
|
|
114
|
-
"""Callbacks receive
|
|
115
|
+
"""Callbacks receive every path relative to their attached endpoint.
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
may be synchronous or awaitable; endpoint implementations own dispatch.
|
|
117
|
+
A receiver may be synchronous or awaitable; endpoints own dispatch.
|
|
118
118
|
"""
|
|
119
119
|
|
|
120
|
-
namespace: bool = False
|
|
121
120
|
message: Callable[[Path, Message], None | Awaitable[None]] | None = None
|
|
122
121
|
closed: Callable[[int, str], None] | None = None
|
|
123
122
|
|
|
124
123
|
|
|
125
124
|
@runtime_checkable
|
|
126
125
|
class Wire(Protocol):
|
|
127
|
-
"""
|
|
126
|
+
"""Send-only access with synchronous admission and asynchronous dispatch.
|
|
128
127
|
|
|
129
128
|
send returns on acceptance or raises on refusal, without running destination
|
|
130
|
-
application code on the sender's stack.
|
|
131
|
-
|
|
132
|
-
selected view shares that closure, while a mount owns only its registrations
|
|
133
|
-
and routing. Structural protocol matching alone does not prove these laws.
|
|
129
|
+
application code on the sender's stack. It grants no receiving or closure
|
|
130
|
+
authority. Structural protocol matching alone does not prove these laws.
|
|
134
131
|
"""
|
|
135
132
|
|
|
136
133
|
def send(self, path: Path, message: Message) -> None: ...
|
|
137
134
|
|
|
138
|
-
|
|
135
|
+
|
|
136
|
+
@runtime_checkable
|
|
137
|
+
class Endpoint(Wire, Protocol):
|
|
138
|
+
"""Owning endpoint access with one receive attachment and endpoint closure.
|
|
139
|
+
|
|
140
|
+
receive refuses a second active attachment or a closed endpoint and returns
|
|
141
|
+
an idempotent detach. A stale detach cannot remove a later attachment. Callbacks receive the complete
|
|
142
|
+
message and its relative path; routing policy belongs above this boundary.
|
|
143
|
+
Detach preserves captured return access and does not close the endpoint.
|
|
144
|
+
Closing notifies the active receiver once; detached receivers are not notified.
|
|
145
|
+
Closing twice has no additional effect and does not release live bindings.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def receive(self, receiver: Receiver) -> Callable[[], None]: ...
|
|
139
149
|
|
|
140
150
|
def close(self, code: int = 1000, reason: str = "") -> None: ...
|
|
@@ -7,7 +7,7 @@ import inspect
|
|
|
7
7
|
import unittest
|
|
8
8
|
from collections.abc import Callable
|
|
9
9
|
|
|
10
|
-
from bitwire import Message, Path, Receiver, ReturnAddress, Wire
|
|
10
|
+
from bitwire import Endpoint, Message, Path, Receiver, ReturnAddress, Wire
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class RecordingEndpoint:
|
|
@@ -15,7 +15,8 @@ class RecordingEndpoint:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self) -> None:
|
|
17
17
|
self.sent: list[tuple[tuple[str, ...], Message]] = []
|
|
18
|
-
self.
|
|
18
|
+
self.receiver: Receiver | None = None
|
|
19
|
+
self.attachment: object | None = None
|
|
19
20
|
self.ending: tuple[int, str] | None = None
|
|
20
21
|
|
|
21
22
|
def __eq__(self, other: object) -> bool:
|
|
@@ -24,17 +25,31 @@ class RecordingEndpoint:
|
|
|
24
25
|
def send(self, path: Path, message: Message) -> None:
|
|
25
26
|
self.sent.append((tuple(path), message))
|
|
26
27
|
|
|
27
|
-
def receive(self,
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
def receive(self, receiver: Receiver) -> Callable[[], None]:
|
|
29
|
+
if self.ending is not None:
|
|
30
|
+
raise RuntimeError("endpoint closed")
|
|
31
|
+
if self.receiver is not None:
|
|
32
|
+
raise RuntimeError("receiver already attached")
|
|
33
|
+
token = object()
|
|
34
|
+
self.receiver = receiver
|
|
35
|
+
self.attachment = token
|
|
30
36
|
|
|
31
37
|
def detach() -> None:
|
|
32
|
-
self.
|
|
38
|
+
if self.attachment is token:
|
|
39
|
+
self.receiver = None
|
|
40
|
+
self.attachment = None
|
|
33
41
|
|
|
34
42
|
return detach
|
|
35
43
|
|
|
36
44
|
def close(self, code: int = 1000, reason: str = "") -> None:
|
|
45
|
+
if self.ending is not None:
|
|
46
|
+
return
|
|
37
47
|
self.ending = (code, reason)
|
|
48
|
+
receiver = self.receiver
|
|
49
|
+
self.receiver = None
|
|
50
|
+
self.attachment = None
|
|
51
|
+
if receiver is not None and receiver.closed is not None:
|
|
52
|
+
receiver.closed(code, reason)
|
|
38
53
|
|
|
39
54
|
|
|
40
55
|
class ConsumerTests(unittest.TestCase):
|
|
@@ -72,14 +87,13 @@ class ConsumerTests(unittest.TestCase):
|
|
|
72
87
|
await asyncio.sleep(0)
|
|
73
88
|
observed.append((tuple(path), message))
|
|
74
89
|
|
|
75
|
-
receiver = Receiver(
|
|
76
|
-
detach = endpoint.receive(
|
|
90
|
+
receiver = Receiver(message=delivered)
|
|
91
|
+
detach = endpoint.receive(receiver)
|
|
77
92
|
request = Message({"version": 1, "kind": "event", "data": None})
|
|
78
93
|
|
|
79
94
|
async def dispatch() -> None:
|
|
80
|
-
registered = endpoint.
|
|
81
|
-
|
|
82
|
-
assert registered.message is not None
|
|
95
|
+
registered = endpoint.receiver
|
|
96
|
+
assert registered is not None and registered.message is not None
|
|
83
97
|
result = registered.message(["reply", ""], request)
|
|
84
98
|
if inspect.isawaitable(result):
|
|
85
99
|
await result
|
|
@@ -88,9 +102,47 @@ class ConsumerTests(unittest.TestCase):
|
|
|
88
102
|
self.assertEqual(observed, [(("reply", ""), request)])
|
|
89
103
|
detach()
|
|
90
104
|
detach()
|
|
91
|
-
self.
|
|
105
|
+
self.assertIsNone(endpoint.receiver)
|
|
92
106
|
self.assertIsNone(endpoint.ending)
|
|
93
107
|
|
|
108
|
+
def test_attachment_ownership_and_stale_detach(self) -> None:
|
|
109
|
+
endpoint: Endpoint = RecordingEndpoint()
|
|
110
|
+
first = Receiver()
|
|
111
|
+
detach = endpoint.receive(first)
|
|
112
|
+
with self.assertRaises(RuntimeError):
|
|
113
|
+
endpoint.receive(first)
|
|
114
|
+
detach()
|
|
115
|
+
second = Receiver()
|
|
116
|
+
second_detach = endpoint.receive(second)
|
|
117
|
+
detach()
|
|
118
|
+
assert isinstance(endpoint, RecordingEndpoint)
|
|
119
|
+
self.assertIs(endpoint.receiver, second)
|
|
120
|
+
second_detach()
|
|
121
|
+
self.assertIsNone(endpoint.receiver)
|
|
122
|
+
|
|
123
|
+
def test_close_notifies_only_active_attachment_once(self) -> None:
|
|
124
|
+
endpoint = RecordingEndpoint()
|
|
125
|
+
endings: list[tuple[int, str]] = []
|
|
126
|
+
detached: list[tuple[int, str]] = []
|
|
127
|
+
endpoint.receive(Receiver(closed=lambda c, r: detached.append((c, r))))()
|
|
128
|
+
endpoint.receive(Receiver(closed=lambda c, r: endings.append((c, r))))
|
|
129
|
+
endpoint.close(1000, "done")
|
|
130
|
+
endpoint.close(1001, "again")
|
|
131
|
+
self.assertEqual(endings, [(1000, "done")])
|
|
132
|
+
self.assertEqual(detached, [])
|
|
133
|
+
with self.assertRaises(RuntimeError):
|
|
134
|
+
endpoint.receive(Receiver())
|
|
135
|
+
|
|
136
|
+
def test_send_only_wire_requires_no_endpoint_control(self) -> None:
|
|
137
|
+
class Access:
|
|
138
|
+
def send(self, path: Path, message: Message) -> None:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
access: Wire = Access()
|
|
142
|
+
self.assertIsInstance(access, Wire)
|
|
143
|
+
self.assertNotIsInstance(access, Endpoint)
|
|
144
|
+
self.assertIs(ReturnAddress(access).wire, access)
|
|
145
|
+
|
|
94
146
|
def test_closed_callback_is_independent_from_message_callback(self) -> None:
|
|
95
147
|
endings: list[tuple[int, str]] = []
|
|
96
148
|
receiver = Receiver(closed=lambda code, reason: endings.append((code, reason)))
|
|
@@ -98,7 +150,7 @@ class ConsumerTests(unittest.TestCase):
|
|
|
98
150
|
receiver.closed(1000, "complete")
|
|
99
151
|
self.assertEqual(endings, [(1000, "complete")])
|
|
100
152
|
self.assertIsNone(receiver.message)
|
|
101
|
-
|
|
153
|
+
|
|
102
154
|
|
|
103
155
|
|
|
104
156
|
if __name__ == "__main__":
|
|
@@ -5,6 +5,7 @@ from typing import assert_type
|
|
|
5
5
|
from bitwire import (
|
|
6
6
|
CancelFrame,
|
|
7
7
|
ErrorFrame,
|
|
8
|
+
Endpoint,
|
|
8
9
|
EventFrame,
|
|
9
10
|
Message,
|
|
10
11
|
Path,
|
|
@@ -18,7 +19,7 @@ from bitwire import (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def consume(wire: Wire, frame: ProfileFrame) -> None:
|
|
22
|
+
def consume(wire: Wire, endpoint: Endpoint, frame: ProfileFrame) -> None:
|
|
22
23
|
address = ReturnAddress(wire)
|
|
23
24
|
wire.send(["scope", ""], Message(frame, address))
|
|
24
25
|
assert_type(address.wire, Wire)
|
|
@@ -30,7 +31,9 @@ def consume(wire: Wire, frame: ProfileFrame) -> None:
|
|
|
30
31
|
async def delivered(path: Path, message: Message) -> None:
|
|
31
32
|
assert_type(message.return_address, ReturnAddress | None)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
endpoint.receive(Receiver(message=delivered))()
|
|
35
|
+
wire.receive(Receiver()) # type: ignore[attr-defined]
|
|
36
|
+
wire.close() # type: ignore[attr-defined]
|
|
34
37
|
wire.send([42], Message(frame)) # type: ignore[list-item]
|
|
35
38
|
|
|
36
39
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitspark_bitwire.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{bitspark_bitwire-0.1.0 → bitspark_bitwire-0.2.0}/src/bitspark_bitwire.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|