proto 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.
- proto-0.1.0/LICENSE +21 -0
- proto-0.1.0/PKG-INFO +238 -0
- proto-0.1.0/README.md +212 -0
- proto-0.1.0/pyproject.toml +41 -0
- proto-0.1.0/setup.cfg +4 -0
- proto-0.1.0/src/proto/__init__.py +47 -0
- proto-0.1.0/src/proto/errors.py +21 -0
- proto-0.1.0/src/proto/message.py +535 -0
- proto-0.1.0/src/proto/py.typed +0 -0
- proto-0.1.0/src/proto/scalars.py +140 -0
- proto-0.1.0/src/proto/schema.py +57 -0
- proto-0.1.0/src/proto/stream.py +62 -0
- proto-0.1.0/src/proto/wire.py +160 -0
- proto-0.1.0/src/proto.egg-info/PKG-INFO +238 -0
- proto-0.1.0/src/proto.egg-info/SOURCES.txt +19 -0
- proto-0.1.0/src/proto.egg-info/dependency_links.txt +1 -0
- proto-0.1.0/src/proto.egg-info/requires.txt +3 -0
- proto-0.1.0/src/proto.egg-info/top_level.txt +1 -0
- proto-0.1.0/tests/test_message.py +292 -0
- proto-0.1.0/tests/test_stream_and_schema.py +97 -0
- proto-0.1.0/tests/test_wire.py +98 -0
proto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nehz
|
|
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.
|
proto-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Schema-first, protobuf wire-compatible binary messages for pure Python, declared with dataclasses.
|
|
5
|
+
Author: nehz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: protobuf,protocol-buffers,serialization,binary,varint,dataclasses,schema
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Networking
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# proto
|
|
28
|
+
|
|
29
|
+
**Protocol Buffers without the toolchain.** `proto` lets you declare binary
|
|
30
|
+
messages as ordinary Python dataclasses and serialize them to bytes that are
|
|
31
|
+
wire-compatible with Google's Protocol Buffers (proto3). No `protoc`, no
|
|
32
|
+
generated code, no C extension, no dependencies: just the standard library.
|
|
33
|
+
|
|
34
|
+
Use it when you need to talk to a protobuf service from a script, persist
|
|
35
|
+
compact records, or prototype a schema in Python first, and still have every
|
|
36
|
+
other protobuf implementation read your bytes.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import proto
|
|
40
|
+
|
|
41
|
+
@proto.message
|
|
42
|
+
class Point:
|
|
43
|
+
x: int = proto.field(1, "sint32")
|
|
44
|
+
y: int = proto.field(2, "sint32")
|
|
45
|
+
|
|
46
|
+
data = proto.encode(Point(3, -4)) # b'\x08\x06\x10\x07'
|
|
47
|
+
assert proto.decode(Point, data) == Point(3, -4)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **Wire-compatible**: varints, zigzag, fixed-width, length-delimited and packed
|
|
53
|
+
encodings match the official protobuf encoding byte for byte. The tests check
|
|
54
|
+
the exact byte strings from the protobuf encoding guide.
|
|
55
|
+
- **Schema-first dataclasses**: field numbers live next to the type annotations.
|
|
56
|
+
Messages are real `dataclasses`, so you keep `==`, `repr`, `replace()` and
|
|
57
|
+
your type checker.
|
|
58
|
+
- **All 15 scalar types**: `int32 int64 uint32 uint64 sint32 sint64 bool
|
|
59
|
+
fixed32 fixed64 sfixed32 sfixed64 float double string bytes`, plus `IntEnum`
|
|
60
|
+
enums, nested and recursive messages, `repeated` fields, and proto3 `optional`.
|
|
61
|
+
- **proto3 semantics**: default values are omitted on the wire, unknown fields are
|
|
62
|
+
skipped, packed and unpacked repeated fields are both accepted, unknown enum
|
|
63
|
+
values are kept as `int` (open enums).
|
|
64
|
+
- **Strict validation**: out-of-range integers, wrong Python types, malformed or
|
|
65
|
+
truncated input raise clear `EncodeError` / `DecodeError` / `SchemaError`.
|
|
66
|
+
- **`.proto` export**: `to_proto()` renders your classes as a `.proto` file, so
|
|
67
|
+
other languages can generate code from the same schema.
|
|
68
|
+
- **Streaming**: length-delimited framing (`writeDelimitedTo` format) for many
|
|
69
|
+
messages in one file or socket.
|
|
70
|
+
- Pure Python 3.10+, fully type-hinted (`py.typed`), zero dependencies.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install proto # once published
|
|
76
|
+
pip install -e . # from a checkout
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> Note: Google's `proto-plus` package also installs a top-level `proto` module.
|
|
80
|
+
> Don't install both in the same environment.
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from __future__ import annotations
|
|
86
|
+
|
|
87
|
+
import enum
|
|
88
|
+
import io
|
|
89
|
+
|
|
90
|
+
import proto
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Role(enum.IntEnum):
|
|
94
|
+
ROLE_UNSPECIFIED = 0 # proto3 enums need a zero value
|
|
95
|
+
ADMIN = 1
|
|
96
|
+
MEMBER = 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@proto.message
|
|
100
|
+
class Address:
|
|
101
|
+
city: str = proto.field(1)
|
|
102
|
+
zip_code: str = proto.field(2)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@proto.message
|
|
106
|
+
class User:
|
|
107
|
+
name: str = proto.field(1) # inferred: string
|
|
108
|
+
id: int = proto.field(2, "uint32") # explicit scalar type
|
|
109
|
+
age: int | None = proto.field(3, "int32") # proto3 `optional`: None = unset
|
|
110
|
+
role: Role = proto.field(4) # enum
|
|
111
|
+
emails: list[str] = proto.field(5) # repeated
|
|
112
|
+
scores: list[int] = proto.field(6, "sint32") # repeated, packed by default
|
|
113
|
+
address: Address | None = proto.field(7) # nested message
|
|
114
|
+
friends: list[User] = proto.field(8) # recursive
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
ada = User("ada", id=1, role=Role.ADMIN, emails=["ada@example.com"],
|
|
118
|
+
address=Address("London", "N1"))
|
|
119
|
+
|
|
120
|
+
data = ada.to_bytes() # same as proto.encode(ada)
|
|
121
|
+
again = User.from_bytes(data) # same as proto.decode(User, data)
|
|
122
|
+
assert again == ada
|
|
123
|
+
|
|
124
|
+
proto.to_dict(ada)
|
|
125
|
+
# {'name': 'ada', 'id': 1, 'role': 'ADMIN', 'emails': ['ada@example.com'],
|
|
126
|
+
# 'scores': [], 'address': {'city': 'London', 'zip_code': 'N1'}, 'friends': []}
|
|
127
|
+
|
|
128
|
+
# Many messages in one stream
|
|
129
|
+
buf = io.BytesIO()
|
|
130
|
+
for user in (ada, User("bob", id=2)):
|
|
131
|
+
proto.write_delimited(buf, user)
|
|
132
|
+
buf.seek(0)
|
|
133
|
+
names = [u.name for u in proto.iter_delimited(User, buf)] # ['ada', 'bob']
|
|
134
|
+
|
|
135
|
+
print(proto.to_proto(User, package="example.v1"))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The last line prints:
|
|
139
|
+
|
|
140
|
+
```proto
|
|
141
|
+
syntax = "proto3";
|
|
142
|
+
|
|
143
|
+
package example.v1;
|
|
144
|
+
|
|
145
|
+
enum Role {
|
|
146
|
+
ROLE_UNSPECIFIED = 0;
|
|
147
|
+
ADMIN = 1;
|
|
148
|
+
MEMBER = 2;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
message Address {
|
|
152
|
+
string city = 1;
|
|
153
|
+
string zip_code = 2;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
message User {
|
|
157
|
+
string name = 1;
|
|
158
|
+
uint32 id = 2;
|
|
159
|
+
optional int32 age = 3;
|
|
160
|
+
Role role = 4;
|
|
161
|
+
repeated string emails = 5;
|
|
162
|
+
repeated sint32 scores = 6;
|
|
163
|
+
Address address = 7;
|
|
164
|
+
repeated User friends = 8;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Declaring fields
|
|
169
|
+
|
|
170
|
+
| Annotation | Inferred `.proto` type | Default when omitted |
|
|
171
|
+
|-------------------------|------------------------|----------------------|
|
|
172
|
+
| `int` | `int64` | `0` |
|
|
173
|
+
| `float` | `double` | `0.0` |
|
|
174
|
+
| `bool` | `bool` | `False` |
|
|
175
|
+
| `str` | `string` | `""` |
|
|
176
|
+
| `bytes` | `bytes` | `b""` |
|
|
177
|
+
| `SomeIntEnum` | `SomeIntEnum` | the member with value `0` |
|
|
178
|
+
| `SomeMessage \| None` | `SomeMessage` | `None` (unset) |
|
|
179
|
+
| `T \| None` (scalar) | `optional T` | `None` (unset) |
|
|
180
|
+
| `list[T]` | `repeated T` | `[]` |
|
|
181
|
+
|
|
182
|
+
Pass `type=` (the second argument of `field`) to choose another scalar
|
|
183
|
+
encoding such as `"sint32"` or `"fixed64"`, or to name an enum/message class
|
|
184
|
+
explicitly.
|
|
185
|
+
|
|
186
|
+
## API overview
|
|
187
|
+
|
|
188
|
+
Everything is importable from the top-level `proto` package.
|
|
189
|
+
|
|
190
|
+
| Name | Description |
|
|
191
|
+
|------|-------------|
|
|
192
|
+
| `@message` / `@message(name="Wire")` | Class decorator. Turns an annotated class into a dataclass-based message and adds `to_bytes()` and classmethod `from_bytes(data)`. `name` sets the name used by `to_proto` (default: class name). |
|
|
193
|
+
| `field(number, type=None, *, default=..., default_factory=..., packed=None)` | Declares a field. `type` is a scalar name, an `IntEnum` subclass or a message class; it is inferred from the annotation when omitted. `packed=False` disables packed encoding for repeated numeric/enum fields. Every annotated attribute must use `field()`. |
|
|
194
|
+
| `encode(msg) -> bytes` | Serialize a message instance. |
|
|
195
|
+
| `decode(cls, data) -> cls` | Parse `bytes`/`bytearray`/`memoryview` into a new `cls` instance. Absent fields get their proto3 zero value; the last occurrence of a singular field wins. |
|
|
196
|
+
| `fields(cls) -> tuple[FieldInfo, ...]` | Resolved schema of a message class, ordered by field number. |
|
|
197
|
+
| `FieldInfo` | Frozen dataclass: `name`, `number`, `kind` (`"scalar"`/`"enum"`/`"message"`), `type_name`, `repeated`, `optional`, `packed`, `scalar`, `target`, and property `wire_type`. |
|
|
198
|
+
| `is_message(obj) -> bool` | True for `@message` classes and their instances. |
|
|
199
|
+
| `to_dict(msg) -> dict` | Plain-dict view: nested messages become dicts, enums become names, unset optionals are omitted. |
|
|
200
|
+
| `from_dict(cls, data) -> cls` | Inverse of `to_dict`; enums may be given by name or number. |
|
|
201
|
+
| `to_proto(*classes, package=None) -> str` | Render the classes and every enum/message they reference as proto3 source. |
|
|
202
|
+
| `write_delimited(stream, msg) -> int` | Write a varint length prefix plus the message; returns bytes written. |
|
|
203
|
+
| `read_delimited(cls, stream) -> cls \| None` | Read one framed message; `None` at a clean end of stream. |
|
|
204
|
+
| `iter_delimited(cls, stream)` | Iterate framed messages until the stream is exhausted. |
|
|
205
|
+
| `ProtoError` | Base exception. Subclasses: `SchemaError` (also a `TypeError`), `EncodeError` and `DecodeError` (also `ValueError`). |
|
|
206
|
+
| `__version__` | `"0.1.0"` |
|
|
207
|
+
|
|
208
|
+
Low-level primitives live in `proto.wire`:
|
|
209
|
+
|
|
210
|
+
| Name | Description |
|
|
211
|
+
|------|-------------|
|
|
212
|
+
| `WireType` | `IntEnum`: `VARINT`, `I64`, `LEN`, `SGROUP`, `EGROUP`, `I32`. |
|
|
213
|
+
| `MAX_FIELD_NUMBER` | `2**29 - 1`. |
|
|
214
|
+
| `encode_varint(value) -> bytes` | Base-128 varint; negatives use 64-bit two's complement. |
|
|
215
|
+
| `decode_varint(data, pos=0) -> (value, new_pos)` | Decode one varint. |
|
|
216
|
+
| `zigzag_encode(value, bits=64) -> int` / `zigzag_decode(value) -> int` | ZigZag mapping used by `sint32`/`sint64`. |
|
|
217
|
+
| `encode_tag(number, wire_type) -> bytes` | Field key. |
|
|
218
|
+
| `decode_tag(data, pos=0) -> (number, wire_type, new_pos)` | Parse a field key. |
|
|
219
|
+
| `skip_field(data, pos, wire_type, number=None) -> int` | Skip an unknown field's payload, groups included. |
|
|
220
|
+
|
|
221
|
+
## Limitations
|
|
222
|
+
|
|
223
|
+
`proto` 0.1 covers the core of proto3. Not supported yet: `oneof`, `map<K, V>`
|
|
224
|
+
fields, well-known types (`Timestamp`, `Any`, ...), proto2 groups (they are
|
|
225
|
+
skipped when decoding), services/gRPC, and merging of repeated occurrences of
|
|
226
|
+
a singular message field (the last occurrence wins). Annotations that name
|
|
227
|
+
other classes must be resolvable from the scope where the class is defined.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v # standard library only
|
|
233
|
+
python3 -m pytest # if pytest is installed
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
proto-0.1.0/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# proto
|
|
2
|
+
|
|
3
|
+
**Protocol Buffers without the toolchain.** `proto` lets you declare binary
|
|
4
|
+
messages as ordinary Python dataclasses and serialize them to bytes that are
|
|
5
|
+
wire-compatible with Google's Protocol Buffers (proto3). No `protoc`, no
|
|
6
|
+
generated code, no C extension, no dependencies: just the standard library.
|
|
7
|
+
|
|
8
|
+
Use it when you need to talk to a protobuf service from a script, persist
|
|
9
|
+
compact records, or prototype a schema in Python first, and still have every
|
|
10
|
+
other protobuf implementation read your bytes.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import proto
|
|
14
|
+
|
|
15
|
+
@proto.message
|
|
16
|
+
class Point:
|
|
17
|
+
x: int = proto.field(1, "sint32")
|
|
18
|
+
y: int = proto.field(2, "sint32")
|
|
19
|
+
|
|
20
|
+
data = proto.encode(Point(3, -4)) # b'\x08\x06\x10\x07'
|
|
21
|
+
assert proto.decode(Point, data) == Point(3, -4)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Wire-compatible**: varints, zigzag, fixed-width, length-delimited and packed
|
|
27
|
+
encodings match the official protobuf encoding byte for byte. The tests check
|
|
28
|
+
the exact byte strings from the protobuf encoding guide.
|
|
29
|
+
- **Schema-first dataclasses**: field numbers live next to the type annotations.
|
|
30
|
+
Messages are real `dataclasses`, so you keep `==`, `repr`, `replace()` and
|
|
31
|
+
your type checker.
|
|
32
|
+
- **All 15 scalar types**: `int32 int64 uint32 uint64 sint32 sint64 bool
|
|
33
|
+
fixed32 fixed64 sfixed32 sfixed64 float double string bytes`, plus `IntEnum`
|
|
34
|
+
enums, nested and recursive messages, `repeated` fields, and proto3 `optional`.
|
|
35
|
+
- **proto3 semantics**: default values are omitted on the wire, unknown fields are
|
|
36
|
+
skipped, packed and unpacked repeated fields are both accepted, unknown enum
|
|
37
|
+
values are kept as `int` (open enums).
|
|
38
|
+
- **Strict validation**: out-of-range integers, wrong Python types, malformed or
|
|
39
|
+
truncated input raise clear `EncodeError` / `DecodeError` / `SchemaError`.
|
|
40
|
+
- **`.proto` export**: `to_proto()` renders your classes as a `.proto` file, so
|
|
41
|
+
other languages can generate code from the same schema.
|
|
42
|
+
- **Streaming**: length-delimited framing (`writeDelimitedTo` format) for many
|
|
43
|
+
messages in one file or socket.
|
|
44
|
+
- Pure Python 3.10+, fully type-hinted (`py.typed`), zero dependencies.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install proto # once published
|
|
50
|
+
pip install -e . # from a checkout
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> Note: Google's `proto-plus` package also installs a top-level `proto` module.
|
|
54
|
+
> Don't install both in the same environment.
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import enum
|
|
62
|
+
import io
|
|
63
|
+
|
|
64
|
+
import proto
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Role(enum.IntEnum):
|
|
68
|
+
ROLE_UNSPECIFIED = 0 # proto3 enums need a zero value
|
|
69
|
+
ADMIN = 1
|
|
70
|
+
MEMBER = 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@proto.message
|
|
74
|
+
class Address:
|
|
75
|
+
city: str = proto.field(1)
|
|
76
|
+
zip_code: str = proto.field(2)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@proto.message
|
|
80
|
+
class User:
|
|
81
|
+
name: str = proto.field(1) # inferred: string
|
|
82
|
+
id: int = proto.field(2, "uint32") # explicit scalar type
|
|
83
|
+
age: int | None = proto.field(3, "int32") # proto3 `optional`: None = unset
|
|
84
|
+
role: Role = proto.field(4) # enum
|
|
85
|
+
emails: list[str] = proto.field(5) # repeated
|
|
86
|
+
scores: list[int] = proto.field(6, "sint32") # repeated, packed by default
|
|
87
|
+
address: Address | None = proto.field(7) # nested message
|
|
88
|
+
friends: list[User] = proto.field(8) # recursive
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
ada = User("ada", id=1, role=Role.ADMIN, emails=["ada@example.com"],
|
|
92
|
+
address=Address("London", "N1"))
|
|
93
|
+
|
|
94
|
+
data = ada.to_bytes() # same as proto.encode(ada)
|
|
95
|
+
again = User.from_bytes(data) # same as proto.decode(User, data)
|
|
96
|
+
assert again == ada
|
|
97
|
+
|
|
98
|
+
proto.to_dict(ada)
|
|
99
|
+
# {'name': 'ada', 'id': 1, 'role': 'ADMIN', 'emails': ['ada@example.com'],
|
|
100
|
+
# 'scores': [], 'address': {'city': 'London', 'zip_code': 'N1'}, 'friends': []}
|
|
101
|
+
|
|
102
|
+
# Many messages in one stream
|
|
103
|
+
buf = io.BytesIO()
|
|
104
|
+
for user in (ada, User("bob", id=2)):
|
|
105
|
+
proto.write_delimited(buf, user)
|
|
106
|
+
buf.seek(0)
|
|
107
|
+
names = [u.name for u in proto.iter_delimited(User, buf)] # ['ada', 'bob']
|
|
108
|
+
|
|
109
|
+
print(proto.to_proto(User, package="example.v1"))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The last line prints:
|
|
113
|
+
|
|
114
|
+
```proto
|
|
115
|
+
syntax = "proto3";
|
|
116
|
+
|
|
117
|
+
package example.v1;
|
|
118
|
+
|
|
119
|
+
enum Role {
|
|
120
|
+
ROLE_UNSPECIFIED = 0;
|
|
121
|
+
ADMIN = 1;
|
|
122
|
+
MEMBER = 2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
message Address {
|
|
126
|
+
string city = 1;
|
|
127
|
+
string zip_code = 2;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
message User {
|
|
131
|
+
string name = 1;
|
|
132
|
+
uint32 id = 2;
|
|
133
|
+
optional int32 age = 3;
|
|
134
|
+
Role role = 4;
|
|
135
|
+
repeated string emails = 5;
|
|
136
|
+
repeated sint32 scores = 6;
|
|
137
|
+
Address address = 7;
|
|
138
|
+
repeated User friends = 8;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Declaring fields
|
|
143
|
+
|
|
144
|
+
| Annotation | Inferred `.proto` type | Default when omitted |
|
|
145
|
+
|-------------------------|------------------------|----------------------|
|
|
146
|
+
| `int` | `int64` | `0` |
|
|
147
|
+
| `float` | `double` | `0.0` |
|
|
148
|
+
| `bool` | `bool` | `False` |
|
|
149
|
+
| `str` | `string` | `""` |
|
|
150
|
+
| `bytes` | `bytes` | `b""` |
|
|
151
|
+
| `SomeIntEnum` | `SomeIntEnum` | the member with value `0` |
|
|
152
|
+
| `SomeMessage \| None` | `SomeMessage` | `None` (unset) |
|
|
153
|
+
| `T \| None` (scalar) | `optional T` | `None` (unset) |
|
|
154
|
+
| `list[T]` | `repeated T` | `[]` |
|
|
155
|
+
|
|
156
|
+
Pass `type=` (the second argument of `field`) to choose another scalar
|
|
157
|
+
encoding such as `"sint32"` or `"fixed64"`, or to name an enum/message class
|
|
158
|
+
explicitly.
|
|
159
|
+
|
|
160
|
+
## API overview
|
|
161
|
+
|
|
162
|
+
Everything is importable from the top-level `proto` package.
|
|
163
|
+
|
|
164
|
+
| Name | Description |
|
|
165
|
+
|------|-------------|
|
|
166
|
+
| `@message` / `@message(name="Wire")` | Class decorator. Turns an annotated class into a dataclass-based message and adds `to_bytes()` and classmethod `from_bytes(data)`. `name` sets the name used by `to_proto` (default: class name). |
|
|
167
|
+
| `field(number, type=None, *, default=..., default_factory=..., packed=None)` | Declares a field. `type` is a scalar name, an `IntEnum` subclass or a message class; it is inferred from the annotation when omitted. `packed=False` disables packed encoding for repeated numeric/enum fields. Every annotated attribute must use `field()`. |
|
|
168
|
+
| `encode(msg) -> bytes` | Serialize a message instance. |
|
|
169
|
+
| `decode(cls, data) -> cls` | Parse `bytes`/`bytearray`/`memoryview` into a new `cls` instance. Absent fields get their proto3 zero value; the last occurrence of a singular field wins. |
|
|
170
|
+
| `fields(cls) -> tuple[FieldInfo, ...]` | Resolved schema of a message class, ordered by field number. |
|
|
171
|
+
| `FieldInfo` | Frozen dataclass: `name`, `number`, `kind` (`"scalar"`/`"enum"`/`"message"`), `type_name`, `repeated`, `optional`, `packed`, `scalar`, `target`, and property `wire_type`. |
|
|
172
|
+
| `is_message(obj) -> bool` | True for `@message` classes and their instances. |
|
|
173
|
+
| `to_dict(msg) -> dict` | Plain-dict view: nested messages become dicts, enums become names, unset optionals are omitted. |
|
|
174
|
+
| `from_dict(cls, data) -> cls` | Inverse of `to_dict`; enums may be given by name or number. |
|
|
175
|
+
| `to_proto(*classes, package=None) -> str` | Render the classes and every enum/message they reference as proto3 source. |
|
|
176
|
+
| `write_delimited(stream, msg) -> int` | Write a varint length prefix plus the message; returns bytes written. |
|
|
177
|
+
| `read_delimited(cls, stream) -> cls \| None` | Read one framed message; `None` at a clean end of stream. |
|
|
178
|
+
| `iter_delimited(cls, stream)` | Iterate framed messages until the stream is exhausted. |
|
|
179
|
+
| `ProtoError` | Base exception. Subclasses: `SchemaError` (also a `TypeError`), `EncodeError` and `DecodeError` (also `ValueError`). |
|
|
180
|
+
| `__version__` | `"0.1.0"` |
|
|
181
|
+
|
|
182
|
+
Low-level primitives live in `proto.wire`:
|
|
183
|
+
|
|
184
|
+
| Name | Description |
|
|
185
|
+
|------|-------------|
|
|
186
|
+
| `WireType` | `IntEnum`: `VARINT`, `I64`, `LEN`, `SGROUP`, `EGROUP`, `I32`. |
|
|
187
|
+
| `MAX_FIELD_NUMBER` | `2**29 - 1`. |
|
|
188
|
+
| `encode_varint(value) -> bytes` | Base-128 varint; negatives use 64-bit two's complement. |
|
|
189
|
+
| `decode_varint(data, pos=0) -> (value, new_pos)` | Decode one varint. |
|
|
190
|
+
| `zigzag_encode(value, bits=64) -> int` / `zigzag_decode(value) -> int` | ZigZag mapping used by `sint32`/`sint64`. |
|
|
191
|
+
| `encode_tag(number, wire_type) -> bytes` | Field key. |
|
|
192
|
+
| `decode_tag(data, pos=0) -> (number, wire_type, new_pos)` | Parse a field key. |
|
|
193
|
+
| `skip_field(data, pos, wire_type, number=None) -> int` | Skip an unknown field's payload, groups included. |
|
|
194
|
+
|
|
195
|
+
## Limitations
|
|
196
|
+
|
|
197
|
+
`proto` 0.1 covers the core of proto3. Not supported yet: `oneof`, `map<K, V>`
|
|
198
|
+
fields, well-known types (`Timestamp`, `Any`, ...), proto2 groups (they are
|
|
199
|
+
skipped when decoding), services/gRPC, and merging of repeated occurrences of
|
|
200
|
+
a singular message field (the last occurrence wins). Annotations that name
|
|
201
|
+
other classes must be resolvable from the scope where the class is defined.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v # standard library only
|
|
207
|
+
python3 -m pytest # if pytest is installed
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proto"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Schema-first, protobuf wire-compatible binary messages for pure Python, declared with dataclasses."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "nehz" }]
|
|
13
|
+
keywords = ["protobuf", "protocol-buffers", "serialization", "binary", "varint", "dataclasses", "schema"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Topic :: System :: Networking",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = ["pytest>=7"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
proto = ["py.typed"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
pythonpath = ["src"]
|
proto-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""proto: schema-first, protobuf wire-compatible messages in pure Python.
|
|
2
|
+
|
|
3
|
+
Declare messages as annotated classes, encode them to bytes that any
|
|
4
|
+
Protocol Buffers implementation can read, and decode them back::
|
|
5
|
+
|
|
6
|
+
import proto
|
|
7
|
+
|
|
8
|
+
@proto.message
|
|
9
|
+
class Point:
|
|
10
|
+
x: int = proto.field(1, "sint32")
|
|
11
|
+
y: int = proto.field(2, "sint32")
|
|
12
|
+
|
|
13
|
+
data = proto.encode(Point(3, -4))
|
|
14
|
+
assert proto.decode(Point, data) == Point(3, -4)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from . import wire
|
|
20
|
+
from .errors import DecodeError, EncodeError, ProtoError, SchemaError
|
|
21
|
+
from .message import FieldInfo, decode, encode, field, fields, from_dict, is_message, message, to_dict
|
|
22
|
+
from .schema import to_proto
|
|
23
|
+
from .stream import iter_delimited, read_delimited, write_delimited
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"message",
|
|
29
|
+
"field",
|
|
30
|
+
"encode",
|
|
31
|
+
"decode",
|
|
32
|
+
"fields",
|
|
33
|
+
"is_message",
|
|
34
|
+
"FieldInfo",
|
|
35
|
+
"to_dict",
|
|
36
|
+
"from_dict",
|
|
37
|
+
"to_proto",
|
|
38
|
+
"write_delimited",
|
|
39
|
+
"read_delimited",
|
|
40
|
+
"iter_delimited",
|
|
41
|
+
"wire",
|
|
42
|
+
"ProtoError",
|
|
43
|
+
"SchemaError",
|
|
44
|
+
"EncodeError",
|
|
45
|
+
"DecodeError",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exception hierarchy for :mod:`proto`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["ProtoError", "SchemaError", "EncodeError", "DecodeError"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProtoError(Exception):
|
|
9
|
+
"""Base class for every error raised by :mod:`proto`."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchemaError(ProtoError, TypeError):
|
|
13
|
+
"""A message class is declared incorrectly (bad field number, type, ...)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EncodeError(ProtoError, ValueError):
|
|
17
|
+
"""A value cannot be encoded (out of range, wrong Python type, ...)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DecodeError(ProtoError, ValueError):
|
|
21
|
+
"""Input bytes are not a valid encoding of the requested message."""
|