Metadata-Version: 2.4
Name: fastproto
Version: 0.4.6
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Dist: protobuf>=6,<8 ; extra == 'plugin'
Provides-Extra: plugin
License-File: LICENSE
Summary: FastProto is fast and efficient protobuf library for Python, built on the top of Rust.
Home-Page: https://github.com/pkozhem/fastproto
Author-email: pkozhem <pkozhem@gmail.com>
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# FastProto

Fast, Pythonic Protocol Buffers — messages are plain, readable `@dataclass`
types, with all encoding and decoding handled by a compiled Rust core.

Google's Python protobuf generates opaque classes full of getters/setters and a
reflection API you have to learn. FastProto instead generates clean dataclasses
you can construct, compare, and `repr()` like any other — and does the wire work
in Rust.

- **Idiomatic messages** — generated code is a `@dataclass` with plain
  annotations (`str`, `int`, `list[...]`, `dict[...]`, `| None`); autocomplete,
  type checkers, and `repr()` all just work.
- **Rust wire codec** — encode/decode run in Rust via [PyO3](https://pyo3.rs),
  not pure Python.
- **Wire-compatible** — bytes interoperate both ways with Google's reference
  `protobuf` runtime.
- **Standard toolchain** — ships a `protoc` plugin; just add `--fastproto_out`.

## Install

```bash
pip install fastproto            # runtime (Python 3.12+)
pip install "fastproto[plugin]"  # + the protoc code generator
```

Code generation also needs the `protoc` compiler itself — install it from your
package manager (`brew install protobuf`, `apt install protobuf-compiler`) or
the [official releases](https://github.com/protocolbuffers/protobuf/releases).

## Quick start

**1. Define** `user.proto`:

```protobuf
syntax = "proto3";
package example;

enum Role {
  ROLE_UNSPECIFIED = 0;
  ROLE_ADMIN = 1;
  ROLE_USER = 2;
}

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

message User {
  int64 id = 1;
  string name = 2;
  optional string email = 3;
  Role role = 4;
  repeated string tags = 5;
  Address address = 6;
  map<string, int32> counters = 7;

  oneof contact {
    string phone = 8;
    string telegram = 9;
  }
}
```

**2. Generate** with `protoc`:

```bash
protoc --proto_path=. --fastproto_out=. user.proto
```

This writes `user_pb.py` — a plain, readable dataclass module:

```python
# @generated by fastproto. DO NOT EDIT.
# source: user.proto
# pyright: reportUnknownVariableType=false
from dataclasses import dataclass, field
from enum import IntEnum

from fastproto import Message, Scalar, message


class Role(IntEnum):
    ROLE_UNSPECIFIED = 0
    ROLE_ADMIN = 1
    ROLE_USER = 2


_ADDRESS_DESCRIPTOR = bytes.fromhex("...")  # @generated (bytes elided)


@message(_ADDRESS_DESCRIPTOR)
@dataclass(slots=True)
class Address(Message):
    city: Scalar.String = ""
    street: Scalar.String = ""


_USER_DESCRIPTOR = bytes.fromhex("...")  # @generated (bytes elided)


@message(_USER_DESCRIPTOR)
@dataclass(slots=True)
class User(Message):
    id: Scalar.Int64 = 0
    name: Scalar.String = ""
    email: Scalar.String | None = None
    role: Role = Role(0)
    tags: list[Scalar.String] = field(default_factory=list)
    address: "Address | None" = None
    counters: dict[Scalar.String, Scalar.Int32] = field(default_factory=dict)
    phone: Scalar.String | None = None
    telegram: Scalar.String | None = None
```

**3. Use it** like any dataclass:

```python
from user_pb import Address, Role, User

user = User(
    id=42,
    name="Ada",
    role=Role.ROLE_ADMIN,
    tags=["vip", "beta"],
    address=Address(city="London", street="Baker St"),
    counters={"logins": 7},
)

data = user.to_bytes()               # serialize to protobuf wire bytes
assert User.from_bytes(data) == user  # and back
```

No `SerializeToString()` / `ParseFromString()` ceremony and no reflection — just
`to_bytes()` / `from_bytes()` on a dataclass you can build, compare, and print.

## Type mapping

- Each proto scalar maps to an alias under `fastproto.Scalar`. An alias is just
  the underlying Python type (`int`, `str`, ...) tagged with `Annotated[...]`, so
  it type-checks as the base type while still recording the exact wire type.

  | proto | Python | proto | Python |
  |---|---|---|---|
  | `double` | `Scalar.Double` | `fixed32` | `Scalar.Fixed32` |
  | `float` | `Scalar.Float` | `fixed64` | `Scalar.Fixed64` |
  | `int32` | `Scalar.Int32` | `sfixed32` | `Scalar.SFixed32` |
  | `int64` | `Scalar.Int64` | `sfixed64` | `Scalar.SFixed64` |
  | `uint32` | `Scalar.UInt32` | `bool` | `Scalar.Bool` |
  | `uint64` | `Scalar.UInt64` | `string` | `Scalar.String` |
  | `sint32` | `Scalar.SInt32` | `bytes` | `Scalar.Bytes` |
  | `sint64` | `Scalar.SInt64` | | |

- Composite fields: `repeated T` → `list[T]`, `map<K, V>` → `dict[K, V]`,
  `enum` → `IntEnum`, and `optional` / message / `oneof` fields → `T | None`.

- Well-known types map to native objects: `google.protobuf.Timestamp` → `datetime`,
  `Duration` → `timedelta`; the rest (`Any`, `Struct`, wrappers, ...) are plain
  dataclasses in `fastproto.wellknown`.

- Nested `message` / `enum` definitions generate as nested classes at any depth
  (`Outer.Inner`, `Outer.Color`), so the Python structure mirrors the `.proto`.

- Multi-file schemas work — `import` in a `.proto` becomes an import between the
  generated modules.

## Semantics

- **Presence (proto3):** plain scalars use their zero value and are not nullable;
  `optional` scalars, message fields, and `oneof` members are `T | None` and
  track explicit presence (a set empty string is distinct from unset). An
  all-default message encodes to `b""`.
- **`oneof`:** members are plain optional fields; setting more than one raises
  `ValueError` at encode time. `msg.which_oneof("group")` returns the name of the
  set member (or `None`), like google's `WhichOneof`.
- **Open enums (proto3):** an enum value not defined in your schema decodes to a
  plain `int` (like google's runtime) and survives re-encoding.
- **Repeated message merge:** not performed. If a singular message field appears
  more than once on the wire, the last occurrence wins (google merges the
  submessages field-by-field). A wire-compat caveat for concatenated streams.
- **Unknown fields:** fields your schema doesn't know are preserved verbatim
  across decode → encode (forward compatibility). They are invisible to
  `__init__` / `repr` / `==`. Exception: proto2 `group` wire types can't be
  skipped, so a message containing one raises `ValueError` on decode.
- **Strict types at the boundary:** field values are taken as-is — `bool` does not
  accept `int`, `str` and `bytes` don't interconvert, and a `repeated` field must
  be a real list (a bare `str`/`bytes` is rejected, not iterated char-by-char). An
  out-of-range integer raises `OverflowError` on encode. `from_bytes` takes
  `bytes` (not yet `bytearray` / `memoryview`).
- **Nesting limit:** messages deeper than 100 levels — or a cyclic object graph
  on encode — raise `ValueError` instead of exhausting the stack.
- **Timestamps:** decoded `datetime`s are aware UTC; naive ones encode as UTC.
  protobuf's sub-microsecond precision (nanos) is truncated to microseconds.
- **References:** sibling, self, nested, and enum references resolve lazily on the
  first `to_bytes()` / `from_bytes()` — nothing for you to wire up.

```python
empty = User()
assert empty.to_bytes() == b"" and empty.email is None
assert User(phone="1").which_oneof("contact") == "phone"
User(phone="1", telegram="a").to_bytes()  # ValueError: ... oneof ...
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, project layout, and the
release flow.

## License

[MIT](LICENSE)

