bitspark-bitwire 0.2.0__tar.gz → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitspark-bitwire
3
- Version: 0.2.0
4
- Summary: The shared relative-path Wire contract for Python
3
+ Version: 0.3.0
4
+ Summary: Addressless Wire primitives, full WireTree structure and addressed carrier contracts for Python
5
5
  Author: Bitspark
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/Bitspark/bitwire
@@ -20,16 +20,41 @@ Dynamic: license-file
20
20
 
21
21
  # Bitwire for Python
22
22
 
23
- `bitspark-bitwire` provides the shared relative-path Wire contract as the Python
23
+ **Source contract: 0.3.0; publication pending.** `Wire` is the addressless
24
+ primitive `send(message)`. `WireTree = DeixisNode<Wire>` provides the complete
25
+ finite, acyclic structure: own value, complete byte-keyed children, partial
26
+ selection and decomposition. Keys are exact arbitrary bytes, including empty
27
+ and non-UTF-8 keys. Empty path selects self; a missing edge is distinct from a
28
+ present refusing primitive. Recomposition preserves own and child identities.
29
+
30
+ This is symmetric with Bitstore's `Data.read()` primitive and
31
+ `DataTree = DeixisNode<Data>`. Derived sending selects the node and invokes its
32
+ own Wire. Construction and derived operators belong in bitruntime; these
33
+ packages publish declarations and criteria, not a production tree runtime.
34
+ See [decision 0012](https://github.com/Bitspark/bitwire/blob/main/docs/decisions/0012-explicit-data-and-wire-trees.md)
35
+ and the [migration guide](https://github.com/Bitspark/bitwire/blob/main/docs/migration-0.3.md).
36
+
37
+ `AddressedWire` explicitly names the former `Wire.send(path, message)` surface.
38
+ It is not a full WireTree. `Endpoint` extends AddressedWire, and return
39
+ capabilities retain AddressedWire so the existing response/lifecycle path
40
+ space, local identity, received context and closure rules remain intact.
41
+ Carrier paths remain exact Unicode-scalar strings under unchanged `bitwire/1`;
42
+ they do not imply support for arbitrary tree byte keys on that carrier.
43
+
44
+ The following addressed-carrier examples use the **0.3 source names**. Older
45
+ 0.2.0 artifacts used `Wire` for the addressed interface; their release evidence
46
+ does not validate the renamed declarations or full structural trees.
47
+
48
+ `bitspark-bitwire` provides the shared relative-path AddressedWire contract as the Python
24
49
  module `bitwire`. It requires Python 3.11 or newer and has no runtime dependencies.
25
50
  The package contains typed declarations and supporting values; endpoint runtimes,
26
51
  carriers, codecs and routing helpers belong to implementations.
27
52
 
28
53
  ```python
29
- from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
54
+ from bitwire import Endpoint, Message, Receiver, ReturnAddress, AddressedWire
30
55
 
31
56
 
32
- def call(endpoint: Wire, replies: Wire) -> None:
57
+ def call(endpoint: AddressedWire, replies: AddressedWire) -> None:
33
58
  endpoint.send(
34
59
  ["cells", "get"],
35
60
  Message(
@@ -46,7 +71,7 @@ def listen(endpoint: Endpoint):
46
71
  return endpoint.receive(Receiver(message=receive))
47
72
  ```
48
73
 
49
- `Wire` is a send-only structural `Protocol`; implementations do not need to inherit
74
+ `AddressedWire` is a send-only structural `Protocol`; implementations do not need to inherit
50
75
  from it. `send` accepts or refuses synchronously; the implementation schedules
51
76
  delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
52
77
  attachment and closure authority separate from send access. One receive attachment
@@ -56,7 +81,7 @@ Path matching belongs to routing compositions, not to these interfaces.
56
81
  Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
57
82
  Receiver paths are relative to the attached endpoint.
58
83
 
59
- `ReturnAddress` uses object identity, including when its Wire cannot be compared
84
+ `ReturnAddress` uses object identity, including when its AddressedWire cannot be compared
60
85
  or hashed. Preserve this object during routing. The return address is local
61
86
  capability data and must never be serialized into the profile envelope. Profile
62
87
  payloads must follow the JSON profile despite their Python `object` annotation.
@@ -64,7 +89,7 @@ Runtime-owned received context associated with the return identity must survive
64
89
  routing. Application-supplied context or metadata does not establish verified
65
90
  invocation context; that verification remains the runtime's responsibility.
66
91
 
67
- The [Wire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
92
+ The [AddressedWire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
68
93
  and [profile boundary](https://github.com/Bitspark/bitwire/blob/main/docs/wire/profile.md)
69
94
  define the behavior. Runtime protocol checks verify only the presence of methods;
70
95
  they cannot establish conformance.
@@ -111,6 +136,6 @@ the workflow from `main`. Tokens are passed only to publication steps. Revoke th
111
136
  bootstrap token after trusted publishing is configured. The workflow verifies the
112
137
  packaged artifact before upload and installs the published PyPI package afterward.
113
138
 
114
- The binding is adapted from Nightseam's Python Wire declarations at commit
139
+ The binding is adapted from Nightseam's Python AddressedWire declarations at commit
115
140
  [`1c63f1c4`](https://github.com/Bitspark/nightseam/blob/1c63f1c4d7e4b5987d4bd32e294177645c92ed8f/duplex/py/nightseam/duplex/wire.py)
116
141
  under Apache-2.0. See the included `LICENSE` and `NOTICE` files.
@@ -1,15 +1,40 @@
1
1
  # Bitwire for Python
2
2
 
3
- `bitspark-bitwire` provides the shared relative-path Wire contract as the Python
3
+ **Source contract: 0.3.0; publication pending.** `Wire` is the addressless
4
+ primitive `send(message)`. `WireTree = DeixisNode<Wire>` provides the complete
5
+ finite, acyclic structure: own value, complete byte-keyed children, partial
6
+ selection and decomposition. Keys are exact arbitrary bytes, including empty
7
+ and non-UTF-8 keys. Empty path selects self; a missing edge is distinct from a
8
+ present refusing primitive. Recomposition preserves own and child identities.
9
+
10
+ This is symmetric with Bitstore's `Data.read()` primitive and
11
+ `DataTree = DeixisNode<Data>`. Derived sending selects the node and invokes its
12
+ own Wire. Construction and derived operators belong in bitruntime; these
13
+ packages publish declarations and criteria, not a production tree runtime.
14
+ See [decision 0012](https://github.com/Bitspark/bitwire/blob/main/docs/decisions/0012-explicit-data-and-wire-trees.md)
15
+ and the [migration guide](https://github.com/Bitspark/bitwire/blob/main/docs/migration-0.3.md).
16
+
17
+ `AddressedWire` explicitly names the former `Wire.send(path, message)` surface.
18
+ It is not a full WireTree. `Endpoint` extends AddressedWire, and return
19
+ capabilities retain AddressedWire so the existing response/lifecycle path
20
+ space, local identity, received context and closure rules remain intact.
21
+ Carrier paths remain exact Unicode-scalar strings under unchanged `bitwire/1`;
22
+ they do not imply support for arbitrary tree byte keys on that carrier.
23
+
24
+ The following addressed-carrier examples use the **0.3 source names**. Older
25
+ 0.2.0 artifacts used `Wire` for the addressed interface; their release evidence
26
+ does not validate the renamed declarations or full structural trees.
27
+
28
+ `bitspark-bitwire` provides the shared relative-path AddressedWire contract as the Python
4
29
  module `bitwire`. It requires Python 3.11 or newer and has no runtime dependencies.
5
30
  The package contains typed declarations and supporting values; endpoint runtimes,
6
31
  carriers, codecs and routing helpers belong to implementations.
7
32
 
8
33
  ```python
9
- from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
34
+ from bitwire import Endpoint, Message, Receiver, ReturnAddress, AddressedWire
10
35
 
11
36
 
12
- def call(endpoint: Wire, replies: Wire) -> None:
37
+ def call(endpoint: AddressedWire, replies: AddressedWire) -> None:
13
38
  endpoint.send(
14
39
  ["cells", "get"],
15
40
  Message(
@@ -26,7 +51,7 @@ def listen(endpoint: Endpoint):
26
51
  return endpoint.receive(Receiver(message=receive))
27
52
  ```
28
53
 
29
- `Wire` is a send-only structural `Protocol`; implementations do not need to inherit
54
+ `AddressedWire` is a send-only structural `Protocol`; implementations do not need to inherit
30
55
  from it. `send` accepts or refuses synchronously; the implementation schedules
31
56
  delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
32
57
  attachment and closure authority separate from send access. One receive attachment
@@ -36,7 +61,7 @@ Path matching belongs to routing compositions, not to these interfaces.
36
61
  Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
37
62
  Receiver paths are relative to the attached endpoint.
38
63
 
39
- `ReturnAddress` uses object identity, including when its Wire cannot be compared
64
+ `ReturnAddress` uses object identity, including when its AddressedWire cannot be compared
40
65
  or hashed. Preserve this object during routing. The return address is local
41
66
  capability data and must never be serialized into the profile envelope. Profile
42
67
  payloads must follow the JSON profile despite their Python `object` annotation.
@@ -44,7 +69,7 @@ Runtime-owned received context associated with the return identity must survive
44
69
  routing. Application-supplied context or metadata does not establish verified
45
70
  invocation context; that verification remains the runtime's responsibility.
46
71
 
47
- The [Wire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
72
+ The [AddressedWire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
48
73
  and [profile boundary](https://github.com/Bitspark/bitwire/blob/main/docs/wire/profile.md)
49
74
  define the behavior. Runtime protocol checks verify only the presence of methods;
50
75
  they cannot establish conformance.
@@ -91,6 +116,6 @@ the workflow from `main`. Tokens are passed only to publication steps. Revoke th
91
116
  bootstrap token after trusted publishing is configured. The workflow verifies the
92
117
  packaged artifact before upload and installs the published PyPI package afterward.
93
118
 
94
- The binding is adapted from Nightseam's Python Wire declarations at commit
119
+ The binding is adapted from Nightseam's Python AddressedWire declarations at commit
95
120
  [`1c63f1c4`](https://github.com/Bitspark/nightseam/blob/1c63f1c4d7e4b5987d4bd32e294177645c92ed8f/duplex/py/nightseam/duplex/wire.py)
96
121
  under Apache-2.0. See the included `LICENSE` and `NOTICE` files.
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bitspark-bitwire"
7
- version = "0.2.0"
8
- description = "The shared relative-path Wire contract for Python"
7
+ version = "0.3.0"
8
+ description = "Addressless Wire primitives, full WireTree structure and addressed carrier contracts for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitspark-bitwire
3
- Version: 0.2.0
4
- Summary: The shared relative-path Wire contract for Python
3
+ Version: 0.3.0
4
+ Summary: Addressless Wire primitives, full WireTree structure and addressed carrier contracts for Python
5
5
  Author: Bitspark
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/Bitspark/bitwire
@@ -20,16 +20,41 @@ Dynamic: license-file
20
20
 
21
21
  # Bitwire for Python
22
22
 
23
- `bitspark-bitwire` provides the shared relative-path Wire contract as the Python
23
+ **Source contract: 0.3.0; publication pending.** `Wire` is the addressless
24
+ primitive `send(message)`. `WireTree = DeixisNode<Wire>` provides the complete
25
+ finite, acyclic structure: own value, complete byte-keyed children, partial
26
+ selection and decomposition. Keys are exact arbitrary bytes, including empty
27
+ and non-UTF-8 keys. Empty path selects self; a missing edge is distinct from a
28
+ present refusing primitive. Recomposition preserves own and child identities.
29
+
30
+ This is symmetric with Bitstore's `Data.read()` primitive and
31
+ `DataTree = DeixisNode<Data>`. Derived sending selects the node and invokes its
32
+ own Wire. Construction and derived operators belong in bitruntime; these
33
+ packages publish declarations and criteria, not a production tree runtime.
34
+ See [decision 0012](https://github.com/Bitspark/bitwire/blob/main/docs/decisions/0012-explicit-data-and-wire-trees.md)
35
+ and the [migration guide](https://github.com/Bitspark/bitwire/blob/main/docs/migration-0.3.md).
36
+
37
+ `AddressedWire` explicitly names the former `Wire.send(path, message)` surface.
38
+ It is not a full WireTree. `Endpoint` extends AddressedWire, and return
39
+ capabilities retain AddressedWire so the existing response/lifecycle path
40
+ space, local identity, received context and closure rules remain intact.
41
+ Carrier paths remain exact Unicode-scalar strings under unchanged `bitwire/1`;
42
+ they do not imply support for arbitrary tree byte keys on that carrier.
43
+
44
+ The following addressed-carrier examples use the **0.3 source names**. Older
45
+ 0.2.0 artifacts used `Wire` for the addressed interface; their release evidence
46
+ does not validate the renamed declarations or full structural trees.
47
+
48
+ `bitspark-bitwire` provides the shared relative-path AddressedWire contract as the Python
24
49
  module `bitwire`. It requires Python 3.11 or newer and has no runtime dependencies.
25
50
  The package contains typed declarations and supporting values; endpoint runtimes,
26
51
  carriers, codecs and routing helpers belong to implementations.
27
52
 
28
53
  ```python
29
- from bitwire import Endpoint, Message, Receiver, ReturnAddress, Wire
54
+ from bitwire import Endpoint, Message, Receiver, ReturnAddress, AddressedWire
30
55
 
31
56
 
32
- def call(endpoint: Wire, replies: Wire) -> None:
57
+ def call(endpoint: AddressedWire, replies: AddressedWire) -> None:
33
58
  endpoint.send(
34
59
  ["cells", "get"],
35
60
  Message(
@@ -46,7 +71,7 @@ def listen(endpoint: Endpoint):
46
71
  return endpoint.receive(Receiver(message=receive))
47
72
  ```
48
73
 
49
- `Wire` is a send-only structural `Protocol`; implementations do not need to inherit
74
+ `AddressedWire` is a send-only structural `Protocol`; implementations do not need to inherit
50
75
  from it. `send` accepts or refuses synchronously; the implementation schedules
51
76
  delivery. `Endpoint` extends it with `receive(receiver)` and `close`, keeping
52
77
  attachment and closure authority separate from send access. One receive attachment
@@ -56,7 +81,7 @@ Path matching belongs to routing compositions, not to these interfaces.
56
81
  Paths are sequences of opaque Unicode-scalar strings and retain empty segments.
57
82
  Receiver paths are relative to the attached endpoint.
58
83
 
59
- `ReturnAddress` uses object identity, including when its Wire cannot be compared
84
+ `ReturnAddress` uses object identity, including when its AddressedWire cannot be compared
60
85
  or hashed. Preserve this object during routing. The return address is local
61
86
  capability data and must never be serialized into the profile envelope. Profile
62
87
  payloads must follow the JSON profile despite their Python `object` annotation.
@@ -64,7 +89,7 @@ Runtime-owned received context associated with the return identity must survive
64
89
  routing. Application-supplied context or metadata does not establish verified
65
90
  invocation context; that verification remains the runtime's responsibility.
66
91
 
67
- The [Wire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
92
+ The [AddressedWire contract](https://github.com/Bitspark/bitwire/blob/main/docs/wire/contract.md)
68
93
  and [profile boundary](https://github.com/Bitspark/bitwire/blob/main/docs/wire/profile.md)
69
94
  define the behavior. Runtime protocol checks verify only the presence of methods;
70
95
  they cannot establish conformance.
@@ -111,6 +136,6 @@ the workflow from `main`. Tokens are passed only to publication steps. Revoke th
111
136
  bootstrap token after trusted publishing is configured. The workflow verifies the
112
137
  packaged artifact before upload and installs the published PyPI package afterward.
113
138
 
114
- The binding is adapted from Nightseam's Python Wire declarations at commit
139
+ The binding is adapted from Nightseam's Python AddressedWire declarations at commit
115
140
  [`1c63f1c4`](https://github.com/Bitspark/nightseam/blob/1c63f1c4d7e4b5987d4bd32e294177645c92ed8f/duplex/py/nightseam/duplex/wire.py)
116
141
  under Apache-2.0. See the included `LICENSE` and `NOTICE` files.
@@ -12,4 +12,5 @@ src/bitspark_bitwire.egg-info/top_level.txt
12
12
  src/bitwire/__init__.py
13
13
  src/bitwire/py.typed
14
14
  tests/test_consumer.py
15
+ tests/test_tree.py
15
16
  tests/typecheck.py
@@ -1,4 +1,4 @@
1
- """The shared relative-path Wire contract, without a runtime dependency.
1
+ """Addressless Wire, complete WireTree structure, and addressed compatibility.
2
2
 
3
3
  Adapted from Nightseam's duplex/py/nightseam/duplex/wire.py at
4
4
  1c63f1c4d7e4b5987d4bd32e294177645c92ed8f under Apache-2.0.
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  from collections.abc import Awaitable, Callable, Mapping, Sequence
11
11
  from dataclasses import dataclass
12
- from typing import Literal, NotRequired, Protocol, TypedDict, runtime_checkable
12
+ from typing import Literal, NotRequired, Protocol, TypeAlias, TypeVar, TypedDict, runtime_checkable
13
13
 
14
14
  __all__ = [
15
15
  "Path",
@@ -24,12 +24,21 @@ __all__ = [
24
24
  "ReturnAddress",
25
25
  "Message",
26
26
  "Receiver",
27
+ "AddressedWire",
27
28
  "Wire",
29
+ "Key",
30
+ "TreePath",
31
+ "DeixisNode",
32
+ "WireTree",
28
33
  "Endpoint",
29
34
  ]
30
35
 
31
36
  # Opaque Unicode-scalar segments: [] != [""] != ["a/b"] != ["a", "b"].
32
37
  Path = Sequence[str]
38
+ # Structural keys admit every byte string, without Unicode conversion.
39
+ Key = bytes
40
+ TreePath = Sequence[Key]
41
+ T_co = TypeVar("T_co", covariant=True)
33
42
  ProfileKind = Literal["request", "response", "event", "cancel"]
34
43
 
35
44
 
@@ -88,14 +97,14 @@ ProfileFrame = RequestFrame | ResultFrame | ErrorFrame | EventFrame | CancelFram
88
97
  class ReturnAddress:
89
98
  """Local capability with identity equality, never a network-envelope member.
90
99
 
91
- Different addresses stay distinct even when they hold the same Wire. The
100
+ Different addresses stay distinct even when they hold the same AddressedWire. The
92
101
  wrapped implementation need not support equality or hashing. Routing must
93
102
  preserve this address object, rather than construct an equivalent wrapper.
94
103
  Runtime-owned received context associated with this identity must survive
95
104
  routing; application payloads are not evidence of verified context.
96
105
  """
97
106
 
98
- wire: Wire
107
+ wire: AddressedWire
99
108
 
100
109
 
101
110
  @dataclass(frozen=True)
@@ -123,7 +132,40 @@ class Receiver:
123
132
 
124
133
  @runtime_checkable
125
134
  class Wire(Protocol):
126
- """Send-only access with synchronous admission and asynchronous dispatch.
135
+ """Addressless send access: admission or refusal, never handler completion.
136
+
137
+ No path, receive attachment or closure authority belongs to this primitive.
138
+ Tree selection followed by own().send(message) supplies structured interaction.
139
+ """
140
+
141
+ def send(self, message: Message) -> None: ...
142
+
143
+
144
+ class DeixisNode(Protocol[T_co]):
145
+ """A complete finite acyclic structure with an own value at every node.
146
+
147
+ Children contain every unique exact byte key, including empty keys. An empty
148
+ path selects this node; a missing child returns None, never the parent's own
149
+ value. Decomposition retains the same own value and complete children.
150
+ Implementations preserve structure and payload identity. Constructors and
151
+ derived operations belong to runtimes, not this declaration package.
152
+ """
153
+
154
+ def own(self) -> T_co: ...
155
+
156
+ def children(self) -> Sequence[tuple[Key, DeixisNode[T_co]]]: ...
157
+
158
+ def at(self, path: TreePath) -> DeixisNode[T_co] | None: ...
159
+
160
+ def decompose(self) -> tuple[T_co, Sequence[tuple[Key, DeixisNode[T_co]]]]: ...
161
+
162
+
163
+ WireTree: TypeAlias = DeixisNode[Wire]
164
+
165
+
166
+ @runtime_checkable
167
+ class AddressedWire(Protocol):
168
+ """Compatibility access for the unchanged addressed bitwire/1 profile.
127
169
 
128
170
  send returns on acceptance or raises on refusal, without running destination
129
171
  application code on the sender's stack. It grants no receiving or closure
@@ -134,7 +176,7 @@ class Wire(Protocol):
134
176
 
135
177
 
136
178
  @runtime_checkable
137
- class Endpoint(Wire, Protocol):
179
+ class Endpoint(AddressedWire, Protocol):
138
180
  """Owning endpoint access with one receive attachment and endpoint closure.
139
181
 
140
182
  receive refuses a second active attachment or a closed endpoint and returns
@@ -7,7 +7,7 @@ import inspect
7
7
  import unittest
8
8
  from collections.abc import Callable
9
9
 
10
- from bitwire import Endpoint, Message, Path, Receiver, ReturnAddress, Wire
10
+ from bitwire import Endpoint, Message, Path, Receiver, ReturnAddress, AddressedWire
11
11
 
12
12
 
13
13
  class RecordingEndpoint:
@@ -20,7 +20,7 @@ class RecordingEndpoint:
20
20
  self.ending: tuple[int, str] | None = None
21
21
 
22
22
  def __eq__(self, other: object) -> bool:
23
- raise AssertionError("ReturnAddress must not compare the wrapped Wire")
23
+ raise AssertionError("ReturnAddress must not compare the wrapped AddressedWire")
24
24
 
25
25
  def send(self, path: Path, message: Message) -> None:
26
26
  self.sent.append((tuple(path), message))
@@ -55,13 +55,13 @@ class RecordingEndpoint:
55
55
  class ConsumerTests(unittest.TestCase):
56
56
  def test_structural_consumer_keeps_path_frame_and_return_object(self) -> None:
57
57
  endpoint = RecordingEndpoint()
58
- wire: Wire = endpoint
58
+ wire: AddressedWire = endpoint
59
59
  replies = ReturnAddress(RecordingEndpoint())
60
60
  request = Message(
61
61
  {"version": 1, "kind": "request", "id": "r1", "params": {"count": 2**80}},
62
62
  replies,
63
63
  )
64
- self.assertIsInstance(endpoint, Wire)
64
+ self.assertIsInstance(endpoint, AddressedWire)
65
65
  for path in ([], [""], ["a/b"], ["a", "b"], ["\u03bb", "\U0001f30d"]):
66
66
  wire.send(path, request)
67
67
 
@@ -138,8 +138,8 @@ class ConsumerTests(unittest.TestCase):
138
138
  def send(self, path: Path, message: Message) -> None:
139
139
  pass
140
140
 
141
- access: Wire = Access()
142
- self.assertIsInstance(access, Wire)
141
+ access: AddressedWire = Access()
142
+ self.assertIsInstance(access, AddressedWire)
143
143
  self.assertNotIsInstance(access, Endpoint)
144
144
  self.assertIs(ReturnAddress(access).wire, access)
145
145
 
@@ -0,0 +1,85 @@
1
+ """Independent finite structural fixture; no production tree implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Generic, TypeVar
8
+ import unittest
9
+
10
+ from bitwire import DeixisNode, Key, Message, TreePath, Wire, WireTree
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Node(Generic[T]):
17
+ value: T
18
+ entries: tuple[tuple[Key, DeixisNode[T]], ...] = ()
19
+
20
+ def own(self) -> T:
21
+ return self.value
22
+
23
+ def children(self) -> Sequence[tuple[Key, DeixisNode[T]]]:
24
+ return self.entries
25
+
26
+ def at(self, path: TreePath) -> DeixisNode[T] | None:
27
+ node: DeixisNode[T] = self
28
+ for key in path:
29
+ child = next((child for name, child in node.children() if name == key), None)
30
+ if child is None:
31
+ return None
32
+ node = child
33
+ return node
34
+
35
+ def decompose(self) -> tuple[T, Sequence[tuple[Key, DeixisNode[T]]]]:
36
+ return self.value, self.entries
37
+
38
+
39
+ class RefusingWire:
40
+ def send(self, message: Message) -> None:
41
+ raise RuntimeError("refused")
42
+
43
+
44
+ class RecordingWire:
45
+ def __init__(self) -> None:
46
+ self.messages: list[Message] = []
47
+
48
+ def send(self, message: Message) -> None:
49
+ self.messages.append(message)
50
+
51
+
52
+ class TreeTests(unittest.TestCase):
53
+ def test_complete_structure_preserves_binary_empty_and_missing_keys(self) -> None:
54
+ child: WireTree = Node[Wire](RefusingWire())
55
+ binary: WireTree = Node[Wire](RefusingWire())
56
+ tree: WireTree = Node[Wire](RefusingWire(), ((b"", child), (b"\xff\x00", binary)))
57
+ self.assertIs(tree.at([]), tree)
58
+ self.assertIs(tree.at([b""]), child)
59
+ self.assertIs(tree.at([b"\xff\x00"]), binary)
60
+ self.assertIsNone(tree.at([b"missing"]))
61
+ self.assertIsNone(tree.at([b"", b""]))
62
+ self.assertEqual([key for key, _ in tree.children()], [b"", b"\xff\x00"])
63
+ value, children = tree.decompose()
64
+ self.assertIs(value, tree.own())
65
+ rebuilt: WireTree = Node[Wire](value, tuple(children))
66
+ self.assertIs(rebuilt.at([b""]), child)
67
+ with self.assertRaisesRegex(RuntimeError, "refused"):
68
+ child.own().send(Message({"version": 1, "kind": "event", "data": None}))
69
+ # A present refusing leaf is still observable, unlike a missing child.
70
+ self.assertEqual(child.children(), ())
71
+
72
+ def test_selected_send_is_own_send_and_retains_message_identity(self) -> None:
73
+ root = RecordingWire()
74
+ destination = RecordingWire()
75
+ tree: WireTree = Node[Wire](root, ((b"route", Node[Wire](destination)),))
76
+ message = Message({"version": 1, "kind": "event", "data": {"value": 1}})
77
+ selected = tree.at([b"route"])
78
+ assert selected is not None
79
+ selected.own().send(message)
80
+ self.assertEqual(root.messages, [])
81
+ self.assertIs(destination.messages[0], message)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ unittest.main()
@@ -15,14 +15,16 @@ from bitwire import (
15
15
  RequestFrame,
16
16
  ResultFrame,
17
17
  ReturnAddress,
18
+ AddressedWire,
18
19
  Wire,
20
+ WireTree,
19
21
  )
20
22
 
21
23
 
22
- def consume(wire: Wire, endpoint: Endpoint, frame: ProfileFrame) -> None:
24
+ def consume(wire: AddressedWire, endpoint: Endpoint, frame: ProfileFrame) -> None:
23
25
  address = ReturnAddress(wire)
24
26
  wire.send(["scope", ""], Message(frame, address))
25
- assert_type(address.wire, Wire)
27
+ assert_type(address.wire, AddressedWire)
26
28
  kind: ProfileKind = frame["kind"]
27
29
  assert_type(kind, ProfileKind)
28
30
  if frame["kind"] == "request":
@@ -37,6 +39,22 @@ def consume(wire: Wire, endpoint: Endpoint, frame: ProfileFrame) -> None:
37
39
  wire.send([42], Message(frame)) # type: ignore[list-item]
38
40
 
39
41
 
42
+ def consume_tree(tree: WireTree, primitive: Wire, message: Message) -> None:
43
+ primitive.send(message)
44
+ assert_type(tree.own(), Wire)
45
+ selected = tree.at([b"", b"\xff\x00"])
46
+ if selected is not None:
47
+ selected.own().send(message)
48
+ own, children = tree.decompose()
49
+ assert_type(own, Wire)
50
+ for key, child in children:
51
+ assert_type(key, bytes)
52
+ assert_type(child, WireTree)
53
+ primitive.send([], message) # type: ignore[call-arg,arg-type]
54
+ tree.at(["text"]) # type: ignore[list-item]
55
+ ReturnAddress(primitive) # type: ignore[arg-type]
56
+
57
+
40
58
  request: RequestFrame = {"version": 1, "kind": "request", "id": "1", "params": {}}
41
59
  result: ResultFrame = {"version": 1, "kind": "response", "id": "1", "result": None}
42
60
  error: ErrorFrame = {"version": 1, "kind": "response", "id": "1", "error": {"code": "no", "message": "No"}}