protowire-python 0.70.0__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
protowire/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 TrendVidia, LLC.
3
+ """protowire — PXF/SBE/envelope codecs, Python wrapper around protowire-cpp."""
4
+
5
+
6
+ # start delvewheel patch
7
+ def _delvewheel_patch_1_12_1():
8
+ import os
9
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'protowire_python.libs'))):
10
+ os.add_dll_directory(libs_dir)
11
+
12
+
13
+ _delvewheel_patch_1_12_1()
14
+ del _delvewheel_patch_1_12_1
15
+ # end delvewheel patch
16
+
17
+ from . import envelope, pxf, sbe
18
+
19
+ __all__ = ["pxf", "sbe", "envelope"]
20
+ __version__ = "0.70.0"
Binary file
protowire/_schema.py ADDED
@@ -0,0 +1,69 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 TrendVidia, LLC.
3
+ """Helpers to extract a serialized FileDescriptorSet from a Python protobuf
4
+ Message subclass — needed because the C++ side speaks FileDescriptorSet bytes,
5
+ not Python descriptors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Iterable
11
+
12
+ from google.protobuf import descriptor_pb2
13
+ from google.protobuf.descriptor import Descriptor, FileDescriptor
14
+ from google.protobuf.message import Message
15
+
16
+
17
+ def fds_for_descriptor(desc: Descriptor) -> bytes:
18
+ """Build a FileDescriptorSet covering desc's file plus all transitive deps.
19
+
20
+ Files are emitted in dependency order so DescriptorPool.BuildFile() succeeds
21
+ on the C++ side.
22
+ """
23
+ visited: dict[str, FileDescriptor] = {}
24
+ order: list[FileDescriptor] = []
25
+
26
+ def walk(fd: FileDescriptor) -> None:
27
+ if fd.name in visited:
28
+ return
29
+ for dep in fd.dependencies:
30
+ walk(dep)
31
+ visited[fd.name] = fd
32
+ order.append(fd)
33
+
34
+ walk(desc.file)
35
+
36
+ fds = descriptor_pb2.FileDescriptorSet()
37
+ for fd in order:
38
+ proto = descriptor_pb2.FileDescriptorProto()
39
+ fd.CopyToProto(proto)
40
+ fds.file.append(proto)
41
+ return fds.SerializeToString()
42
+
43
+
44
+ def fds_for_message(msg: Message) -> bytes:
45
+ return fds_for_descriptor(type(msg).DESCRIPTOR)
46
+
47
+
48
+ def fds_for_files(files: Iterable[FileDescriptor]) -> bytes:
49
+ """Build a FileDescriptorSet covering all given files plus transitive deps."""
50
+ visited: dict[str, FileDescriptor] = {}
51
+ order: list[FileDescriptor] = []
52
+
53
+ def walk(fd: FileDescriptor) -> None:
54
+ if fd.name in visited:
55
+ return
56
+ for dep in fd.dependencies:
57
+ walk(dep)
58
+ visited[fd.name] = fd
59
+ order.append(fd)
60
+
61
+ for f in files:
62
+ walk(f)
63
+
64
+ fds = descriptor_pb2.FileDescriptorSet()
65
+ for fd in order:
66
+ proto = descriptor_pb2.FileDescriptorProto()
67
+ fd.CopyToProto(proto)
68
+ fds.file.append(proto)
69
+ return fds.SerializeToString()
protowire/envelope.py ADDED
@@ -0,0 +1,351 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 TrendVidia, LLC.
3
+ """Standard API response envelope — wire-compatible with the Go envelope package.
4
+
5
+ Wire format mirrors the Go `protowire:"N"` struct tags: signed ints zig-zag,
6
+ strings/bytes length-delimited, nested messages length-delimited. We implement
7
+ the wire format in pure Python rather than crossing the FFI for envelope ops
8
+ because there is no schema involved — field numbers are fixed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field as _field
14
+ from typing import Optional
15
+
16
+
17
+ # --- low-level wire helpers (proto3 / protowire) -------------------------
18
+
19
+
20
+ def _enc_varint(out: bytearray, v: int) -> None:
21
+ while v >= 0x80:
22
+ out.append((v & 0x7F) | 0x80)
23
+ v >>= 7
24
+ out.append(v & 0x7F)
25
+
26
+
27
+ def _enc_zigzag64(v: int) -> int:
28
+ return (v << 1) ^ (v >> 63) & 0xFFFFFFFFFFFFFFFF
29
+
30
+
31
+ def _enc_tag(out: bytearray, num: int, wire: int) -> None:
32
+ _enc_varint(out, (num << 3) | wire)
33
+
34
+
35
+ def _enc_string(out: bytearray, num: int, s: str) -> None:
36
+ if not s:
37
+ return
38
+ b = s.encode("utf-8")
39
+ _enc_tag(out, num, 2)
40
+ _enc_varint(out, len(b))
41
+ out.extend(b)
42
+
43
+
44
+ def _enc_bytes(out: bytearray, num: int, b: bytes) -> None:
45
+ if not b:
46
+ return
47
+ _enc_tag(out, num, 2)
48
+ _enc_varint(out, len(b))
49
+ out.extend(b)
50
+
51
+
52
+ def _enc_repeated_string(out: bytearray, num: int, ss: list[str]) -> None:
53
+ for s in ss:
54
+ # Repeated strings: one tag+value per element, even when empty.
55
+ _enc_tag(out, num, 2)
56
+ b = s.encode("utf-8")
57
+ _enc_varint(out, len(b))
58
+ out.extend(b)
59
+
60
+
61
+ def _enc_int32(out: bytearray, num: int, v: int) -> None:
62
+ if v == 0:
63
+ return
64
+ _enc_tag(out, num, 0)
65
+ if v < 0:
66
+ v &= 0xFFFFFFFFFFFFFFFF
67
+ _enc_varint(out, v)
68
+
69
+
70
+ def _enc_sint32(out: bytearray, num: int, v: int) -> None:
71
+ """Signed int32 with zig-zag encoding (matches Go protowire pb)."""
72
+ if v == 0:
73
+ return
74
+ _enc_tag(out, num, 0)
75
+ _enc_varint(out, _enc_zigzag64(v))
76
+
77
+
78
+ def _enc_submessage(out: bytearray, num: int, sub: bytes) -> None:
79
+ _enc_tag(out, num, 2)
80
+ _enc_varint(out, len(sub))
81
+ out.extend(sub)
82
+
83
+
84
+ def _dec_varint(buf: bytes, i: int) -> tuple[int, int]:
85
+ v = 0
86
+ shift = 0
87
+ n = len(buf)
88
+ while True:
89
+ if i >= n:
90
+ raise ValueError("truncated varint")
91
+ b = buf[i]
92
+ i += 1
93
+ v |= (b & 0x7F) << shift
94
+ if b < 0x80:
95
+ return v, i
96
+ shift += 7
97
+ if shift > 63:
98
+ raise ValueError("varint overflow")
99
+
100
+
101
+ def _dec_zigzag64(v: int) -> int:
102
+ return (v >> 1) ^ -(v & 1)
103
+
104
+
105
+ def _dec_tag(buf: bytes, i: int) -> tuple[int, int, int]:
106
+ v, i = _dec_varint(buf, i)
107
+ return v >> 3, v & 7, i
108
+
109
+
110
+ def _dec_string(buf: bytes, i: int) -> tuple[str, int]:
111
+ n, i = _dec_varint(buf, i)
112
+ end = i + n
113
+ return buf[i:end].decode("utf-8"), end
114
+
115
+
116
+ def _dec_bytes(buf: bytes, i: int) -> tuple[bytes, int]:
117
+ n, i = _dec_varint(buf, i)
118
+ end = i + n
119
+ return bytes(buf[i:end]), end
120
+
121
+
122
+ def _dec_int32(v: int) -> int:
123
+ """Decode a varint as a two's-complement int32 (sign-extended from int64)."""
124
+ if v >= 0x8000000000000000:
125
+ v -= 0x10000000000000000
126
+ return v
127
+
128
+
129
+ def _skip_field(buf: bytes, i: int, wire: int) -> int:
130
+ if wire == 0: # varint
131
+ _, i = _dec_varint(buf, i)
132
+ return i
133
+ if wire == 1: # fixed64
134
+ return i + 8
135
+ if wire == 2: # length-delimited
136
+ ln, i = _dec_varint(buf, i)
137
+ if ln < 0 or i + ln > len(buf):
138
+ raise ValueError("truncated length-delimited field")
139
+ return i + ln
140
+ if wire == 5: # fixed32
141
+ return i + 4
142
+ raise ValueError(f"unsupported wire type: {wire}")
143
+
144
+
145
+ # --- public types --------------------------------------------------------
146
+
147
+
148
+ @dataclass
149
+ class FieldError:
150
+ field: str = ""
151
+ code: str = ""
152
+ message: str = ""
153
+ args: list[str] = _field(default_factory=list)
154
+
155
+ def encode(self) -> bytes:
156
+ out = bytearray()
157
+ _enc_string(out, 1, self.field)
158
+ _enc_string(out, 2, self.code)
159
+ _enc_string(out, 3, self.message)
160
+ _enc_repeated_string(out, 4, self.args)
161
+ return bytes(out)
162
+
163
+ @classmethod
164
+ def decode(cls, data: bytes) -> "FieldError":
165
+ out = cls()
166
+ i = 0
167
+ n = len(data)
168
+ while i < n:
169
+ num, wire, i = _dec_tag(data, i)
170
+ if num == 1 and wire == 2:
171
+ out.field, i = _dec_string(data, i)
172
+ elif num == 2 and wire == 2:
173
+ out.code, i = _dec_string(data, i)
174
+ elif num == 3 and wire == 2:
175
+ out.message, i = _dec_string(data, i)
176
+ elif num == 4 and wire == 2:
177
+ v, i = _dec_string(data, i)
178
+ out.args.append(v)
179
+ else:
180
+ i = _skip_field(data, i, wire)
181
+ return out
182
+
183
+
184
+ @dataclass
185
+ class AppError:
186
+ code: str = ""
187
+ message: str = ""
188
+ args: list[str] = _field(default_factory=list)
189
+ details: list[FieldError] = _field(default_factory=list)
190
+ metadata: dict[str, str] = _field(default_factory=dict)
191
+
192
+ def with_field(
193
+ self,
194
+ field_name: str,
195
+ code: str,
196
+ message: str,
197
+ *args: str,
198
+ ) -> "AppError":
199
+ self.details.append(
200
+ FieldError(field=field_name, code=code, message=message, args=list(args))
201
+ )
202
+ return self
203
+
204
+ def with_meta(self, key: str, value: str) -> "AppError":
205
+ self.metadata[key] = value
206
+ return self
207
+
208
+ def encode(self) -> bytes:
209
+ out = bytearray()
210
+ _enc_string(out, 1, self.code)
211
+ _enc_string(out, 2, self.message)
212
+ _enc_repeated_string(out, 3, self.args)
213
+ for d in self.details:
214
+ _enc_submessage(out, 4, d.encode())
215
+ # Metadata: map<string,string> as repeated message{key=1,value=2}, field 5.
216
+ for k, v in self.metadata.items():
217
+ entry = bytearray()
218
+ _enc_string(entry, 1, k)
219
+ _enc_string(entry, 2, v)
220
+ _enc_submessage(out, 5, bytes(entry))
221
+ return bytes(out)
222
+
223
+ @classmethod
224
+ def decode(cls, data: bytes) -> "AppError":
225
+ out = cls()
226
+ i = 0
227
+ n = len(data)
228
+ while i < n:
229
+ num, wire, i = _dec_tag(data, i)
230
+ if num == 1 and wire == 2:
231
+ out.code, i = _dec_string(data, i)
232
+ elif num == 2 and wire == 2:
233
+ out.message, i = _dec_string(data, i)
234
+ elif num == 3 and wire == 2:
235
+ v, i = _dec_string(data, i)
236
+ out.args.append(v)
237
+ elif num == 4 and wire == 2:
238
+ sub, i = _dec_bytes(data, i)
239
+ out.details.append(FieldError.decode(sub))
240
+ elif num == 5 and wire == 2:
241
+ # Map entry: message{key=1,value=2}.
242
+ sub, i = _dec_bytes(data, i)
243
+ key, val, j = "", "", 0
244
+ m = len(sub)
245
+ while j < m:
246
+ knum, kwire, j = _dec_tag(sub, j)
247
+ if knum == 1 and kwire == 2:
248
+ key, j = _dec_string(sub, j)
249
+ elif knum == 2 and kwire == 2:
250
+ val, j = _dec_string(sub, j)
251
+ else:
252
+ j = _skip_field(sub, j, kwire)
253
+ out.metadata[key] = val
254
+ else:
255
+ i = _skip_field(data, i, wire)
256
+ return out
257
+
258
+
259
+ @dataclass
260
+ class Envelope:
261
+ status: int = 0
262
+ transport_error: str = ""
263
+ data: bytes = b""
264
+ error: Optional[AppError] = None
265
+
266
+ # --- builders ---
267
+ @classmethod
268
+ def ok(cls, status: int, data: bytes) -> "Envelope":
269
+ return cls(status=status, data=bytes(data))
270
+
271
+ @classmethod
272
+ def err(
273
+ cls, status: int, code: str, message: str, *args: str
274
+ ) -> "Envelope":
275
+ return cls(
276
+ status=status,
277
+ error=AppError(code=code, message=message, args=list(args)),
278
+ )
279
+
280
+ @classmethod
281
+ def transport_err(cls, err: str) -> "Envelope":
282
+ return cls(transport_error=err)
283
+
284
+ # --- queries ---
285
+ def is_ok(self) -> bool:
286
+ return not self.transport_error and self.error is None
287
+
288
+ def is_transport_error(self) -> bool:
289
+ return bool(self.transport_error)
290
+
291
+ def is_app_error(self) -> bool:
292
+ return self.error is not None
293
+
294
+ def error_code(self) -> str:
295
+ return self.error.code if self.error else ""
296
+
297
+ def field_errors(self) -> dict[str, FieldError]:
298
+ if self.error is None or not self.error.details:
299
+ return {}
300
+ return {fe.field: fe for fe in self.error.details}
301
+
302
+ # --- wire encode/decode (matches protowire pb) ---
303
+ def encode(self) -> bytes:
304
+ out = bytearray()
305
+ # status is plain int32 (proto3 int32 wire encoding, sign-extended
306
+ # to a 10-byte varint for negative values). Matches the Go envelope
307
+ # struct tag `protowire:"1"` (no `,zigzag` option).
308
+ _enc_int32(out, 1, self.status)
309
+ _enc_string(out, 2, self.transport_error)
310
+ _enc_bytes(out, 3, self.data)
311
+ if self.error is not None:
312
+ _enc_submessage(out, 4, self.error.encode())
313
+ return bytes(out)
314
+
315
+ @classmethod
316
+ def decode(cls, data: bytes) -> "Envelope":
317
+ out = cls()
318
+ i = 0
319
+ n = len(data)
320
+ while i < n:
321
+ num, wire, i = _dec_tag(data, i)
322
+ if num == 1 and wire == 0:
323
+ v, i = _dec_varint(data, i)
324
+ out.status = _dec_int32(v)
325
+ elif num == 2 and wire == 2:
326
+ out.transport_error, i = _dec_string(data, i)
327
+ elif num == 3 and wire == 2:
328
+ out.data, i = _dec_bytes(data, i)
329
+ elif num == 4 and wire == 2:
330
+ sub, i = _dec_bytes(data, i)
331
+ out.error = AppError.decode(sub)
332
+ else:
333
+ i = _skip_field(data, i, wire)
334
+ return out
335
+
336
+
337
+ # Free-function aliases mirroring the Go API.
338
+ def OK(status: int, data: bytes) -> Envelope:
339
+ return Envelope.ok(status, data)
340
+
341
+
342
+ def Err(status: int, code: str, message: str, *args: str) -> Envelope:
343
+ return Envelope.err(status, code, message, *args)
344
+
345
+
346
+ def TransportErr(err: str) -> Envelope:
347
+ return Envelope.transport_err(err)
348
+
349
+
350
+ def NewAppError(code: str, message: str, *args: str) -> AppError:
351
+ return AppError(code=code, message=message, args=list(args))
protowire/pxf.py ADDED
@@ -0,0 +1,95 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 TrendVidia, LLC.
3
+ """PXF text ↔ protobuf Message — Python mirror of github.com/trendvidia/protowire/encoding/pxf.
4
+
5
+ The boundary with C++ is FileDescriptorSet bytes + binary proto bytes; Message
6
+ objects never cross. The wrapper handles conversion on each side.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Iterable
13
+
14
+ from google.protobuf.message import Message
15
+
16
+ from . import _protowire
17
+ from ._schema import fds_for_message
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Result:
22
+ """Field-level presence metadata, mirror of Go pxf.Result."""
23
+
24
+ set_paths: frozenset[str]
25
+ null_paths: frozenset[str]
26
+
27
+ def is_set(self, path: str) -> bool:
28
+ return path in self.set_paths and path not in self.null_paths
29
+
30
+ def is_null(self, path: str) -> bool:
31
+ return path in self.null_paths
32
+
33
+ def is_absent(self, path: str) -> bool:
34
+ return path not in self.set_paths and path not in self.null_paths
35
+
36
+ def null_fields(self) -> list[str]:
37
+ return sorted(self.null_paths)
38
+
39
+
40
+ def marshal(msg: Message) -> str:
41
+ """Encode `msg` as PXF text. Mirrors Go pxf.Marshal."""
42
+ fds = fds_for_message(msg)
43
+ return _protowire.pxf_marshal(msg.SerializeToString(), fds, msg.DESCRIPTOR.full_name)
44
+
45
+
46
+ # --- bytes-only helpers (used by the CLI / advanced callers) -------------
47
+
48
+
49
+ def marshal_bytes(msg_bytes: bytes, fds: bytes, full_name: str) -> str:
50
+ """Encode raw proto-binary bytes (against an explicit FDS) as PXF text."""
51
+ return _protowire.pxf_marshal(bytes(msg_bytes), bytes(fds), full_name)
52
+
53
+
54
+ def unmarshal_bytes(
55
+ data: str | bytes, fds: bytes, full_name: str, *, discard_unknown: bool = False
56
+ ) -> bytes:
57
+ """Decode PXF text into raw proto-binary bytes against an explicit FDS."""
58
+ text = data.encode("utf-8") if isinstance(data, str) else bytes(data)
59
+ return _protowire.pxf_unmarshal(text, bytes(fds), full_name, discard_unknown)
60
+
61
+
62
+ def unmarshal_full_bytes(
63
+ data: str | bytes, fds: bytes, full_name: str, *, discard_unknown: bool = False
64
+ ) -> tuple[bytes, Result]:
65
+ text = data.encode("utf-8") if isinstance(data, str) else bytes(data)
66
+ raw, set_paths, null_paths = _protowire.pxf_unmarshal_full(
67
+ text, bytes(fds), full_name, discard_unknown
68
+ )
69
+ return raw, Result(frozenset(set_paths), frozenset(null_paths))
70
+
71
+
72
+ def unmarshal(data: str | bytes, msg: Message, *, discard_unknown: bool = False) -> None:
73
+ """Decode PXF text into `msg` (in place). Mirrors Go pxf.Unmarshal."""
74
+ text = data.encode("utf-8") if isinstance(data, str) else bytes(data)
75
+ fds = fds_for_message(msg)
76
+ raw = _protowire.pxf_unmarshal(text, fds, msg.DESCRIPTOR.full_name, discard_unknown)
77
+ msg.Clear()
78
+ msg.MergeFromString(raw)
79
+
80
+
81
+ def unmarshal_full(
82
+ data: str | bytes, msg: Message, *, discard_unknown: bool = False
83
+ ) -> Result:
84
+ """Decode PXF + return per-field presence (set/null) metadata.
85
+
86
+ Mirrors Go pxf.UnmarshalFull.
87
+ """
88
+ text = data.encode("utf-8") if isinstance(data, str) else bytes(data)
89
+ fds = fds_for_message(msg)
90
+ raw, set_paths, null_paths = _protowire.pxf_unmarshal_full(
91
+ text, fds, msg.DESCRIPTOR.full_name, discard_unknown
92
+ )
93
+ msg.Clear()
94
+ msg.MergeFromString(raw)
95
+ return Result(frozenset(set_paths), frozenset(null_paths))
protowire/py.typed ADDED
File without changes
protowire/sbe.py ADDED
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 TrendVidia, LLC.
3
+ """SBE codec — Python mirror of github.com/trendvidia/protowire-go/encoding/sbe."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Iterable
8
+
9
+ from google.protobuf.descriptor import FileDescriptor
10
+ from google.protobuf.message import Message
11
+
12
+ from . import _protowire
13
+ from ._schema import fds_for_files
14
+
15
+
16
+ class View:
17
+ """Zero-copy view over an SBE-encoded buffer.
18
+
19
+ Wraps the native ``_protowire.View``. The underlying data is owned by
20
+ the native object (a shared heap copy of the input bytes) and stays alive
21
+ as long as any view, sub-view, or group entry referencing it is alive.
22
+ """
23
+
24
+ __slots__ = ("_native",)
25
+
26
+ def __init__(self, native: "_protowire.View") -> None:
27
+ self._native = native
28
+
29
+ def int(self, name: str) -> int:
30
+ return self._native.int(name)
31
+
32
+ def uint(self, name: str) -> int:
33
+ return self._native.uint(name)
34
+
35
+ def float(self, name: str) -> float:
36
+ return self._native.float(name)
37
+
38
+ def bool(self, name: str) -> bool:
39
+ return self._native.bool(name)
40
+
41
+ def string(self, name: str) -> str:
42
+ return self._native.string(name)
43
+
44
+ def bytes(self, name: str) -> bytes:
45
+ """Read a fixed-length bytes field as the full N-byte slice (no trim)."""
46
+ return self._native.bytes(name)
47
+
48
+ def composite(self, name: str) -> "View":
49
+ """Sub-view over a non-repeated nested message (SBE composite)."""
50
+ return View(self._native.composite(name))
51
+
52
+ def group(self, name: str) -> "GroupView":
53
+ """View over a repeating group field."""
54
+ return GroupView(self._native.group(name))
55
+
56
+
57
+ class GroupView:
58
+ """View over an SBE repeating group. Iterate via :py:meth:`entry`."""
59
+
60
+ __slots__ = ("_native",)
61
+
62
+ def __init__(self, native: "_protowire.GroupView") -> None:
63
+ self._native = native
64
+
65
+ def len(self) -> int:
66
+ return self._native.len()
67
+
68
+ def __len__(self) -> int:
69
+ return self._native.len()
70
+
71
+ def entry(self, i: int) -> View:
72
+ return View(self._native.entry(i))
73
+
74
+
75
+ class Codec:
76
+ """SBE codec built from one or more proto FileDescriptors with SBE annotations."""
77
+
78
+ def __init__(self, files: Iterable[FileDescriptor]) -> None:
79
+ files = list(files)
80
+ if not files:
81
+ raise ValueError("sbe.Codec requires at least one FileDescriptor")
82
+ fds = fds_for_files(files)
83
+ # Pass the *selected* file names so the C++ Codec only registers those
84
+ # — transitive dep files (descriptor.proto, sbe/annotations.proto)
85
+ # don't carry an (sbe.schema_id) option and would otherwise fail.
86
+ self._impl = _protowire.SbeCodec.create(fds, [f.name for f in files])
87
+
88
+ @classmethod
89
+ def from_message(cls, msg_type: type[Message]) -> "Codec":
90
+ return cls([msg_type.DESCRIPTOR.file])
91
+
92
+ def marshal(self, msg: Message) -> bytes:
93
+ return self._impl.marshal(msg.SerializeToString(), msg.DESCRIPTOR.full_name)
94
+
95
+ def unmarshal(self, data: bytes, msg: Message) -> None:
96
+ raw = self._impl.unmarshal(bytes(data), msg.DESCRIPTOR.full_name)
97
+ msg.Clear()
98
+ msg.MergeFromString(raw)
99
+
100
+ def view(self, data: bytes) -> View:
101
+ return View(self._impl.new_view(bytes(data)))
@@ -0,0 +1,2 @@
1
+ Version: 1.12.1
2
+ Arguments: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-bpz2baut\\cp311-win_amd64\\build\\venv\\Scripts\\delvewheel', 'repair', '--add-path', 'C:\\vcpkg\\installed\\x64-windows\\bin', '-w', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-bpz2baut\\cp311-win_amd64\\repaired_wheel', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-bpz2baut\\cp311-win_amd64\\built_wheel\\protowire_python-0.70.0-cp311-cp311-win_amd64.whl']
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: protowire-python
3
+ Version: 0.70.0
4
+ Summary: Python wrapper around protowire-cpp — PXF text, SBE binary, and envelope codecs.
5
+ Keywords: protobuf,pxf,sbe,wire-format,fix,trading
6
+ Author-Email: "TrendVidia, LLC" <open-source@trendvidia.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: C++
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: System :: Networking
23
+ Classifier: Typing :: Typed
24
+ Project-URL: Homepage, https://protowire.org
25
+ Project-URL: Repository, https://github.com/trendvidia/protowire-python
26
+ Project-URL: Bug Tracker, https://github.com/trendvidia/protowire-python/issues
27
+ Project-URL: Changelog, https://github.com/trendvidia/protowire-python/blob/main/CHANGELOG.md
28
+ Project-URL: Specification, https://github.com/trendvidia/protowire
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: protobuf>=4.0
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7; extra == "test"
33
+ Description-Content-Type: text/markdown
34
+
35
+ # protowire-python
36
+
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
38
+ [![PyPI](https://img.shields.io/pypi/v/protowire-python.svg)](https://pypi.org/project/protowire-python/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/protowire-python.svg)](https://pypi.org/project/protowire-python/)
40
+ [![CI](https://github.com/trendvidia/protowire-python/actions/workflows/ci.yml/badge.svg)](https://github.com/trendvidia/protowire-python/actions/workflows/ci.yml)
41
+
42
+ Python port of [protowire](https://protowire.org) — a protobuf-backed wire-format
43
+ toolkit. CPython 3.10+, MIT, [nanobind](https://github.com/wjakob/nanobind) FFI
44
+ over [`protowire-cpp`](https://github.com/trendvidia/protowire-cpp). Verified
45
+ for byte-equivalence against the canonical Go reference and seven other
46
+ sibling ports.
47
+
48
+ The native extension uses [nanobind](https://github.com/wjakob/nanobind) with
49
+ [scikit-build-core](https://github.com/scikit-build/scikit-build-core) as the
50
+ build backend. The FFI boundary is intentionally narrow: Python sends a
51
+ serialized `FileDescriptorSet` plus a fully-qualified message name; binary
52
+ proto bytes flow back. `google.protobuf.Message` objects never cross the
53
+ language boundary.
54
+
55
+ ## Install
56
+
57
+ ```sh
58
+ pip install protowire-python
59
+ ```
60
+
61
+ The PyPI distribution is named `protowire-python` (the bare `protowire`
62
+ name was taken). The import name stays `protowire`:
63
+
64
+ ```python
65
+ from protowire import pxf, sbe, envelope
66
+ ```
67
+
68
+ Wheels are published for CPython 3.10–3.13 on Linux × {x86_64, aarch64},
69
+ macOS × {x86_64, arm64}, and Windows × x86_64. On other platforms `pip`
70
+ will fall back to a source build (requires CMake ≥ 3.20 and a C++20
71
+ compiler).
72
+
73
+ ## API
74
+
75
+ ```python
76
+ from protowire import pxf, sbe, envelope
77
+
78
+ # PXF — schema implicit in the message type.
79
+ text = pxf.marshal(my_msg)
80
+ pxf.unmarshal(text, my_msg)
81
+ result = pxf.unmarshal_full(text, my_msg)
82
+ result.is_set("nested.value"), result.is_null("flag")
83
+
84
+ # SBE — codec built from one or more FileDescriptors with sbe annotations.
85
+ codec = sbe.Codec.from_message(OrderType)
86
+ data = codec.marshal(order)
87
+ codec.unmarshal(data, order_out)
88
+ view = codec.view(data); view.uint("order_id")
89
+
90
+ # Envelope — wire-compatible with the Go envelope package.
91
+ e = envelope.OK(200, b"payload")
92
+ e = envelope.Err(400, "VALIDATION", "bad input").error.with_field(
93
+ "name", "REQUIRED", "missing"
94
+ )
95
+ ```
96
+
97
+ ## Build from source
98
+
99
+ ```sh
100
+ git clone https://github.com/trendvidia/protowire-cpp.git ../protowire-cpp
101
+
102
+ python3 -m venv .venv
103
+ source .venv/bin/activate
104
+ pip install -e '.[test]'
105
+
106
+ pytest
107
+ ```
108
+
109
+ The build looks for [`protowire-cpp`](https://github.com/trendvidia/protowire-cpp)
110
+ at `../protowire-cpp` by default. Override with
111
+ `PROTOWIRE_CPP_DIR=/abs/path pip install -e .` or
112
+ `pip install -e . --config-settings=cmake.define.PROTOWIRE_CPP_DIR=/abs/path`.
113
+
114
+ Required: CMake ≥ 3.20, a C++20 compiler, protobuf headers + libs.
115
+
116
+ - Linux: `apt-get install protobuf-compiler libprotobuf-dev libprotoc-dev`
117
+ - macOS: `brew install protobuf`
118
+ - Windows: `vcpkg install protobuf` and pass the toolchain file via
119
+ `CMAKE_TOOLCHAIN_FILE`
120
+
121
+ ## Command-line tool
122
+
123
+ The `protowire` CLI is shared across every port and lives in the spec repo at
124
+ [github.com/trendvidia/protowire/cmd/protowire](https://github.com/trendvidia/protowire/tree/main/cmd/protowire).
125
+ Install:
126
+
127
+ ```sh
128
+ go install github.com/trendvidia/protowire/cmd/protowire@latest
129
+ ```
130
+
131
+ Python users use this library for in-process encode/decode and the shared CLI
132
+ for command-line operations. There is no separate Python CLI binary.
133
+
134
+ ## Wire compatibility
135
+
136
+ Verified manually against the Go module:
137
+
138
+ - Go `pxf.Marshal` → file → Python `pxf.unmarshal` round-trips a representative AllTypes message.
139
+ - Python `pxf.marshal` → file → Go `pxf.Unmarshal` round-trips equally.
140
+
141
+ Because the wire codec is the C++ one, this port inherits all of
142
+ [`protowire-cpp`](https://github.com/trendvidia/protowire-cpp)'s
143
+ cross-port equivalence guarantees.
144
+
145
+ ## Limitations & open gaps
146
+
147
+ - **No pure-Python fallback.** A C++ toolchain (clang or gcc, plus CMake) is
148
+ required at install time on platforms where we don't ship a wheel.
149
+ Pure-`google.protobuf`-Python encode/decode without C++ is not available —
150
+ opening that up is a meaningful refactor and would need a separate decoder
151
+ path.
152
+ - **The FFI is narrow on purpose.** `google.protobuf.Message` objects never
153
+ cross the boundary — Python sends a `FileDescriptorSet` + fully-qualified
154
+ message name and bytes flow back. This keeps the C++ side type-stable but
155
+ means Python callers serialize their messages once before each call. A
156
+ `MessageView`-style zero-copy path would be welcome.
157
+ - **No standalone Python CLI.** The shared CLI lives in
158
+ [trendvidia/protowire/cmd/protowire](https://github.com/trendvidia/protowire/tree/main/cmd/protowire);
159
+ Python callers either invoke that binary or use the in-process API.
160
+ - **Free-threaded Python (PEP 703 / 3.13t)** is untested. nanobind supports
161
+ it but the build hasn't been validated against `--disable-gil` interpreters.
162
+
163
+ ## Repository layout
164
+
165
+ ```
166
+ protowire-python/
167
+ ├── LICENSE # MIT
168
+ ├── README.md
169
+ ├── CHANGELOG.md
170
+ ├── CONTRIBUTING.md, SECURITY.md,
171
+ │ GOVERNANCE.md, CODE_OF_CONDUCT.md
172
+ ├── pyproject.toml # scikit-build-core + nanobind
173
+ ├── CMakeLists.txt # links protowire-cpp
174
+ ├── src/_protowire/module.cc # FFI entry point (nanobind)
175
+ ├── src/protowire/ # pure-Python public API
176
+ ├── tests/ # pytest suites
177
+ ├── testdata/ # .proto fixtures
178
+ ├── scripts/ # cross-port test harnesses
179
+ └── .github/ # CI: build matrix + cibuildwheel + CodeQL
180
+ ```
@@ -0,0 +1,15 @@
1
+ protowire/envelope.py,sha256=imc-4P6Vi0U7YFq03r10qY_2g98tQg5BnU-6B9dtg7I,10754
2
+ protowire/pxf.py,sha256=d3DDTAxrUv3kR1s_FnpN-KFwaI16UCxURadj4m52E78,3428
3
+ protowire/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ protowire/sbe.py,sha256=PiBRb_ogwiFcF2_QqLH_4OlHAiv53VSXD2FCIEdUFtM,3391
5
+ protowire/_protowire.cp311-win_amd64.pyd,sha256=k0Nivj2g5VEf2xxDYcSQbZG-E78zXlRzbRusjziooWU,317440
6
+ protowire/_schema.py,sha256=KxbAd4gayQVS7x1vzMLRPiQ6Y5m3EVDMOrGWh1OsEzo,2094
7
+ protowire/__init__.py,sha256=3E3c_xZB0G1_yR4x7jDAkskLfbk3chUQbdHCYyyPdTY,587
8
+ protowire_python-0.70.0.dist-info/DELVEWHEEL,sha256=XvLnXJpuuhw0eWEf6qYHWgDhrOe7zg8TpZObxxd1hrs,466
9
+ protowire_python-0.70.0.dist-info/METADATA,sha256=z-iClluZRXU5Th7yJeq1ZP1pIRt2qGM0JfQuLE9iV-A,7412
10
+ protowire_python-0.70.0.dist-info/RECORD,,
11
+ protowire_python-0.70.0.dist-info/WHEEL,sha256=NHmw5Qi_104FW4Xq2pbA3Gs3F6EqwqCa-xRmRnEiDI8,106
12
+ protowire_python-0.70.0.dist-info/licenses/LICENSE,sha256=E9M3EvB_npBXeSxf_HvG1pXpnZOARKMCJeegMRA8eWo,1094
13
+ protowire_python.libs/abseil_dll-82db16a5872b9152133f5e2ff4143de1.dll,sha256=mYxIAX8MhP8tlsd_6Mi6E7aOacKSkkiPwRvsOddpyrc,2002944
14
+ protowire_python.libs/libprotobuf-d2223d7553419e37c8b847b8bfc74ff0.dll,sha256=sYFlBXDwwN0vXes4admd-76bUTdHZNBNbuTunqkebAk,11296256
15
+ protowire_python.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll,sha256=pMIim9wqKmMKzcCVtNhgCOXD47x3cxdDVPPaT1vrnN4,575056
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 0.12.2
3
+ Root-Is-Purelib: false
4
+ Tag: cp311-cp311-win_amd64
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TrendVidia, LLC.
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
13
+ all 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
21
+ THE SOFTWARE.