magpie-bridge 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Albert Moky
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,133 @@
1
+ Metadata-Version: 2.1
2
+ Name: magpie_bridge
3
+ Version: 0.1.0
4
+ Summary: Reliable UDP Relay Network protocol library
5
+ Author: Albert Moky
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/moky/magpie-bridge
8
+ Project-URL: Repository, https://github.com/moky/magpie-bridge
9
+ Project-URL: Issues, https://github.com/moky/magpie-bridge/issues
10
+ Keywords: udp,relay,network,protocol,magpie-bridge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Networking
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+
28
+ # magpie-bridge (Python)
29
+
30
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
31
+ [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/magpie-bridge/)
32
+
33
+ Reliable UDP Relay Network — wire protocol library for Python.
34
+
35
+ This package implements the Magpie Bridge protocol codec: packet packing,
36
+ unpacking, validation, message segmentation, and acknowledgement helpers.
37
+
38
+ > [中文文档](README.zh-CN.md)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install magpie-bridge
44
+ ```
45
+
46
+ Or install from source:
47
+
48
+ ```bash
49
+ git clone https://github.com/moky/magpie-bridge.git
50
+ cd magpie-bridge/python
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
58
+
59
+ # Build and send a small packet
60
+ pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
61
+ data = pkt.pack() # bytes ready for UDP send
62
+
63
+ # Parse a received packet
64
+ received = Packet.unpack(data)
65
+ print(received.body) # b"hello"
66
+
67
+ # Split a large message into segments
68
+ message = b"x" * 5000
69
+ segments = split_message(message, sn=2, source_bid=7, target_bid=42)
70
+ for seg in segments:
71
+ send(seg.pack())
72
+
73
+ # Build an acknowledgement
74
+ ack = build_ack(received)
75
+ send(ack.pack()) # body=b"OK", bids swapped
76
+ ```
77
+
78
+ ## Protocol Overview
79
+
80
+ | Field | Size | Description |
81
+ | ----------------- | ------ | ------------------------------------ |
82
+ | Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
83
+ | Type | 1 B | packet type + parameter width |
84
+ | Head Length | 1 B | 20 / 22 / 24 / 28 |
85
+ | Body Length | 2 B | body size in octets |
86
+ | Target Bridge ID | 4 B | destination bid |
87
+ | Source Bridge ID | 4 B | originator bid |
88
+ | Serial Number | 4 B | message identity |
89
+ | index / count | 0/2/4/8 B | segment ordinal / total segments |
90
+ | Body | 0..1200 B | application payload |
91
+
92
+ - Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
93
+ - Maximum body size: **1200** octets.
94
+ - See `../docs/design-protocol.md` for the full specification.
95
+
96
+ ## API Reference
97
+
98
+ ### `Packet`
99
+
100
+ ```python
101
+ Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
102
+ ```
103
+
104
+ - `pack() -> bytes` — serialize.
105
+ - `Packet.unpack(data: bytes) -> Packet` — parse.
106
+ - `validate() -> bool` — check whether the packet is well-formed.
107
+ - `message_id -> int` — deduplication key (`sn` for data packets,
108
+ `(sn << (type & 0x0F)) | index` for ACKs).
109
+ - `head_length`, `param_width`, `is_ack` — derived properties.
110
+
111
+ ### Helpers
112
+
113
+ - `select_type(message_size) -> int` — choose Type by message size.
114
+ - `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
115
+ - `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
116
+ - `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
117
+ control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
118
+
119
+ ### Exceptions
120
+
121
+ - `ProtocolError` — raised on invalid packets or parsing failures.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ cd python
127
+ pip install -e ".[dev]" # or: pip install pytest
128
+ pytest
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,106 @@
1
+ # magpie-bridge (Python)
2
+
3
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
+ [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/magpie-bridge/)
5
+
6
+ Reliable UDP Relay Network — wire protocol library for Python.
7
+
8
+ This package implements the Magpie Bridge protocol codec: packet packing,
9
+ unpacking, validation, message segmentation, and acknowledgement helpers.
10
+
11
+ > [中文文档](README.zh-CN.md)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install magpie-bridge
17
+ ```
18
+
19
+ Or install from source:
20
+
21
+ ```bash
22
+ git clone https://github.com/moky/magpie-bridge.git
23
+ cd magpie-bridge/python
24
+ pip install -e .
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```python
30
+ from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
31
+
32
+ # Build and send a small packet
33
+ pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
34
+ data = pkt.pack() # bytes ready for UDP send
35
+
36
+ # Parse a received packet
37
+ received = Packet.unpack(data)
38
+ print(received.body) # b"hello"
39
+
40
+ # Split a large message into segments
41
+ message = b"x" * 5000
42
+ segments = split_message(message, sn=2, source_bid=7, target_bid=42)
43
+ for seg in segments:
44
+ send(seg.pack())
45
+
46
+ # Build an acknowledgement
47
+ ack = build_ack(received)
48
+ send(ack.pack()) # body=b"OK", bids swapped
49
+ ```
50
+
51
+ ## Protocol Overview
52
+
53
+ | Field | Size | Description |
54
+ | ----------------- | ------ | ------------------------------------ |
55
+ | Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
56
+ | Type | 1 B | packet type + parameter width |
57
+ | Head Length | 1 B | 20 / 22 / 24 / 28 |
58
+ | Body Length | 2 B | body size in octets |
59
+ | Target Bridge ID | 4 B | destination bid |
60
+ | Source Bridge ID | 4 B | originator bid |
61
+ | Serial Number | 4 B | message identity |
62
+ | index / count | 0/2/4/8 B | segment ordinal / total segments |
63
+ | Body | 0..1200 B | application payload |
64
+
65
+ - Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
66
+ - Maximum body size: **1200** octets.
67
+ - See `../docs/design-protocol.md` for the full specification.
68
+
69
+ ## API Reference
70
+
71
+ ### `Packet`
72
+
73
+ ```python
74
+ Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
75
+ ```
76
+
77
+ - `pack() -> bytes` — serialize.
78
+ - `Packet.unpack(data: bytes) -> Packet` — parse.
79
+ - `validate() -> bool` — check whether the packet is well-formed.
80
+ - `message_id -> int` — deduplication key (`sn` for data packets,
81
+ `(sn << (type & 0x0F)) | index` for ACKs).
82
+ - `head_length`, `param_width`, `is_ack` — derived properties.
83
+
84
+ ### Helpers
85
+
86
+ - `select_type(message_size) -> int` — choose Type by message size.
87
+ - `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
88
+ - `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
89
+ - `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
90
+ control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
91
+
92
+ ### Exceptions
93
+
94
+ - `ProtocolError` — raised on invalid packets or parsing failures.
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ cd python
100
+ pip install -e ".[dev]" # or: pip install pytest
101
+ pytest
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "magpie_bridge"
7
+ version = "0.1.0"
8
+ description = "Reliable UDP Relay Network protocol library"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Albert Moky"},
13
+ ]
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Internet",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: System :: Networking",
29
+ ]
30
+ keywords = ["udp", "relay", "network", "protocol", "magpie-bridge"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/moky/magpie-bridge"
34
+ Repository = "https://github.com/moky/magpie-bridge"
35
+ Issues = "https://github.com/moky/magpie-bridge/issues"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+ """Magpie Bridge protocol library.
2
+
3
+ Reliable UDP Relay Network wire protocol codec.
4
+ """
5
+
6
+ from .constants import (
7
+ ACK_BIT,
8
+ HEAD_FIXED,
9
+ MAGIC,
10
+ MAX_BODY,
11
+ MSS,
12
+ TYPE_ACK_LARGE,
13
+ TYPE_ACK_MEDIUM,
14
+ TYPE_ACK_MINI,
15
+ TYPE_ACK_SMALL,
16
+ TYPE_LARGE,
17
+ TYPE_MEDIUM,
18
+ TYPE_MINI,
19
+ TYPE_SMALL,
20
+ )
21
+ from .errors import ProtocolError
22
+ from .protocol import Packet, build_ack, build_control, select_type, split_message
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "ACK_BIT",
29
+ "HEAD_FIXED",
30
+ "MAGIC",
31
+ "MAX_BODY",
32
+ "MSS",
33
+ "Packet",
34
+ "ProtocolError",
35
+ "TYPE_ACK_LARGE",
36
+ "TYPE_ACK_MEDIUM",
37
+ "TYPE_ACK_MINI",
38
+ "TYPE_ACK_SMALL",
39
+ "TYPE_LARGE",
40
+ "TYPE_MEDIUM",
41
+ "TYPE_MINI",
42
+ "TYPE_SMALL",
43
+ "build_ack",
44
+ "build_control",
45
+ "select_type",
46
+ "split_message",
47
+ ]
@@ -0,0 +1,34 @@
1
+ """Protocol constants for Magpie Bridge."""
2
+
3
+ # Magic code: 'M', 'B', 0x00, 0x01 (Magpie Bridge v1)
4
+ MAGIC = b"MB\x00\x01"
5
+
6
+ # Fixed head size (octets), before the optional parameter area
7
+ HEAD_FIXED = 20
8
+
9
+ # Maximum UDP payload size (octets), dual-stack safe: 1280 - 40 - 8
10
+ MSS = 1232
11
+
12
+ # Maximum body size (octets): MSS - 32 (reserved head budget)
13
+ MAX_BODY = 1200
14
+
15
+ # High bit of Type marks an acknowledgement (reply) packet
16
+ ACK_BIT = 0x80
17
+
18
+ # Data packet types (low nibble = parameter width in octets)
19
+ TYPE_MINI = 0x00 # no parameter area, miniature single packet
20
+ TYPE_SMALL = 0x01 # 1-octet parameters, 1 <= K < 256
21
+ TYPE_MEDIUM = 0x02 # 2-octet parameters, 256 <= K < 65536
22
+ TYPE_LARGE = 0x04 # 4-octet parameters, 65536 <= K < 2^32
23
+
24
+ # Acknowledgement packet types
25
+ TYPE_ACK_MINI = 0x80
26
+ TYPE_ACK_SMALL = 0x81
27
+ TYPE_ACK_MEDIUM = 0x82
28
+ TYPE_ACK_LARGE = 0x84
29
+
30
+ # Mapping from low nibble to parameter width (octets)
31
+ PARAM_WIDTH = {0: 0, 1: 1, 2: 2, 4: 4}
32
+
33
+ # Mapping from low nibble to total head length (octets)
34
+ HEAD_LENGTH = {0: 20, 1: 22, 2: 24, 4: 28}
@@ -0,0 +1,5 @@
1
+ """Protocol exceptions for Magpie Bridge."""
2
+
3
+
4
+ class ProtocolError(Exception):
5
+ """Raised when a packet fails validation or cannot be parsed."""
@@ -0,0 +1,225 @@
1
+ """Magpie Bridge protocol packet codec.
2
+
3
+ Implements packing, unpacking, validation and helpers for the wire protocol
4
+ defined in docs/design-protocol.md.
5
+ """
6
+
7
+ import struct
8
+ from typing import List, Optional
9
+
10
+ from .constants import (
11
+ ACK_BIT,
12
+ HEAD_FIXED,
13
+ MAGIC,
14
+ MAX_BODY,
15
+ MSS,
16
+ PARAM_WIDTH,
17
+ TYPE_LARGE,
18
+ TYPE_MEDIUM,
19
+ TYPE_MINI,
20
+ TYPE_SMALL,
21
+ )
22
+ from .errors import ProtocolError
23
+
24
+
25
+ class Packet:
26
+ """A single Magpie Bridge protocol frame (head + body)."""
27
+
28
+ def __init__(
29
+ self,
30
+ type: int = TYPE_MINI,
31
+ target_bid: int = 0,
32
+ source_bid: int = 0,
33
+ sn: int = 0,
34
+ index: Optional[int] = None,
35
+ count: Optional[int] = None,
36
+ body: bytes = b"",
37
+ ) -> None:
38
+ self.type = type
39
+ self.target_bid = target_bid
40
+ self.source_bid = source_bid
41
+ self.sn = sn
42
+ self.index = index
43
+ self.count = count
44
+ self.body = bytes(body)
45
+
46
+ # -- derived properties ------------------------------------------------
47
+
48
+ @property
49
+ def param_width(self) -> int:
50
+ """Width (octets) of each parameter (index, count)."""
51
+ low = self.type & 0x0F
52
+ if low not in PARAM_WIDTH:
53
+ raise ProtocolError(f"invalid type low nibble: {low}")
54
+ return PARAM_WIDTH[low]
55
+
56
+ @property
57
+ def head_length(self) -> int:
58
+ """Total head length including the parameter area."""
59
+ return HEAD_FIXED + 2 * self.param_width
60
+
61
+ @property
62
+ def is_ack(self) -> bool:
63
+ """True when the high bit of Type is set."""
64
+ return bool(self.type & ACK_BIT)
65
+
66
+ @property
67
+ def message_id(self) -> int:
68
+ """Deduplication key combining sn and index.
69
+
70
+ For data packets the key is sn alone; for acknowledgement packets
71
+ the key is (sn << (type & 0x0F)) | index.
72
+ """
73
+ if self.type & ACK_BIT == 0:
74
+ return self.sn
75
+ return (self.sn << (self.type & 0x0F)) | (self.index or 0)
76
+
77
+ # -- pack / unpack -----------------------------------------------------
78
+
79
+ def pack(self) -> bytes:
80
+ """Serialize the packet to bytes. Raises ProtocolError on invalid state."""
81
+ w = self.param_width
82
+ body = self.body
83
+
84
+ if len(body) > MAX_BODY:
85
+ raise ProtocolError(f"body too large: {len(body)} > {MAX_BODY}")
86
+ if self.head_length + len(body) > MSS:
87
+ raise ProtocolError(
88
+ f"packet too large: {self.head_length + len(body)} > {MSS}"
89
+ )
90
+
91
+ if w:
92
+ if self.index is None or self.count is None:
93
+ raise ProtocolError("index and count are required for segmented packets")
94
+ if not (0 <= self.index < self.count):
95
+ raise ProtocolError(
96
+ f"invalid index/count: {self.index}/{self.count}"
97
+ )
98
+ max_val = (1 << (8 * w)) - 1
99
+ if self.index > max_val or self.count > max_val:
100
+ raise ProtocolError("index/count exceeds parameter width")
101
+
102
+ head = bytearray()
103
+ head += MAGIC
104
+ head.append(self.type & 0xFF)
105
+ head.append(self.head_length & 0xFF)
106
+ head += struct.pack(">H", len(body))
107
+ head += struct.pack(
108
+ ">III",
109
+ self.target_bid & 0xFFFFFFFF,
110
+ self.source_bid & 0xFFFFFFFF,
111
+ self.sn & 0xFFFFFFFF,
112
+ )
113
+ if w:
114
+ head += self.index.to_bytes(w, "big")
115
+ head += self.count.to_bytes(w, "big")
116
+ return bytes(head) + body
117
+
118
+ @classmethod
119
+ def unpack(cls, data: bytes) -> "Packet":
120
+ """Parse bytes into a Packet. Raises ProtocolError on malformed input."""
121
+ if len(data) < HEAD_FIXED:
122
+ raise ProtocolError(f"packet too short: {len(data)} < {HEAD_FIXED}")
123
+ if data[0:4] != MAGIC:
124
+ raise ProtocolError("bad magic code")
125
+
126
+ type_ = data[4]
127
+ head_len = data[5]
128
+ body_len = struct.unpack(">H", data[6:8])[0]
129
+
130
+ if head_len + body_len != len(data):
131
+ raise ProtocolError(
132
+ f"length mismatch: head={head_len} body={body_len} total={len(data)}"
133
+ )
134
+
135
+ low = type_ & 0x0F
136
+ if low not in PARAM_WIDTH:
137
+ raise ProtocolError(f"invalid type low nibble: {low}")
138
+ w = PARAM_WIDTH[low]
139
+ expected_head = HEAD_FIXED + 2 * w
140
+ if head_len != expected_head:
141
+ raise ProtocolError(
142
+ f"head length mismatch: {head_len} != {expected_head}"
143
+ )
144
+
145
+ target, source, sn = struct.unpack(">III", data[8:20])
146
+ index = count = None
147
+ if w:
148
+ index = int.from_bytes(data[20:20 + w], "big")
149
+ count = int.from_bytes(data[20 + w:20 + 2 * w], "big")
150
+
151
+ body = data[head_len:head_len + body_len]
152
+ return cls(type_, target, source, sn, index, count, body)
153
+
154
+ def validate(self) -> bool:
155
+ """Return True if the packet can be packed without error."""
156
+ try:
157
+ self.pack()
158
+ return True
159
+ except ProtocolError:
160
+ return False
161
+
162
+ def __repr__(self) -> str:
163
+ return (
164
+ f"Packet(type=0x{self.type:02x}, target={self.target_bid}, "
165
+ f"source={self.source_bid}, sn={self.sn}, "
166
+ f"index={self.index}, count={self.count}, body={len(self.body)}B)"
167
+ )
168
+
169
+
170
+ # -- helpers ---------------------------------------------------------------
171
+
172
+
173
+ def select_type(message_size: int) -> int:
174
+ """Select the data packet Type for a message of size K (octets)."""
175
+ if message_size <= 0:
176
+ return TYPE_MINI
177
+ if message_size < 256:
178
+ return TYPE_SMALL
179
+ if message_size < 65536:
180
+ return TYPE_MEDIUM
181
+ return TYPE_LARGE
182
+
183
+
184
+ def split_message(
185
+ data: bytes,
186
+ sn: int,
187
+ source_bid: int,
188
+ target_bid: int,
189
+ ) -> List[Packet]:
190
+ """Split a message into one or more packets (max MAX_BODY octets each)."""
191
+ if not data:
192
+ return [Packet(TYPE_MINI, target_bid, source_bid, sn, body=b"")]
193
+
194
+ type_ = select_type(len(data))
195
+ count = (len(data) + MAX_BODY - 1) // MAX_BODY
196
+ packets: List[Packet] = []
197
+ for i in range(count):
198
+ chunk = data[i * MAX_BODY:(i + 1) * MAX_BODY]
199
+ packets.append(
200
+ Packet(type_, target_bid, source_bid, sn, i, count, chunk)
201
+ )
202
+ return packets
203
+
204
+
205
+ def build_ack(packet: Packet) -> Packet:
206
+ """Build a data acknowledgement (body = b'OK') for the given packet."""
207
+ return Packet(
208
+ type=packet.type | ACK_BIT,
209
+ target_bid=packet.source_bid,
210
+ source_bid=packet.target_bid,
211
+ sn=packet.sn,
212
+ index=packet.index,
213
+ count=packet.count,
214
+ body=b"OK",
215
+ )
216
+
217
+
218
+ def build_control(
219
+ body: bytes,
220
+ sn: int,
221
+ source_bid: int = 0,
222
+ target_bid: int = 0,
223
+ ) -> Packet:
224
+ """Build a control packet (SYN/ACK/PING/PONG/FIN) using Type 0 (mini)."""
225
+ return Packet(TYPE_MINI, target_bid, source_bid, sn, body=body)
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.1
2
+ Name: magpie-bridge
3
+ Version: 0.1.0
4
+ Summary: Reliable UDP Relay Network protocol library
5
+ Author: Albert Moky
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/moky/magpie-bridge
8
+ Project-URL: Repository, https://github.com/moky/magpie-bridge
9
+ Project-URL: Issues, https://github.com/moky/magpie-bridge/issues
10
+ Keywords: udp,relay,network,protocol,magpie-bridge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Networking
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+
28
+ # magpie-bridge (Python)
29
+
30
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
31
+ [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/magpie-bridge/)
32
+
33
+ Reliable UDP Relay Network — wire protocol library for Python.
34
+
35
+ This package implements the Magpie Bridge protocol codec: packet packing,
36
+ unpacking, validation, message segmentation, and acknowledgement helpers.
37
+
38
+ > [中文文档](README.zh-CN.md)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install magpie-bridge
44
+ ```
45
+
46
+ Or install from source:
47
+
48
+ ```bash
49
+ git clone https://github.com/moky/magpie-bridge.git
50
+ cd magpie-bridge/python
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
58
+
59
+ # Build and send a small packet
60
+ pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
61
+ data = pkt.pack() # bytes ready for UDP send
62
+
63
+ # Parse a received packet
64
+ received = Packet.unpack(data)
65
+ print(received.body) # b"hello"
66
+
67
+ # Split a large message into segments
68
+ message = b"x" * 5000
69
+ segments = split_message(message, sn=2, source_bid=7, target_bid=42)
70
+ for seg in segments:
71
+ send(seg.pack())
72
+
73
+ # Build an acknowledgement
74
+ ack = build_ack(received)
75
+ send(ack.pack()) # body=b"OK", bids swapped
76
+ ```
77
+
78
+ ## Protocol Overview
79
+
80
+ | Field | Size | Description |
81
+ | ----------------- | ------ | ------------------------------------ |
82
+ | Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
83
+ | Type | 1 B | packet type + parameter width |
84
+ | Head Length | 1 B | 20 / 22 / 24 / 28 |
85
+ | Body Length | 2 B | body size in octets |
86
+ | Target Bridge ID | 4 B | destination bid |
87
+ | Source Bridge ID | 4 B | originator bid |
88
+ | Serial Number | 4 B | message identity |
89
+ | index / count | 0/2/4/8 B | segment ordinal / total segments |
90
+ | Body | 0..1200 B | application payload |
91
+
92
+ - Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
93
+ - Maximum body size: **1200** octets.
94
+ - See `../docs/design-protocol.md` for the full specification.
95
+
96
+ ## API Reference
97
+
98
+ ### `Packet`
99
+
100
+ ```python
101
+ Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
102
+ ```
103
+
104
+ - `pack() -> bytes` — serialize.
105
+ - `Packet.unpack(data: bytes) -> Packet` — parse.
106
+ - `validate() -> bool` — check whether the packet is well-formed.
107
+ - `message_id -> int` — deduplication key (`sn` for data packets,
108
+ `(sn << (type & 0x0F)) | index` for ACKs).
109
+ - `head_length`, `param_width`, `is_ack` — derived properties.
110
+
111
+ ### Helpers
112
+
113
+ - `select_type(message_size) -> int` — choose Type by message size.
114
+ - `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
115
+ - `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
116
+ - `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
117
+ control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
118
+
119
+ ### Exceptions
120
+
121
+ - `ProtocolError` — raised on invalid packets or parsing failures.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ cd python
127
+ pip install -e ".[dev]" # or: pip install pytest
128
+ pytest
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/magpie_bridge/__init__.py
5
+ src/magpie_bridge/constants.py
6
+ src/magpie_bridge/errors.py
7
+ src/magpie_bridge/protocol.py
8
+ src/magpie_bridge.egg-info/PKG-INFO
9
+ src/magpie_bridge.egg-info/SOURCES.txt
10
+ src/magpie_bridge.egg-info/dependency_links.txt
11
+ src/magpie_bridge.egg-info/top_level.txt
12
+ tests/test_protocol.py
@@ -0,0 +1 @@
1
+ magpie_bridge
@@ -0,0 +1,150 @@
1
+ """Tests for the Magpie Bridge protocol codec."""
2
+
3
+ import pytest
4
+
5
+ from magpie_bridge import (
6
+ MAX_BODY,
7
+ MSS,
8
+ Packet,
9
+ ProtocolError,
10
+ TYPE_LARGE,
11
+ TYPE_MEDIUM,
12
+ TYPE_MINI,
13
+ TYPE_SMALL,
14
+ build_ack,
15
+ build_control,
16
+ select_type,
17
+ split_message,
18
+ )
19
+
20
+
21
+ class TestPacketPackUnpack:
22
+ def test_mini_packet_roundtrip(self):
23
+ pkt = Packet(TYPE_MINI, 1, 2, 100, body=b"hello")
24
+ data = pkt.pack()
25
+ assert data[0:4] == b"MB\x00\x01"
26
+ assert len(data) == 20 + 5
27
+ restored = Packet.unpack(data)
28
+ assert restored.type == TYPE_MINI
29
+ assert restored.target_bid == 1
30
+ assert restored.source_bid == 2
31
+ assert restored.sn == 100
32
+ assert restored.index is None
33
+ assert restored.count is None
34
+ assert restored.body == b"hello"
35
+
36
+ def test_segmented_packet_roundtrip(self):
37
+ pkt = Packet(TYPE_SMALL, 10, 20, 5, index=0, count=3, body=b"abc")
38
+ data = pkt.pack()
39
+ assert len(data) == 22 + 3
40
+ restored = Packet.unpack(data)
41
+ assert restored.type == TYPE_SMALL
42
+ assert restored.head_length == 22
43
+ assert restored.index == 0
44
+ assert restored.count == 3
45
+ assert restored.body == b"abc"
46
+
47
+ def test_medium_and_large_widths(self):
48
+ for type_, width in [(TYPE_MEDIUM, 2), (TYPE_LARGE, 4)]:
49
+ pkt = Packet(type_, 1, 2, 7, index=5, count=10, body=b"x")
50
+ data = pkt.pack()
51
+ assert len(data) == 20 + 2 * width + 1
52
+ restored = Packet.unpack(data)
53
+ assert restored.index == 5
54
+ assert restored.count == 10
55
+
56
+ def test_bad_magic_raises(self):
57
+ data = b"XXXX" + b"\x00" * 16
58
+ with pytest.raises(ProtocolError):
59
+ Packet.unpack(data)
60
+
61
+ def test_too_short_raises(self):
62
+ with pytest.raises(ProtocolError):
63
+ Packet.unpack(b"MB\x00\x01")
64
+
65
+ def test_length_mismatch_raises(self):
66
+ pkt = Packet(TYPE_MINI, 1, 2, 1, body=b"ok")
67
+ data = pkt.pack()
68
+ with pytest.raises(ProtocolError):
69
+ Packet.unpack(data + b"extra")
70
+
71
+ def test_body_too_large_raises(self):
72
+ pkt = Packet(TYPE_MINI, 1, 2, 1, body=b"x" * (MAX_BODY + 1))
73
+ with pytest.raises(ProtocolError):
74
+ pkt.pack()
75
+
76
+ def test_invalid_index_raises(self):
77
+ pkt = Packet(TYPE_SMALL, 1, 2, 1, index=5, count=3, body=b"x")
78
+ with pytest.raises(ProtocolError):
79
+ pkt.pack()
80
+
81
+ def test_validate(self):
82
+ good = Packet(TYPE_MINI, 1, 2, 1, body=b"ok")
83
+ bad = Packet(TYPE_SMALL, 1, 2, 1, index=5, count=3, body=b"x")
84
+ assert good.validate() is True
85
+ assert bad.validate() is False
86
+
87
+
88
+ class TestHelpers:
89
+ def test_select_type(self):
90
+ assert select_type(0) == TYPE_MINI
91
+ assert select_type(100) == TYPE_SMALL
92
+ assert select_type(255) == TYPE_SMALL
93
+ assert select_type(256) == TYPE_MEDIUM
94
+ assert select_type(65535) == TYPE_MEDIUM
95
+ assert select_type(65536) == TYPE_LARGE
96
+
97
+ def test_split_message_single(self):
98
+ data = b"hello world"
99
+ packets = split_message(data, 42, 1, 2)
100
+ assert len(packets) == 1
101
+ assert packets[0].count == 1
102
+ assert packets[0].index == 0
103
+ assert packets[0].body == data
104
+
105
+ def test_split_message_empty(self):
106
+ packets = split_message(b"", 1, 1, 2)
107
+ assert len(packets) == 1
108
+ assert packets[0].type == TYPE_MINI
109
+ assert packets[0].body == b""
110
+
111
+ def test_split_message_multiple(self):
112
+ data = b"x" * (MAX_BODY * 2 + 10)
113
+ packets = split_message(data, 7, 1, 2)
114
+ assert len(packets) == 3
115
+ assert packets[0].count == 3
116
+ assert packets[-1].body == b"x" * 10
117
+ for i, pkt in enumerate(packets):
118
+ assert pkt.index == i
119
+ assert pkt.sn == 7
120
+
121
+ def test_build_ack(self):
122
+ pkt = Packet(TYPE_SMALL, 10, 20, 5, index=1, count=3, body=b"abc")
123
+ ack = build_ack(pkt)
124
+ assert ack.type == 0x81
125
+ assert ack.target_bid == 20
126
+ assert ack.source_bid == 10
127
+ assert ack.sn == 5
128
+ assert ack.index == 1
129
+ assert ack.count == 3
130
+ assert ack.body == b"OK"
131
+
132
+ def test_build_control(self):
133
+ pkt = build_control(b"SYN", 1, source_bid=0, target_bid=0)
134
+ assert pkt.type == TYPE_MINI
135
+ assert pkt.body == b"SYN"
136
+ assert pkt.head_length == 20
137
+
138
+ def test_message_id_data_packet(self):
139
+ pkt = Packet(TYPE_SMALL, 1, 2, 100, index=3, count=5, body=b"x")
140
+ assert pkt.message_id == 100
141
+
142
+ def test_message_id_ack_packet(self):
143
+ pkt = Packet(0x81, 1, 2, 100, index=3, count=5, body=b"OK")
144
+ assert pkt.message_id == (100 << 1) | 3
145
+
146
+ def test_max_body_fits_mss(self):
147
+ body = b"y" * MAX_BODY
148
+ pkt = Packet(TYPE_LARGE, 1, 2, 1, index=0, count=1, body=body)
149
+ data = pkt.pack()
150
+ assert len(data) <= MSS