Metadata-Version: 2.4
Name: proto
Version: 0.1.0
Summary: Schema-first, protobuf wire-compatible binary messages for pure Python, declared with dataclasses.
Author: nehz
License-Expression: MIT
Keywords: protobuf,protocol-buffers,serialization,binary,varint,dataclasses,schema
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# proto

**Protocol Buffers without the toolchain.** `proto` lets you declare binary
messages as ordinary Python dataclasses and serialize them to bytes that are
wire-compatible with Google's Protocol Buffers (proto3). No `protoc`, no
generated code, no C extension, no dependencies: just the standard library.

Use it when you need to talk to a protobuf service from a script, persist
compact records, or prototype a schema in Python first, and still have every
other protobuf implementation read your bytes.

```python
import proto

@proto.message
class Point:
    x: int = proto.field(1, "sint32")
    y: int = proto.field(2, "sint32")

data = proto.encode(Point(3, -4))     # b'\x08\x06\x10\x07'
assert proto.decode(Point, data) == Point(3, -4)
```

## Features

- **Wire-compatible**: varints, zigzag, fixed-width, length-delimited and packed
  encodings match the official protobuf encoding byte for byte. The tests check
  the exact byte strings from the protobuf encoding guide.
- **Schema-first dataclasses**: field numbers live next to the type annotations.
  Messages are real `dataclasses`, so you keep `==`, `repr`, `replace()` and
  your type checker.
- **All 15 scalar types**: `int32 int64 uint32 uint64 sint32 sint64 bool
  fixed32 fixed64 sfixed32 sfixed64 float double string bytes`, plus `IntEnum`
  enums, nested and recursive messages, `repeated` fields, and proto3 `optional`.
- **proto3 semantics**: default values are omitted on the wire, unknown fields are
  skipped, packed and unpacked repeated fields are both accepted, unknown enum
  values are kept as `int` (open enums).
- **Strict validation**: out-of-range integers, wrong Python types, malformed or
  truncated input raise clear `EncodeError` / `DecodeError` / `SchemaError`.
- **`.proto` export**: `to_proto()` renders your classes as a `.proto` file, so
  other languages can generate code from the same schema.
- **Streaming**: length-delimited framing (`writeDelimitedTo` format) for many
  messages in one file or socket.
- Pure Python 3.10+, fully type-hinted (`py.typed`), zero dependencies.

## Install

```bash
pip install proto            # once published
pip install -e .             # from a checkout
```

> Note: Google's `proto-plus` package also installs a top-level `proto` module.
> Don't install both in the same environment.

## Quickstart

```python
from __future__ import annotations

import enum
import io

import proto


class Role(enum.IntEnum):
    ROLE_UNSPECIFIED = 0     # proto3 enums need a zero value
    ADMIN = 1
    MEMBER = 2


@proto.message
class Address:
    city: str = proto.field(1)
    zip_code: str = proto.field(2)


@proto.message
class User:
    name: str = proto.field(1)                      # inferred: string
    id: int = proto.field(2, "uint32")              # explicit scalar type
    age: int | None = proto.field(3, "int32")       # proto3 `optional`: None = unset
    role: Role = proto.field(4)                     # enum
    emails: list[str] = proto.field(5)              # repeated
    scores: list[int] = proto.field(6, "sint32")    # repeated, packed by default
    address: Address | None = proto.field(7)        # nested message
    friends: list[User] = proto.field(8)            # recursive


ada = User("ada", id=1, role=Role.ADMIN, emails=["ada@example.com"],
           address=Address("London", "N1"))

data = ada.to_bytes()                 # same as proto.encode(ada)
again = User.from_bytes(data)         # same as proto.decode(User, data)
assert again == ada

proto.to_dict(ada)
# {'name': 'ada', 'id': 1, 'role': 'ADMIN', 'emails': ['ada@example.com'],
#  'scores': [], 'address': {'city': 'London', 'zip_code': 'N1'}, 'friends': []}

# Many messages in one stream
buf = io.BytesIO()
for user in (ada, User("bob", id=2)):
    proto.write_delimited(buf, user)
buf.seek(0)
names = [u.name for u in proto.iter_delimited(User, buf)]   # ['ada', 'bob']

print(proto.to_proto(User, package="example.v1"))
```

The last line prints:

