gbp-stack 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.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: gbp-stack
3
+ Version: 0.2.0
4
+ Summary: Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
5
+ Author: Group Protocol Stack contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/F000NKKK/Group-Protocol-Stack
8
+ Project-URL: Repository, https://github.com/F000NKKK/Group-Protocol-Stack
9
+ Project-URL: Documentation, https://github.com/F000NKKK/Group-Protocol-Stack#readme
10
+ Project-URL: Issues, https://github.com/F000NKKK/Group-Protocol-Stack/issues
11
+ Keywords: mls,rfc9420,e2ee,group-messaging,chat,voice,signaling,cryptography,ffi
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Topic :: System :: Networking
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # gbp-stack — Python bindings for the Group Protocol Stack
26
+
27
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE)
28
+
29
+ Python bindings for the [Group Protocol Stack](https://github.com/F000NKKK/Group-Protocol-Stack):
30
+ a layered, end-to-end encrypted group-messaging protocol family built on top
31
+ of [MLS (RFC 9420)](https://www.rfc-editor.org/rfc/rfc9420).
32
+
33
+ This package wraps the native `gbp_stack` shared library through `ctypes`.
34
+ The wheel for each supported platform bundles the appropriate native binary
35
+ under `gbp_stack/_native/<rid>/`.
36
+
37
+ ## Layers
38
+
39
+ ```
40
+ ┌── application ──────────────────────────────────────────────────────┐
41
+ │ GtpClient · GapClient · GspClient (TCP / UDP / SCTP-like) │
42
+ ├──────────────────────────────────────────────────────────────────────┤
43
+ │ GroupNode (GBP — IP-like base) │
44
+ ├──────────────────────────────────────────────────────────────────────┤
45
+ │ MlsContext (RFC 9420) │
46
+ └──────────────────────────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ from gbp_stack import MlsContext, GroupNode, GtpClient
53
+
54
+ with MlsContext.create("alice") as alice_mls, \
55
+ MlsContext.create("bob") as bob_mls:
56
+
57
+ bob_kp = bob_mls.export_key_package()
58
+ welcome = alice_mls.invite(bob_kp)
59
+ bob_mls.accept_welcome(welcome)
60
+
61
+ group_id = alice_mls.group_id
62
+ with GroupNode.create(member_id=1, group_id=group_id) as alice, \
63
+ GroupNode.create(member_id=2, group_id=group_id) as bob, \
64
+ GtpClient.create() as gtp_alice, \
65
+ GtpClient.create() as gtp_bob:
66
+
67
+ alice.bootstrap_as_creator(alice_mls.epoch)
68
+ bob.bootstrap_as_joiner(bob_mls.epoch)
69
+
70
+ frame = gtp_alice.send(alice, alice_mls, target=2,
71
+ message_id=0xCAFE_F00D, text="hello")
72
+ for ev in bob.on_wire(bob_mls, frame.wire):
73
+ if ev.kind == "payload_received" and ev.stream_type == 2:
74
+ print(gtp_bob.accept(ev.plaintext).text)
75
+ ```
76
+
77
+ ## License
78
+
79
+ Licensed under [Apache License, Version 2.0](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE).
@@ -0,0 +1,55 @@
1
+ # gbp-stack — Python bindings for the Group Protocol Stack
2
+
3
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE)
4
+
5
+ Python bindings for the [Group Protocol Stack](https://github.com/F000NKKK/Group-Protocol-Stack):
6
+ a layered, end-to-end encrypted group-messaging protocol family built on top
7
+ of [MLS (RFC 9420)](https://www.rfc-editor.org/rfc/rfc9420).
8
+
9
+ This package wraps the native `gbp_stack` shared library through `ctypes`.
10
+ The wheel for each supported platform bundles the appropriate native binary
11
+ under `gbp_stack/_native/<rid>/`.
12
+
13
+ ## Layers
14
+
15
+ ```
16
+ ┌── application ──────────────────────────────────────────────────────┐
17
+ │ GtpClient · GapClient · GspClient (TCP / UDP / SCTP-like) │
18
+ ├──────────────────────────────────────────────────────────────────────┤
19
+ │ GroupNode (GBP — IP-like base) │
20
+ ├──────────────────────────────────────────────────────────────────────┤
21
+ │ MlsContext (RFC 9420) │
22
+ └──────────────────────────────────────────────────────────────────────┘
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ from gbp_stack import MlsContext, GroupNode, GtpClient
29
+
30
+ with MlsContext.create("alice") as alice_mls, \
31
+ MlsContext.create("bob") as bob_mls:
32
+
33
+ bob_kp = bob_mls.export_key_package()
34
+ welcome = alice_mls.invite(bob_kp)
35
+ bob_mls.accept_welcome(welcome)
36
+
37
+ group_id = alice_mls.group_id
38
+ with GroupNode.create(member_id=1, group_id=group_id) as alice, \
39
+ GroupNode.create(member_id=2, group_id=group_id) as bob, \
40
+ GtpClient.create() as gtp_alice, \
41
+ GtpClient.create() as gtp_bob:
42
+
43
+ alice.bootstrap_as_creator(alice_mls.epoch)
44
+ bob.bootstrap_as_joiner(bob_mls.epoch)
45
+
46
+ frame = gtp_alice.send(alice, alice_mls, target=2,
47
+ message_id=0xCAFE_F00D, text="hello")
48
+ for ev in bob.on_wire(bob_mls, frame.wire):
49
+ if ev.kind == "payload_received" and ev.stream_type == 2:
50
+ print(gtp_bob.accept(ev.plaintext).text)
51
+ ```
52
+
53
+ ## License
54
+
55
+ Licensed under [Apache License, Version 2.0](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE).
@@ -0,0 +1,40 @@
1
+ """Python bindings for the Group Protocol Stack.
2
+
3
+ This package exposes a high-level Python API on top of the native
4
+ ``gbp_stack`` shared library:
5
+
6
+ * :class:`MlsContext` — RFC 9420 MLS context.
7
+ * :class:`GroupNode` — GBP-layer group node (the IP-like base).
8
+ * :class:`GtpClient`, :class:`GapClient`, :class:`GspClient` — sub-protocol
9
+ clients (text, audio, signalling).
10
+
11
+ See :func:`version` for the underlying library version and the project
12
+ README for a worked example.
13
+ """
14
+
15
+ from .gbp_node import GroupNode, NodeEvent, NodeState, OutboundFrame, StreamType
16
+ from .gap_client import GapAcceptResult, GapClient
17
+ from .gsp_client import GspAcceptResult, GspClient, SignalType
18
+ from .gtp_client import GtpAcceptResult, GtpClient
19
+ from .mls_context import MlsContext
20
+ from ._native import last_error, version
21
+
22
+ __all__ = [
23
+ "GapAcceptResult",
24
+ "GapClient",
25
+ "GroupNode",
26
+ "GspAcceptResult",
27
+ "GspClient",
28
+ "GtpAcceptResult",
29
+ "GtpClient",
30
+ "MlsContext",
31
+ "NodeEvent",
32
+ "NodeState",
33
+ "OutboundFrame",
34
+ "SignalType",
35
+ "StreamType",
36
+ "last_error",
37
+ "version",
38
+ ]
39
+
40
+ __version__ = "0.2.0"
@@ -0,0 +1,189 @@
1
+ """Low-level ctypes bindings to the native ``gbp_stack`` shared library.
2
+
3
+ The library is loaded from a platform-specific subdirectory of
4
+ ``gbp_stack/_native/`` (so it can be packaged inside the wheel), with a
5
+ fallback to the OS loader path. Call sites should not depend on the symbols
6
+ in this module directly — use the high-level wrappers in :mod:`gbp_stack`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import os
13
+ import platform
14
+ from ctypes import (
15
+ Structure,
16
+ c_bool,
17
+ c_char_p,
18
+ c_int32,
19
+ c_size_t,
20
+ c_uint8,
21
+ c_uint16,
22
+ c_uint32,
23
+ c_uint64,
24
+ c_void_p,
25
+ )
26
+ from typing import Iterable
27
+
28
+
29
+ def _candidate_paths() -> Iterable[str]:
30
+ here = os.path.dirname(os.path.abspath(__file__))
31
+ system = platform.system().lower()
32
+ machine = platform.machine().lower()
33
+ if system == "windows":
34
+ rid = "win-x64" if machine in ("amd64", "x86_64") else f"win-{machine}"
35
+ name = "gbp_stack.dll"
36
+ elif system == "darwin":
37
+ rid = "osx-arm64" if machine == "arm64" else "osx-x64"
38
+ name = "libgbp_stack.dylib"
39
+ else:
40
+ rid = "linux-x64" if machine in ("x86_64", "amd64") else f"linux-{machine}"
41
+ name = "libgbp_stack.so"
42
+ yield os.path.join(here, "_native", rid, name)
43
+ yield os.path.join(here, "_native", name)
44
+ yield name
45
+
46
+
47
+ def _load() -> ctypes.CDLL:
48
+ last_err: OSError | None = None
49
+ for path in _candidate_paths():
50
+ try:
51
+ return ctypes.CDLL(path)
52
+ except OSError as e:
53
+ last_err = e
54
+ raise OSError(
55
+ "failed to load native gbp_stack library; tried: "
56
+ + ", ".join(_candidate_paths())
57
+ + (f"; last error: {last_err}" if last_err else "")
58
+ )
59
+
60
+
61
+ _lib = _load()
62
+
63
+
64
+ class GbpBuffer(Structure):
65
+ """``(ptr, len, cap)`` triple matching the FFI ``GbpBuffer`` struct."""
66
+
67
+ _fields_ = [
68
+ ("ptr", c_void_p),
69
+ ("len", c_size_t),
70
+ ("cap", c_size_t),
71
+ ]
72
+
73
+
74
+ def _bind(name: str, restype, argtypes):
75
+ fn = getattr(_lib, name)
76
+ fn.restype = restype
77
+ fn.argtypes = argtypes
78
+ return fn
79
+
80
+
81
+ gbp_buffer_free = _bind("gbp_buffer_free", None, [GbpBuffer])
82
+ gbp_string_free = _bind("gbp_string_free", None, [c_void_p])
83
+ _gbp_last_error = _bind("gbp_last_error", c_void_p, [])
84
+ _gbp_version = _bind("gbp_version", c_void_p, [])
85
+
86
+ gbp_mls_create = _bind("gbp_mls_create", c_int32, [c_void_p, c_size_t])
87
+ gbp_mls_destroy = _bind("gbp_mls_destroy", None, [c_int32])
88
+ gbp_mls_epoch = _bind("gbp_mls_epoch", c_uint64, [c_int32])
89
+ gbp_mls_group_id = _bind("gbp_mls_group_id", c_bool, [c_int32, c_void_p])
90
+ gbp_mls_export_key_package = _bind("gbp_mls_export_key_package", GbpBuffer, [c_int32])
91
+ gbp_mls_invite = _bind("gbp_mls_invite", GbpBuffer, [c_int32, c_void_p, c_size_t])
92
+ gbp_mls_accept_welcome = _bind(
93
+ "gbp_mls_accept_welcome", c_bool, [c_int32, c_void_p, c_size_t]
94
+ )
95
+
96
+ gbp_node_create = _bind("gbp_node_create", c_int32, [c_uint32, c_void_p])
97
+ gbp_node_destroy = _bind("gbp_node_destroy", None, [c_int32])
98
+ gbp_node_bootstrap_creator = _bind("gbp_node_bootstrap_creator", c_bool, [c_int32, c_uint64])
99
+ gbp_node_bootstrap_joiner = _bind("gbp_node_bootstrap_joiner", c_bool, [c_int32, c_uint64])
100
+ gbp_node_state = _bind("gbp_node_state", c_uint32, [c_int32])
101
+ gbp_node_epoch = _bind("gbp_node_epoch", c_uint64, [c_int32])
102
+ gbp_node_last_transition_id = _bind("gbp_node_last_transition_id", c_uint32, [c_int32])
103
+ gbp_node_set_epoch = _bind("gbp_node_set_epoch", c_bool, [c_int32, c_uint64])
104
+ gbp_node_apply_transition = _bind("gbp_node_apply_transition", c_bool, [c_int32, c_uint32])
105
+ gbp_node_send_control = _bind(
106
+ "gbp_node_send_control",
107
+ GbpBuffer,
108
+ [c_int32, c_int32, c_uint32, c_uint16, c_uint32, c_uint32, c_void_p, c_size_t],
109
+ )
110
+ gbp_node_on_wire = _bind(
111
+ "gbp_node_on_wire", c_void_p, [c_int32, c_int32, c_void_p, c_size_t]
112
+ )
113
+ gbp_node_drain_events = _bind("gbp_node_drain_events", c_void_p, [c_int32])
114
+
115
+ gtp_client_create = _bind("gtp_client_create", c_int32, [])
116
+ gtp_client_destroy = _bind("gtp_client_destroy", None, [c_int32])
117
+ gtp_client_reset = _bind("gtp_client_reset", None, [c_int32])
118
+ gtp_client_send = _bind(
119
+ "gtp_client_send",
120
+ GbpBuffer,
121
+ [c_int32, c_int32, c_int32, c_uint32, c_uint64, c_void_p, c_size_t],
122
+ )
123
+ gtp_client_accept = _bind("gtp_client_accept", c_void_p, [c_int32, c_void_p, c_size_t])
124
+
125
+ gap_client_create = _bind("gap_client_create", c_int32, [])
126
+ gap_client_destroy = _bind("gap_client_destroy", None, [c_int32])
127
+ gap_client_reset = _bind("gap_client_reset", None, [c_int32])
128
+ gap_client_send = _bind(
129
+ "gap_client_send",
130
+ GbpBuffer,
131
+ [c_int32, c_int32, c_int32, c_uint32, c_uint32, c_uint64, c_void_p, c_size_t],
132
+ )
133
+ gap_client_accept = _bind(
134
+ "gap_client_accept", c_void_p, [c_int32, c_uint64, c_void_p, c_size_t]
135
+ )
136
+
137
+ gsp_client_create = _bind("gsp_client_create", c_int32, [])
138
+ gsp_client_destroy = _bind("gsp_client_destroy", None, [c_int32])
139
+ gsp_client_reset = _bind("gsp_client_reset", None, [c_int32])
140
+ gsp_client_send = _bind(
141
+ "gsp_client_send",
142
+ GbpBuffer,
143
+ [c_int32, c_int32, c_int32, c_uint32, c_uint32, c_uint32, c_uint32],
144
+ )
145
+ gsp_client_accept = _bind("gsp_client_accept", c_void_p, [c_int32, c_void_p, c_size_t])
146
+
147
+
148
+ def take_buffer(buf: GbpBuffer) -> bytes:
149
+ """Copy a returned :class:`GbpBuffer` into a ``bytes`` object and free it."""
150
+ if not buf.ptr or buf.len == 0:
151
+ gbp_buffer_free(buf)
152
+ return b""
153
+ raw = ctypes.string_at(buf.ptr, buf.len)
154
+ gbp_buffer_free(buf)
155
+ return raw
156
+
157
+
158
+ def take_cstring(ptr: int) -> str:
159
+ """Copy a returned C string into a ``str`` and free it."""
160
+ if not ptr:
161
+ return ""
162
+ try:
163
+ raw = ctypes.cast(ptr, c_char_p).value
164
+ return raw.decode("utf-8") if raw is not None else ""
165
+ finally:
166
+ gbp_string_free(ptr)
167
+
168
+
169
+ def last_error() -> str:
170
+ """Return the text of the last FFI error on this thread."""
171
+ return take_cstring(_gbp_last_error())
172
+
173
+
174
+ def version() -> str:
175
+ """Return the native library version string."""
176
+ return take_cstring(_gbp_version())
177
+
178
+
179
+ def call_with_bytes(data: bytes, fn):
180
+ """Invoke ``fn(ptr, length)`` with a temporary ctypes buffer.
181
+
182
+ The buffer's lifetime is bounded to this call; ``fn`` MUST NOT retain
183
+ ``ptr`` past its return.
184
+ """
185
+ length = len(data)
186
+ if length == 0:
187
+ return fn(None, 0)
188
+ arr = (c_uint8 * length).from_buffer_copy(data)
189
+ return fn(ctypes.cast(arr, c_void_p), length)
@@ -0,0 +1,106 @@
1
+ """Group Audio Protocol client wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ from . import _native as _n
10
+ from .gbp_node import GroupNode, OutboundFrame, _unpack
11
+ from .mls_context import MlsContext
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class GapAcceptResult:
16
+ """Outcome of :meth:`GapClient.accept`."""
17
+
18
+ status: str
19
+ source: Optional[int] = None
20
+ seq: Optional[int] = None
21
+ bytes_: Optional[int] = None
22
+ reason: Optional[str] = None
23
+
24
+ @classmethod
25
+ def _parse(cls, json_text: str) -> "GapAcceptResult":
26
+ d = json.loads(json_text) if json_text else {}
27
+ return cls(
28
+ status=d.get("status", "?"),
29
+ source=d.get("source"),
30
+ seq=d.get("seq"),
31
+ bytes_=d.get("bytes"),
32
+ reason=d.get("reason"),
33
+ )
34
+
35
+
36
+ class GapClient:
37
+ """Group Audio Protocol client.
38
+
39
+ Maintains a per-source ``rtp_sequence`` window and validates ``key_phase``
40
+ against the current group epoch.
41
+ """
42
+
43
+ __slots__ = ("_handle",)
44
+
45
+ def __init__(self, handle: int) -> None:
46
+ self._handle = handle
47
+
48
+ @classmethod
49
+ def create(cls) -> "GapClient":
50
+ """Create a fresh GAP client."""
51
+ h = _n.gap_client_create()
52
+ if h <= 0:
53
+ raise OSError("gap_client_create")
54
+ return cls(h)
55
+
56
+ @property
57
+ def handle(self) -> int:
58
+ """Native handle (i32)."""
59
+ return self._handle
60
+
61
+ def send(
62
+ self,
63
+ node: GroupNode,
64
+ mls: MlsContext,
65
+ target: int,
66
+ media_source_id: int,
67
+ rtp_timestamp: int,
68
+ opus: bytes,
69
+ ) -> OutboundFrame:
70
+ """Send an Opus audio frame."""
71
+ def call(ptr, length):
72
+ return _n.gap_client_send(
73
+ self._handle, node.handle, mls.handle, target,
74
+ media_source_id, rtp_timestamp, ptr, length,
75
+ )
76
+ buf = _n.call_with_bytes(opus, call)
77
+ return _unpack(buf, "gap_client_send")
78
+
79
+ def accept(self, plaintext: bytes, current_epoch: int) -> GapAcceptResult:
80
+ """Accept a plaintext payload delivered by the GBP layer."""
81
+ def call(ptr, length):
82
+ return _n.gap_client_accept(self._handle, current_epoch, ptr, length)
83
+ ptr = _n.call_with_bytes(plaintext, call)
84
+ return GapAcceptResult._parse(_n.take_cstring(ptr))
85
+
86
+ def reset(self) -> None:
87
+ """Clear the replay window. Intended for use after an epoch change."""
88
+ _n.gap_client_reset(self._handle)
89
+
90
+ def close(self) -> None:
91
+ """Release the native handle. Idempotent."""
92
+ if self._handle:
93
+ _n.gap_client_destroy(self._handle)
94
+ self._handle = 0
95
+
96
+ def __enter__(self) -> "GapClient":
97
+ return self
98
+
99
+ def __exit__(self, exc_type, exc, tb) -> None:
100
+ self.close()
101
+
102
+ def __del__(self) -> None:
103
+ try:
104
+ self.close()
105
+ except Exception:
106
+ pass
@@ -0,0 +1,256 @@
1
+ """GBP-layer group node wrapper (the IP-like base)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import ctypes
7
+ import enum
8
+ import json
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional
11
+
12
+ from . import _native as _n
13
+ from .mls_context import MlsContext
14
+
15
+
16
+ class StreamType(enum.IntEnum):
17
+ """Stream class."""
18
+
19
+ CONTROL = 0
20
+ AUDIO = 1
21
+ TEXT = 2
22
+ SIGNAL = 3
23
+
24
+
25
+ class NodeState(enum.IntEnum):
26
+ """Node FSM state."""
27
+
28
+ IDLE = 0
29
+ CONNECTING = 1
30
+ ESTABLISHING_GROUP = 2
31
+ ACTIVE = 3
32
+ RESYNCING = 4
33
+ FAILED = 5
34
+ CLOSED = 6
35
+
36
+
37
+ class ControlOpcode(enum.IntEnum):
38
+ """Control plane opcode."""
39
+
40
+ PREPARE_TRANSITION = 0x0001
41
+ READY_FOR_TRANSITION = 0x0002
42
+ EXECUTE_TRANSITION = 0x0003
43
+ ABORT_TRANSITION = 0x0004
44
+ GROUP_STATE_DIGEST_REQUEST = 0x0005
45
+ GROUP_STATE_DIGEST_RESPONSE = 0x0006
46
+ REPORT_INVALID_COMMIT = 0x0007
47
+ CAPABILITIES_ADVERTISE = 0x0008
48
+ ACK = 0x0009
49
+ NACK = 0x000A
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class OutboundFrame:
54
+ """Wire frame produced by the native library."""
55
+
56
+ target: int
57
+ wire: bytes
58
+
59
+
60
+ @dataclass
61
+ class NodeEvent:
62
+ """Event surfaced by the GBP layer.
63
+
64
+ ``kind`` tells which fields are populated; common kinds are:
65
+
66
+ * ``state_changed`` — populates ``from_state`` and ``to_state``;
67
+ * ``payload_received`` — populates ``stream_type``, ``stream_id``,
68
+ ``sequence_no``, ``flags`` and ``plaintext``;
69
+ * ``control`` — populates ``sender``, ``opcode`` and ``transition_id``;
70
+ * ``error`` — populates ``code``, ``code_hex``, ``class_``, ``retryable``,
71
+ ``fatal`` and ``reason``;
72
+ * ``epoch_advanced`` — populates ``epoch`` and ``transition_id``.
73
+ """
74
+
75
+ kind: str
76
+ from_state: Optional[str] = None
77
+ to_state: Optional[str] = None
78
+ stream_type_name: Optional[str] = None
79
+ stream_type: Optional[StreamType] = None
80
+ stream_id: Optional[int] = None
81
+ sequence_no: Optional[int] = None
82
+ flags: Optional[int] = None
83
+ plaintext: Optional[bytes] = None
84
+ sender: Optional[int] = None
85
+ opcode: Optional[str] = None
86
+ opcode_code: Optional[int] = None
87
+ transition_id: Optional[int] = None
88
+ code: Optional[int] = None
89
+ code_hex: Optional[str] = None
90
+ class_: Optional[int] = None
91
+ retryable: Optional[bool] = None
92
+ fatal: Optional[bool] = None
93
+ reason: Optional[str] = None
94
+ epoch: Optional[int] = None
95
+
96
+ @classmethod
97
+ def _from_dict(cls, d: dict) -> "NodeEvent":
98
+ st_code = d.get("stream_type_code")
99
+ plaintext_b64 = d.get("plaintext_b64")
100
+ return cls(
101
+ kind=d["kind"],
102
+ from_state=d.get("from"),
103
+ to_state=d.get("to"),
104
+ stream_type_name=d.get("stream_type"),
105
+ stream_type=StreamType(st_code) if st_code is not None else None,
106
+ stream_id=d.get("stream_id"),
107
+ sequence_no=d.get("sequence_no"),
108
+ flags=d.get("flags"),
109
+ plaintext=base64.b64decode(plaintext_b64) if plaintext_b64 else None,
110
+ sender=d.get("from") if isinstance(d.get("from"), int) else None,
111
+ opcode=d.get("opcode"),
112
+ opcode_code=d.get("opcode_code"),
113
+ transition_id=d.get("transition_id"),
114
+ code=d.get("code"),
115
+ code_hex=d.get("code_hex"),
116
+ class_=d.get("class"),
117
+ retryable=d.get("retryable"),
118
+ fatal=d.get("fatal"),
119
+ reason=d.get("reason"),
120
+ epoch=d.get("epoch"),
121
+ )
122
+
123
+
124
+ def _parse_events(json_text: str) -> List[NodeEvent]:
125
+ if not json_text or json_text == "[]":
126
+ return []
127
+ return [NodeEvent._from_dict(d) for d in json.loads(json_text)]
128
+
129
+
130
+ def _unpack(buf: _n.GbpBuffer, what: str) -> OutboundFrame:
131
+ raw = _n.take_buffer(buf)
132
+ if not raw:
133
+ raise OSError(f"{what}: {_n.last_error()}")
134
+ if len(raw) < 4:
135
+ raise OSError(f"{what}: buffer too short")
136
+ target = int.from_bytes(raw[:4], byteorder="little", signed=False)
137
+ return OutboundFrame(target=target, wire=raw[4:])
138
+
139
+
140
+ class GroupNode:
141
+ """GBP-layer group node.
142
+
143
+ Owns the framing, AEAD, replay window, FSM and control plane.
144
+ Sub-protocol semantics live in :class:`gbp_stack.GtpClient`,
145
+ :class:`gbp_stack.GapClient` and :class:`gbp_stack.GspClient`.
146
+ """
147
+
148
+ __slots__ = ("_handle", "member_id", "_group_id")
149
+
150
+ def __init__(self, handle: int, member_id: int, group_id: bytes) -> None:
151
+ self._handle = handle
152
+ self.member_id = member_id
153
+ self._group_id = bytes(group_id)
154
+
155
+ @classmethod
156
+ def create(cls, member_id: int, group_id: bytes) -> "GroupNode":
157
+ """Create a node bound to ``group_id`` (which MUST be 16 bytes)."""
158
+ if len(group_id) != 16:
159
+ raise ValueError("group_id must be 16 bytes")
160
+ gid = (ctypes.c_uint8 * 16).from_buffer_copy(group_id)
161
+ handle = _n.gbp_node_create(member_id, ctypes.cast(gid, ctypes.c_void_p))
162
+ if handle <= 0:
163
+ raise OSError(f"node_create: {_n.last_error()}")
164
+ return cls(handle, member_id, group_id)
165
+
166
+ @property
167
+ def handle(self) -> int:
168
+ """Native handle (i32)."""
169
+ return self._handle
170
+
171
+ @property
172
+ def state(self) -> NodeState:
173
+ """Current FSM state."""
174
+ return NodeState(_n.gbp_node_state(self._handle))
175
+
176
+ @property
177
+ def epoch(self) -> int:
178
+ """Current node epoch."""
179
+ return int(_n.gbp_node_epoch(self._handle))
180
+
181
+ @property
182
+ def last_transition_id(self) -> int:
183
+ """Last applied ``transition_id``."""
184
+ return int(_n.gbp_node_last_transition_id(self._handle))
185
+
186
+ @property
187
+ def group_id(self) -> bytes:
188
+ """16-byte group identifier."""
189
+ return self._group_id
190
+
191
+ def bootstrap_as_creator(self, epoch: int) -> None:
192
+ """Drive the node from ``IDLE`` to ``ACTIVE`` as a creator."""
193
+ if not _n.gbp_node_bootstrap_creator(self._handle, epoch):
194
+ raise OSError(_n.last_error())
195
+
196
+ def bootstrap_as_joiner(self, epoch: int) -> None:
197
+ """Drive the node from ``IDLE`` to ``ACTIVE`` as a joiner."""
198
+ if not _n.gbp_node_bootstrap_joiner(self._handle, epoch):
199
+ raise OSError(_n.last_error())
200
+
201
+ def set_epoch_for_testing(self, epoch: int) -> None:
202
+ """Forcibly override ``current_epoch`` (intended for tests of late peers)."""
203
+ if not _n.gbp_node_set_epoch(self._handle, epoch):
204
+ raise OSError(_n.last_error())
205
+
206
+ def apply_transition(self, transition_id: int) -> None:
207
+ """Apply an epoch transition locally."""
208
+ if not _n.gbp_node_apply_transition(self._handle, transition_id):
209
+ raise OSError(_n.last_error())
210
+
211
+ def send_control(
212
+ self,
213
+ mls: MlsContext,
214
+ target: int,
215
+ opcode: ControlOpcode,
216
+ transition_id: int,
217
+ request_id: int,
218
+ args: bytes = b"",
219
+ ) -> OutboundFrame:
220
+ """Send a control plane message on Stream 0."""
221
+ def call(ptr, length):
222
+ return _n.gbp_node_send_control(
223
+ self._handle, mls.handle, target, int(opcode),
224
+ transition_id, request_id, ptr, length,
225
+ )
226
+ buf = _n.call_with_bytes(args, call)
227
+ return _unpack(buf, "send_control")
228
+
229
+ def on_wire(self, mls: MlsContext, wire: bytes) -> List[NodeEvent]:
230
+ """Feed wire bytes to the node and return the resulting events."""
231
+ def call(ptr, length):
232
+ return _n.gbp_node_on_wire(self._handle, mls.handle, ptr, length)
233
+ ptr = _n.call_with_bytes(wire, call)
234
+ return _parse_events(_n.take_cstring(ptr))
235
+
236
+ def drain_events(self) -> List[NodeEvent]:
237
+ """Drain queued events without consuming any wire bytes."""
238
+ return _parse_events(_n.take_cstring(_n.gbp_node_drain_events(self._handle)))
239
+
240
+ def close(self) -> None:
241
+ """Release the native handle. Idempotent."""
242
+ if self._handle:
243
+ _n.gbp_node_destroy(self._handle)
244
+ self._handle = 0
245
+
246
+ def __enter__(self) -> "GroupNode":
247
+ return self
248
+
249
+ def __exit__(self, exc_type, exc, tb) -> None:
250
+ self.close()
251
+
252
+ def __del__(self) -> None:
253
+ try:
254
+ self.close()
255
+ except Exception:
256
+ pass
@@ -0,0 +1,123 @@
1
+ """Group Signaling Protocol client wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ import json
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+ from . import _native as _n
11
+ from .gbp_node import GroupNode, OutboundFrame, _unpack
12
+ from .mls_context import MlsContext
13
+
14
+
15
+ class SignalType(enum.IntEnum):
16
+ """Signal opcode registry."""
17
+
18
+ JOIN = 100
19
+ LEAVE = 101
20
+ ROLE_CHANGE = 102
21
+ MUTE = 200
22
+ UNMUTE = 201
23
+ STREAM_START = 300
24
+ STREAM_STOP = 301
25
+ CODEC_UPDATE = 400
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class GspAcceptResult:
30
+ """Outcome of :meth:`GspClient.accept`."""
31
+
32
+ status: str
33
+ signal: Optional[str] = None
34
+ signal_code: Optional[SignalType] = None
35
+ sender: Optional[int] = None
36
+ role_claim: Optional[int] = None
37
+ request_id: Optional[int] = None
38
+ reason: Optional[str] = None
39
+
40
+ @classmethod
41
+ def _parse(cls, json_text: str) -> "GspAcceptResult":
42
+ d = json.loads(json_text) if json_text else {}
43
+ sc = d.get("signal_code")
44
+ return cls(
45
+ status=d.get("status", "?"),
46
+ signal=d.get("signal"),
47
+ signal_code=SignalType(sc) if sc is not None else None,
48
+ sender=d.get("sender"),
49
+ role_claim=d.get("role_claim"),
50
+ request_id=d.get("request_id"),
51
+ reason=d.get("reason"),
52
+ )
53
+
54
+
55
+ class GspClient:
56
+ """Group Signaling Protocol client.
57
+
58
+ Tracks ``request_id`` deduplication and maintains the local membership
59
+ and mute-list state.
60
+ """
61
+
62
+ __slots__ = ("_handle",)
63
+
64
+ def __init__(self, handle: int) -> None:
65
+ self._handle = handle
66
+
67
+ @classmethod
68
+ def create(cls) -> "GspClient":
69
+ """Create a fresh GSP client."""
70
+ h = _n.gsp_client_create()
71
+ if h <= 0:
72
+ raise OSError("gsp_client_create")
73
+ return cls(h)
74
+
75
+ @property
76
+ def handle(self) -> int:
77
+ """Native handle (i32)."""
78
+ return self._handle
79
+
80
+ def send(
81
+ self,
82
+ node: GroupNode,
83
+ mls: MlsContext,
84
+ target: int,
85
+ signal: SignalType,
86
+ role_claim: int,
87
+ request_id: int,
88
+ ) -> OutboundFrame:
89
+ """Send a signal."""
90
+ buf = _n.gsp_client_send(
91
+ self._handle, node.handle, mls.handle, target,
92
+ int(signal), role_claim, request_id,
93
+ )
94
+ return _unpack(buf, "gsp_client_send")
95
+
96
+ def accept(self, plaintext: bytes) -> GspAcceptResult:
97
+ """Accept a plaintext payload delivered by the GBP layer."""
98
+ def call(ptr, length):
99
+ return _n.gsp_client_accept(self._handle, ptr, length)
100
+ ptr = _n.call_with_bytes(plaintext, call)
101
+ return GspAcceptResult._parse(_n.take_cstring(ptr))
102
+
103
+ def reset(self) -> None:
104
+ """Clear the request-id deduplication set. Intended for use after an epoch change."""
105
+ _n.gsp_client_reset(self._handle)
106
+
107
+ def close(self) -> None:
108
+ """Release the native handle. Idempotent."""
109
+ if self._handle:
110
+ _n.gsp_client_destroy(self._handle)
111
+ self._handle = 0
112
+
113
+ def __enter__(self) -> "GspClient":
114
+ return self
115
+
116
+ def __exit__(self, exc_type, exc, tb) -> None:
117
+ self.close()
118
+
119
+ def __del__(self) -> None:
120
+ try:
121
+ self.close()
122
+ except Exception:
123
+ pass
@@ -0,0 +1,104 @@
1
+ """Group Text Protocol client wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ from . import _native as _n
10
+ from .gbp_node import GroupNode, OutboundFrame, _unpack
11
+ from .mls_context import MlsContext
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class GtpAcceptResult:
16
+ """Outcome of :meth:`GtpClient.accept`."""
17
+
18
+ status: str
19
+ sender: Optional[int] = None
20
+ message_id: Optional[int] = None
21
+ text: Optional[str] = None
22
+ reason: Optional[str] = None
23
+
24
+ @classmethod
25
+ def _parse(cls, json_text: str) -> "GtpAcceptResult":
26
+ d = json.loads(json_text) if json_text else {}
27
+ return cls(
28
+ status=d.get("status", "?"),
29
+ sender=d.get("sender"),
30
+ message_id=d.get("message_id"),
31
+ text=d.get("text"),
32
+ reason=d.get("reason"),
33
+ )
34
+
35
+
36
+ class GtpClient:
37
+ """Group Text Protocol client.
38
+
39
+ Tracks idempotency by ``(sender_id, message_id)``.
40
+ """
41
+
42
+ __slots__ = ("_handle",)
43
+
44
+ def __init__(self, handle: int) -> None:
45
+ self._handle = handle
46
+
47
+ @classmethod
48
+ def create(cls) -> "GtpClient":
49
+ """Create a fresh GTP client."""
50
+ h = _n.gtp_client_create()
51
+ if h <= 0:
52
+ raise OSError("gtp_client_create")
53
+ return cls(h)
54
+
55
+ @property
56
+ def handle(self) -> int:
57
+ """Native handle (i32)."""
58
+ return self._handle
59
+
60
+ def send(
61
+ self,
62
+ node: GroupNode,
63
+ mls: MlsContext,
64
+ target: int,
65
+ message_id: int,
66
+ text: str,
67
+ ) -> OutboundFrame:
68
+ """Send a text message."""
69
+ data = text.encode("utf-8")
70
+ def call(ptr, length):
71
+ return _n.gtp_client_send(
72
+ self._handle, node.handle, mls.handle, target, message_id, ptr, length
73
+ )
74
+ buf = _n.call_with_bytes(data, call)
75
+ return _unpack(buf, "gtp_client_send")
76
+
77
+ def accept(self, plaintext: bytes) -> GtpAcceptResult:
78
+ """Accept a plaintext payload delivered by the GBP layer."""
79
+ def call(ptr, length):
80
+ return _n.gtp_client_accept(self._handle, ptr, length)
81
+ ptr = _n.call_with_bytes(plaintext, call)
82
+ return GtpAcceptResult._parse(_n.take_cstring(ptr))
83
+
84
+ def reset(self) -> None:
85
+ """Clear the idempotency state. Intended for use after an epoch change."""
86
+ _n.gtp_client_reset(self._handle)
87
+
88
+ def close(self) -> None:
89
+ """Release the native handle. Idempotent."""
90
+ if self._handle:
91
+ _n.gtp_client_destroy(self._handle)
92
+ self._handle = 0
93
+
94
+ def __enter__(self) -> "GtpClient":
95
+ return self
96
+
97
+ def __exit__(self, exc_type, exc, tb) -> None:
98
+ self.close()
99
+
100
+ def __del__(self) -> None:
101
+ try:
102
+ self.close()
103
+ except Exception:
104
+ pass
@@ -0,0 +1,93 @@
1
+ """MLS (RFC 9420) context wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+
7
+ from . import _native as _n
8
+
9
+
10
+ class MlsContext:
11
+ """Managed wrapper around an MLS context owned by the native library.
12
+
13
+ Owns a single-member group plus a published ``KeyPackage`` that can be
14
+ used to invite this member into another group. Always use as a context
15
+ manager so the native handle is released.
16
+ """
17
+
18
+ __slots__ = ("_handle", "identity")
19
+
20
+ def __init__(self, handle: int, identity: str) -> None:
21
+ self._handle = handle
22
+ self.identity = identity
23
+
24
+ @classmethod
25
+ def create(cls, identity: str) -> "MlsContext":
26
+ """Create a fresh MLS context."""
27
+ data = identity.encode("utf-8")
28
+ handle = _n.call_with_bytes(data, _n.gbp_mls_create)
29
+ if handle <= 0:
30
+ raise OSError(f"gbp_mls_create: {_n.last_error()}")
31
+ return cls(handle, identity)
32
+
33
+ @property
34
+ def handle(self) -> int:
35
+ """Native handle (i32)."""
36
+ return self._handle
37
+
38
+ @property
39
+ def epoch(self) -> int:
40
+ """Current group epoch."""
41
+ return int(_n.gbp_mls_epoch(self._handle))
42
+
43
+ @property
44
+ def group_id(self) -> bytes:
45
+ """16-byte group identifier."""
46
+ buf = (ctypes.c_uint8 * 16)()
47
+ if not _n.gbp_mls_group_id(self._handle, ctypes.cast(buf, ctypes.c_void_p)):
48
+ raise OSError(f"group_id: {_n.last_error()}")
49
+ return bytes(buf)
50
+
51
+ def export_key_package(self) -> bytes:
52
+ """Export this member's TLS-serialised KeyPackage."""
53
+ buf = _n.gbp_mls_export_key_package(self._handle)
54
+ out = _n.take_buffer(buf)
55
+ if not out:
56
+ raise OSError(f"export_key_package: {_n.last_error()}")
57
+ return out
58
+
59
+ def invite(self, key_package: bytes) -> bytes:
60
+ """Invite ``key_package`` into the local group; returns the Welcome."""
61
+ def call(ptr, length):
62
+ return _n.gbp_mls_invite(self._handle, ptr, length)
63
+ buf = _n.call_with_bytes(key_package, call)
64
+ out = _n.take_buffer(buf)
65
+ if not out:
66
+ raise OSError(f"invite: {_n.last_error()}")
67
+ return out
68
+
69
+ def accept_welcome(self, welcome: bytes) -> None:
70
+ """Replace the local group with the one described by ``welcome``."""
71
+ def call(ptr, length):
72
+ return _n.gbp_mls_accept_welcome(self._handle, ptr, length)
73
+ ok = _n.call_with_bytes(welcome, call)
74
+ if not ok:
75
+ raise OSError(f"accept_welcome: {_n.last_error()}")
76
+
77
+ def close(self) -> None:
78
+ """Release the native handle. Idempotent."""
79
+ if self._handle:
80
+ _n.gbp_mls_destroy(self._handle)
81
+ self._handle = 0
82
+
83
+ def __enter__(self) -> "MlsContext":
84
+ return self
85
+
86
+ def __exit__(self, exc_type, exc, tb) -> None:
87
+ self.close()
88
+
89
+ def __del__(self) -> None:
90
+ try:
91
+ self.close()
92
+ except Exception:
93
+ pass
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: gbp-stack
3
+ Version: 0.2.0
4
+ Summary: Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
5
+ Author: Group Protocol Stack contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/F000NKKK/Group-Protocol-Stack
8
+ Project-URL: Repository, https://github.com/F000NKKK/Group-Protocol-Stack
9
+ Project-URL: Documentation, https://github.com/F000NKKK/Group-Protocol-Stack#readme
10
+ Project-URL: Issues, https://github.com/F000NKKK/Group-Protocol-Stack/issues
11
+ Keywords: mls,rfc9420,e2ee,group-messaging,chat,voice,signaling,cryptography,ffi
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Topic :: System :: Networking
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # gbp-stack — Python bindings for the Group Protocol Stack
26
+
27
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE)
28
+
29
+ Python bindings for the [Group Protocol Stack](https://github.com/F000NKKK/Group-Protocol-Stack):
30
+ a layered, end-to-end encrypted group-messaging protocol family built on top
31
+ of [MLS (RFC 9420)](https://www.rfc-editor.org/rfc/rfc9420).
32
+
33
+ This package wraps the native `gbp_stack` shared library through `ctypes`.
34
+ The wheel for each supported platform bundles the appropriate native binary
35
+ under `gbp_stack/_native/<rid>/`.
36
+
37
+ ## Layers
38
+
39
+ ```
40
+ ┌── application ──────────────────────────────────────────────────────┐
41
+ │ GtpClient · GapClient · GspClient (TCP / UDP / SCTP-like) │
42
+ ├──────────────────────────────────────────────────────────────────────┤
43
+ │ GroupNode (GBP — IP-like base) │
44
+ ├──────────────────────────────────────────────────────────────────────┤
45
+ │ MlsContext (RFC 9420) │
46
+ └──────────────────────────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ from gbp_stack import MlsContext, GroupNode, GtpClient
53
+
54
+ with MlsContext.create("alice") as alice_mls, \
55
+ MlsContext.create("bob") as bob_mls:
56
+
57
+ bob_kp = bob_mls.export_key_package()
58
+ welcome = alice_mls.invite(bob_kp)
59
+ bob_mls.accept_welcome(welcome)
60
+
61
+ group_id = alice_mls.group_id
62
+ with GroupNode.create(member_id=1, group_id=group_id) as alice, \
63
+ GroupNode.create(member_id=2, group_id=group_id) as bob, \
64
+ GtpClient.create() as gtp_alice, \
65
+ GtpClient.create() as gtp_bob:
66
+
67
+ alice.bootstrap_as_creator(alice_mls.epoch)
68
+ bob.bootstrap_as_joiner(bob_mls.epoch)
69
+
70
+ frame = gtp_alice.send(alice, alice_mls, target=2,
71
+ message_id=0xCAFE_F00D, text="hello")
72
+ for ev in bob.on_wire(bob_mls, frame.wire):
73
+ if ev.kind == "payload_received" and ev.stream_type == 2:
74
+ print(gtp_bob.accept(ev.plaintext).text)
75
+ ```
76
+
77
+ ## License
78
+
79
+ Licensed under [Apache License, Version 2.0](https://github.com/F000NKKK/Group-Protocol-Stack/blob/main/LICENSE).
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ gbp_stack/__init__.py
5
+ gbp_stack/_native.py
6
+ gbp_stack/gap_client.py
7
+ gbp_stack/gbp_node.py
8
+ gbp_stack/gsp_client.py
9
+ gbp_stack/gtp_client.py
10
+ gbp_stack/mls_context.py
11
+ gbp_stack.egg-info/PKG-INFO
12
+ gbp_stack.egg-info/SOURCES.txt
13
+ gbp_stack.egg-info/dependency_links.txt
14
+ gbp_stack.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ gbp_stack
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gbp-stack"
7
+ version = "0.2.0"
8
+ description = "Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Group Protocol Stack contributors" }]
13
+ keywords = ["mls", "rfc9420", "e2ee", "group-messaging", "chat", "voice", "signaling", "cryptography", "ffi"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Topic :: Security :: Cryptography",
24
+ "Topic :: System :: Networking",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/F000NKKK/Group-Protocol-Stack"
29
+ Repository = "https://github.com/F000NKKK/Group-Protocol-Stack"
30
+ Documentation = "https://github.com/F000NKKK/Group-Protocol-Stack#readme"
31
+ Issues = "https://github.com/F000NKKK/Group-Protocol-Stack/issues"
32
+
33
+ [tool.setuptools]
34
+ include-package-data = true
35
+
36
+ [tool.setuptools.packages.find]
37
+ include = ["gbp_stack*"]
38
+
39
+ [tool.setuptools.package-data]
40
+ "gbp_stack" = [
41
+ "_native/win-x64/*.dll",
42
+ "_native/linux-x64/*.so",
43
+ "_native/osx-x64/*.dylib",
44
+ "_native/osx-arm64/*.dylib",
45
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ """Setup shim that marks the wheel as platform-specific.
2
+
3
+ The package ships a native shared library, so the produced wheel must NOT be
4
+ tagged as ``py3-none-any``; it must carry a real platform tag. We do this by
5
+ overriding ``bdist_wheel.root_is_pure``.
6
+ """
7
+
8
+ from setuptools import setup
9
+
10
+ try:
11
+ from setuptools.command.bdist_wheel import bdist_wheel # type: ignore
12
+ except ImportError: # older setuptools
13
+ from wheel.bdist_wheel import bdist_wheel # type: ignore
14
+
15
+
16
+ class _bdist_wheel(bdist_wheel): # type: ignore[misc, valid-type]
17
+ def finalize_options(self) -> None:
18
+ super().finalize_options()
19
+ self.root_is_pure = False
20
+
21
+
22
+ setup(cmdclass={"bdist_wheel": _bdist_wheel})