```proto
syntax = "proto3";

package example.v1;

enum Role {
  ROLE_UNSPECIFIED = 0;
  ADMIN = 1;
  MEMBER = 2;
}

message Address {
  string city = 1;
  string zip_code = 2;
}

message User {
  string name = 1;
  uint32 id = 2;
  optional int32 age = 3;
  Role role = 4;
  repeated string emails = 5;
  repeated sint32 scores = 6;
  Address address = 7;
  repeated User friends = 8;
}
```

## Declaring fields

| Annotation              | Inferred `.proto` type | Default when omitted |
|-------------------------|------------------------|----------------------|
| `int`                   | `int64`                | `0`                  |
| `float`                 | `double`               | `0.0`                |
| `bool`                  | `bool`                 | `False`              |
| `str`                   | `string`               | `""`                 |
| `bytes`                 | `bytes`                | `b""`                |
| `SomeIntEnum`           | `SomeIntEnum`          | the member with value `0` |
| `SomeMessage \| None`   | `SomeMessage`          | `None` (unset)       |
| `T \| None` (scalar)    | `optional T`           | `None` (unset)       |
| `list[T]`               | `repeated T`           | `[]`                 |

Pass `type=` (the second argument of `field`) to choose another scalar
encoding such as `"sint32"` or `"fixed64"`, or to name an enum/message class
explicitly.

## API overview

Everything is importable from the top-level `proto` package.

| Name | Description |
|------|-------------|
| `@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). |
| `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()`. |
| `encode(msg) -> bytes` | Serialize a message instance. |
| `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. |
| `fields(cls) -> tuple[FieldInfo, ...]` | Resolved schema of a message class, ordered by field number. |
| `FieldInfo` | Frozen dataclass: `name`, `number`, `kind` (`"scalar"`/`"enum"`/`"message"`), `type_name`, `repeated`, `optional`, `packed`, `scalar`, `target`, and property `wire_type`. |
| `is_message(obj) -> bool` | True for `@message` classes and their instances. |
| `to_dict(msg) -> dict` | Plain-dict view: nested messages become dicts, enums become names, unset optionals are omitted. |
| `from_dict(cls, data) -> cls` | Inverse of `to_dict`; enums may be given by name or number. |
| `to_proto(*classes, package=None) -> str` | Render the classes and every enum/message they reference as proto3 source. |
| `write_delimited(stream, msg) -> int` | Write a varint length prefix plus the message; returns bytes written. |
| `read_delimited(cls, stream) -> cls \| None` | Read one framed message; `None` at a clean end of stream. |
| `iter_delimited(cls, stream)` | Iterate framed messages until the stream is exhausted. |
| `ProtoError` | Base exception. Subclasses: `SchemaError` (also a `TypeError`), `EncodeError` and `DecodeError` (also `ValueError`). |
| `__version__` | `"0.1.0"` |

Low-level primitives live in `proto.wire`:

| Name | Description |
|------|-------------|
| `WireType` | `IntEnum`: `VARINT`, `I64`, `LEN`, `SGROUP`, `EGROUP`, `I32`. |
| `MAX_FIELD_NUMBER` | `2**29 - 1`. |
| `encode_varint(value) -> bytes` | Base-128 varint; negatives use 64-bit two's complement. |
| `decode_varint(data, pos=0) -> (value, new_pos)` | Decode one varint. |
| `zigzag_encode(value, bits=64) -> int` / `zigzag_decode(value) -> int` | ZigZag mapping used by `sint32`/`sint64`. |
| `encode_tag(number, wire_type) -> bytes` | Field key. |
| `decode_tag(data, pos=0) -> (number, wire_type, new_pos)` | Parse a field key. |
| `skip_field(data, pos, wire_type, number=None) -> int` | Skip an unknown field's payload, groups included. |

## Limitations

`proto` 0.1 covers the core of proto3. Not supported yet: `oneof`, `map<K, V>`
fields, well-known types (`Timestamp`, `Any`, ...), proto2 groups (they are
skipped when decoding), services/gRPC, and merging of repeated occurrences of
a singular message field (the last occurrence wins). Annotations that name
other classes must be resolvable from the scope where the class is defined.

## Development

```bash
PYTHONPATH=src python3 -m unittest discover -s tests -v   # standard library only
python3 -m pytest                                        # if pytest is installed
```

## License

MIT
