# Better Protobuf / gRPC Support for Python
![](https://github.com/betterproto/python-betterproto2/actions/workflows/ci.yml/badge.svg)
> :octocat: If you're reading this on github, please be aware that it might mention unreleased features! See the latest released README on [pypi](https://pypi.org/project/betterproto/).
This project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments (e.g. Protobuf 2). The following are supported:
- Protobuf 3 & gRPC code generation
- Both binary & JSON serialization is built-in
- Python 3.7+ making use of:
- Enums
- Dataclasses
- `async`/`await`
- Timezone-aware `datetime` and `timedelta` objects
- Relative imports
- Mypy type checking
- [Pydantic Models](https://docs.pydantic.dev/) generation (see #generating-pydantic-models)
This project is heavily inspired by, and borrows functionality from:
- https://github.com/protocolbuffers/protobuf/tree/master/python
- https://github.com/eigenein/protobuf/
- https://github.com/vmagamedov/grpclib
## Motivation
This project exists because I am unhappy with the state of the official Google protoc plugin for Python.
- No `async` support (requires additional `grpclib` plugin)
- No typing support or code completion/intelligence (requires additional `mypy` plugin)
- No `__init__.py` module files get generated
- Output is not importable
- Import paths break in Python 3 unless you mess with `sys.path`
- Bugs when names clash (e.g. `codecs` package)
- Generated code is not idiomatic
- Completely unreadable runtime code-generation
- Much code looks like C++ or Java ported 1:1 to Python
- Capitalized function names like `HasField()` and `SerializeToString()`
- Uses `SerializeToString()` rather than the built-in `__bytes__()`
- Special wrapped types don't use Python's `None`
- Timestamp/duration types don't use Python's built-in `datetime` module
This project is a reimplementation from the ground up focused on idiomatic modern Python to help fix some of the above. While it may not be a 1:1 drop-in replacement due to changed method names and call patterns, the wire format is identical.
## Installation
First, install the package. Note that the `[compiler]` feature flag tells it to install extra dependencies only needed by the `protoc` plugin:
```sh
# Install both the library and compiler
pip install "betterproto[compiler]"
# Install just the library (to use the generated code output)
pip install betterproto
```
*Betterproto* is under active development. To install the latest beta version, use `pip install --pre betterproto`.
## Getting Started
### Compiling proto files
Given you installed the compiler and have a proto file, e.g `example.proto`:
```protobuf
syntax = "proto3";
package hello;
// Greeting represents a message you can tell a user.
message Greeting {
string message = 1;
}
```
You can run the following to invoke protoc directly:
```sh
mkdir lib
protoc -I . --python_betterproto_out=lib example.proto
```
or run the following to invoke protoc via grpcio-tools:
```sh
pip install grpcio-tools
python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto
```
This will generate `lib/hello/__init__.py` which looks like:
```python
# Generated by the protocol buffer compiler. DO NOT EDIT!
# sources: example.proto
# plugin: python-betterproto
from dataclasses import dataclass
import betterproto
@dataclass
class Greeting(betterproto.Message):
"""Greeting represents a message you can tell a user."""
message: str = betterproto.string_field(1)
```
Now you can use it!
```python
>>> from lib.hello import Greeting
>>> test = Greeting()
>>> test
Greeting(message='')
>>> test.message = "Hey!"
>>> test
Greeting(message="Hey!")
>>> serialized = bytes(test)
>>> serialized
b'\n\x04Hey!'
>>> another = Greeting().parse(serialized)
>>> another
Greeting(message="Hey!")
>>> another.to_dict()
{"message": "Hey!"}
>>> another.to_json(indent=2)
'{\n "message": "Hey!"\n}'
```
### Async gRPC Support
The generated Protobuf `Message` classes are compatible with [grpclib](https://github.com/vmagamedov/grpclib) so you are free to use it if you like. That said, this project also includes support for async gRPC stub generation with better static type checking and code completion support. It is enabled by default.
Given an example service definition:
```protobuf
syntax = "proto3";
package echo;
message EchoRequest {
string value = 1;
// Number of extra times to echo
uint32 extra_times = 2;
}
message EchoResponse {
repeated string values = 1;
}
message EchoStreamResponse {
string value = 1;
}
service Echo {
rpc Echo(EchoRequest) returns (EchoResponse);
rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse);
}
```
Generate echo proto file:
```
python -m grpc_tools.protoc -I . --python_betterproto_out=. echo.proto
```
A client can be implemented as follows:
```python
import asyncio
import echo
from grpclib.client import Channel
async def main():
channel = Channel(host="127.0.0.1", port=50051)
service = echo.EchoStub(channel)
response = await service.echo(echo.EchoRequest(value="hello", extra_times=1))
print(response)
async for response in service.echo_stream(echo.EchoRequest(value="hello", extra_times=1)):
print(response)
# don't forget to close the channel when done!
channel.close()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
which would output
```python
EchoResponse(values=['hello', 'hello'])
EchoStreamResponse(value='hello')
EchoStreamResponse(value='hello')
```
This project also produces server-facing stubs that can be used to implement a Python
gRPC server.
To use them, simply subclass the base class in the generated files and override the
service methods:
```python
import asyncio
from echo import EchoBase, EchoRequest, EchoResponse, EchoStreamResponse
from grpclib.server import Server
from typing import AsyncIterator
class EchoService(EchoBase):
async def echo(self, echo_request: "EchoRequest") -> "EchoResponse":
return EchoResponse([echo_request.value for _ in range(echo_request.extra_times)])
async def echo_stream(self, echo_request: "EchoRequest") -> AsyncIterator["EchoStreamResponse"]:
for _ in range(echo_request.extra_times):
yield EchoStreamResponse(echo_request.value)
async def main():
server = Server([EchoService()])
await server.start("127.0.0.1", 50051)
await server.wait_closed()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
### JSON
Both serializing and parsing are supported to/from JSON and Python dictionaries using the following methods:
- Dicts: `Message().to_dict()`, `Message().from_dict(...)`
- JSON: `Message().to_json()`, `Message().from_json(...)`
For compatibility the default is to convert field names to `camelCase`. You can control this behavior by passing a casing value, e.g:
```python
MyMessage().to_dict(casing=betterproto.Casing.SNAKE)
```
### One-of Support
Protobuf supports grouping fields in a `oneof` clause. Only one of the fields in the group may be set at a given time. For example, given the proto:
```protobuf
syntax = "proto3";
message Test {
oneof foo {
bool on = 1;
int32 count = 2;
string name = 3;
}
}
```
On Python 3.10 and later, you can use a `match` statement to access the provided one-of field, which supports type-checking:
```py
test = Test()
match test:
case Test(on=bool(value)):
print(value) # value: bool
case Test(count=int(value)):
print(value) # value: int
case Test(name=str(value)):
print(value) # value: str
case _:
print("No value provided")
```
You can also use `betterproto.which_one_of(message, group_name)` to determine which of the fields was set. It returns a tuple of the field name and value, or a blank string and `None` if unset.
```py
>>> test = Test()
>>> betterproto.which_one_of(test, "foo")
["", None]
>>> test.on = True
>>> betterproto.which_one_of(test, "foo")
["on", True]
# Setting one member of the group resets the others.
>>> test.count = 57
>>> betterproto.which_one_of(test, "foo")
["count", 57]
# Default (zero) values also work.
>>> test.name = ""
>>> betterproto.which_one_of(test, "foo")
["name", ""]
```
Again this is a little different than the official Google code generator:
```py
# Old way (official Google protobuf package)
>>> message.WhichOneof("group")
"foo"
# New way (this project)
>>> betterproto.which_one_of(message, "group")
["foo", "foo's value"]
```
### Well-Known Google Types
Google provides several well-known message types like a timestamp, duration, and several wrappers used to provide optional zero value support. Each of these has a special JSON representation and is handled a little differently from normal messages. The Python mapping for these is as follows:
| Google Message | Python Type | Default |
| --------------------------- | ---------------------------------------- | ---------------------- |
| `google.protobuf.duration` | [`datetime.timedelta`][td] | `0` |
| `google.protobuf.timestamp` | Timezone-aware [`datetime.datetime`][dt] | `1970-01-01T00:00:00Z` |
| `google.protobuf.*Value` | `Optional[...]` | `None` |
| `google.protobuf.*` | `betterproto.lib.google.protobuf.*` | `None` |
[td]: https://docs.python.org/3/library/datetime.html#timedelta-objects
[dt]: https://docs.python.org/3/library/datetime.html#datetime.datetime
For the wrapper types, the Python type corresponds to the wrapped type, e.g. `google.protobuf.BoolValue` becomes `Optional[bool]` while `google.protobuf.Int32Value` becomes `Optional[int]`. All of the optional values default to `None`, so don't forget to check for that possible state. Given:
```protobuf
syntax = "proto3";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
message Test {
google.protobuf.BoolValue maybe = 1;
google.protobuf.Timestamp ts = 2;
google.protobuf.Duration duration = 3;
}
```
You can do stuff like:
```py
>>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"})
>>> t
Test(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000))
>>> t.ts - t.duration
datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc)
>>> t.ts.isoformat()
'2019-01-01T12:00:00+00:00'
>>> t.maybe = None
>>> t.to_dict()
{'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'}
```
## Generating Pydantic Models
You can use python-betterproto to generate pydantic based models, using
pydantic dataclasses. This means the results of the protobuf unmarshalling will
be typed checked. The usage is the same, but you need to add a custom option
when calling the protobuf compiler:
```
protoc -I . --python_betterproto_opt=pydantic_dataclasses --python_betterproto_out=lib example.proto
```
With the important change being `--python_betterproto_opt=pydantic_dataclasses`. This will
swap the dataclass implementation from the builtin python dataclass to the
pydantic dataclass. You must have pydantic as a dependency in your project for
this to work.
## Configuration typing imports
By default typing types will be imported directly from typing. This sometimes can lead to issues in generation if types that are being generated conflict with the name. In this case you can configure the way types are imported from 3 different options:
### Direct
```
protoc -I . --python_betterproto_opt=typing.direct --python_betterproto_out=lib example.proto
```
this configuration is the default, and will import types as follows:
```
from typing import (
List,
Optional,
Union
)
...
value: List[str] = []
value2: Optional[str] = None
value3: Union[str, int] = 1
```
### Root
```
protoc -I . --python_betterproto_opt=typing.root --python_betterproto_out=lib example.proto
```
this configuration loads the root typing module, and then access the types off of it directly:
```
import typing
...
value: typing.List[str] = []
value2: typing.Optional[str] = None
value3: typing.Union[str, int] = 1
```
### 310
```
protoc -I . --python_betterproto_opt=typing.310 --python_betterproto_out=lib example.proto
```
this configuration avoid loading typing all together if possible and uses the python 3.10 pattern:
```
...
value: list[str] = []
value2: str | None = None
value3: str | int = 1
```
## Development
- _Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!_
- _See how you can help → [Contributing](.github/CONTRIBUTING.md)_
### Requirements
- Python (3.8 or higher)
- [poetry](https://python-poetry.org/docs/#installation)
*Needed to install dependencies in a virtual environment*
- [poethepoet](https://github.com/nat-n/poethepoet) for running development tasks as defined in pyproject.toml
- Can be installed to your host environment via `pip install poethepoet` then executed as simple `poe`
- or run from the poetry venv as `poetry run poe`
### Setup
```sh
# Get set up with the virtual env & dependencies
poetry install -E compiler
# Activate the poetry environment
poetry shell
```
### Code style
This project enforces [black](https://github.com/psf/black) python code formatting.
Before committing changes run:
```sh
poe format
```
To avoid merge conflicts later, non-black formatted python code will fail in CI.
### Tests
There are two types of tests:
1. Standard tests
2. Custom tests
#### Standard tests
Adding a standard test case is easy.
- Create a new directory `betterproto/tests/inputs/<name>`
- add `<name>.proto` with a message called `Test`
- add `<name>.json` with some test data (optional)
It will be picked up automatically when you run the tests.
- See also: [Standard Tests Development Guide](tests/README.md)
#### Custom tests
Custom tests are found in `tests/test_*.py` and are run with pytest.
#### Running
Here's how to run the tests.
```sh
# Generate assets from sample .proto files required by the tests
poe generate
# Run the tests
poe test
```
To run tests as they are run in CI (with tox) run:
```sh
poe full-test
```
### (Re)compiling Google Well-known Types
Betterproto includes compiled versions for Google's well-known types at [src/betterproto/lib/google](src/betterproto/lib/google).
Be sure to regenerate these files when modifying the plugin output format, and validate by running the tests.
Normally, the plugin does not compile any references to `google.protobuf`, since they are pre-compiled. To force compilation of `google.protobuf`, use the option `--custom_opt=INCLUDE_GOOGLE`.
Assuming your `google.protobuf` source files (included with all releases of `protoc`) are located in `/usr/local/include`, you can regenerate them as follows:
```sh
protoc \
--plugin=protoc-gen-custom=src/betterproto/plugin/main.py \
--custom_opt=INCLUDE_GOOGLE \
--custom_out=src/betterproto/lib \
-I /usr/local/include/ \
/usr/local/include/google/protobuf/*.proto
```
### TODO
- [x] Fixed length fields
- [x] Packed fixed-length
- [x] Zig-zag signed fields (sint32, sint64)
- [x] Don't encode zero values for nested types
- [x] Enums
- [x] Repeated message fields
- [x] Maps
- [x] Maps of message fields
- [x] Support passthrough of unknown fields
- [x] Refs to nested types
- [x] Imports in proto files
- [x] Well-known Google types
- [ ] Support as request input
- [ ] Support as response output
- [ ] Automatically wrap/unwrap responses
- [x] OneOf support
- [x] Basic support on the wire
- [x] Check which was set from the group
- [x] Setting one unsets the others
- [ ] JSON that isn't completely naive.
- [x] 64-bit ints as strings
- [x] Maps
- [x] Lists
- [x] Bytes as base64
- [ ] Any support
- [x] Enum strings
- [x] Well known types support (timestamp, duration, wrappers)
- [x] Support different casing (orig vs. camel vs. others?)
- [x] Async service stubs
- [x] Unary-unary
- [x] Server streaming response
- [x] Client streaming request
- [x] Renaming messages and fields to conform to Python name standards
- [x] Renaming clashes with language keywords
- [x] Python package
- [x] Automate running tests
- [ ] Cleanup!
## License
Copyright © 2019 Daniel G. Taylor
Copyright © 2024 The betterproto contributors
Raw data
{
"_id": null,
"home_page": "https://github.com/betterproto/python-betterproto2",
"name": "betterproto2",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "protobuf, gRPC",
"author": "Adrien Vannson",
"author_email": "adrien.vannson@protonmail.com",
"download_url": "https://files.pythonhosted.org/packages/b6/a5/608e7d8cb00c99f23fa9a43199c5f2ffef1504ed95569495a8a6594a34e3/betterproto2-0.0.2.tar.gz",
"platform": null,
"description": "# Better Protobuf / gRPC Support for Python\n\n![](https://github.com/betterproto/python-betterproto2/actions/workflows/ci.yml/badge.svg)\n\n> :octocat: If you're reading this on github, please be aware that it might mention unreleased features! See the latest released README on [pypi](https://pypi.org/project/betterproto/).\n\nThis project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments (e.g. Protobuf 2). The following are supported:\n\n- Protobuf 3 & gRPC code generation\n - Both binary & JSON serialization is built-in\n- Python 3.7+ making use of:\n - Enums\n - Dataclasses\n - `async`/`await`\n - Timezone-aware `datetime` and `timedelta` objects\n - Relative imports\n - Mypy type checking\n- [Pydantic Models](https://docs.pydantic.dev/) generation (see #generating-pydantic-models)\n\nThis project is heavily inspired by, and borrows functionality from:\n\n- https://github.com/protocolbuffers/protobuf/tree/master/python\n- https://github.com/eigenein/protobuf/\n- https://github.com/vmagamedov/grpclib\n\n## Motivation\n\nThis project exists because I am unhappy with the state of the official Google protoc plugin for Python.\n\n- No `async` support (requires additional `grpclib` plugin)\n- No typing support or code completion/intelligence (requires additional `mypy` plugin)\n- No `__init__.py` module files get generated\n- Output is not importable\n - Import paths break in Python 3 unless you mess with `sys.path`\n- Bugs when names clash (e.g. `codecs` package)\n- Generated code is not idiomatic\n - Completely unreadable runtime code-generation\n - Much code looks like C++ or Java ported 1:1 to Python\n - Capitalized function names like `HasField()` and `SerializeToString()`\n - Uses `SerializeToString()` rather than the built-in `__bytes__()`\n - Special wrapped types don't use Python's `None`\n - Timestamp/duration types don't use Python's built-in `datetime` module\n\n\nThis project is a reimplementation from the ground up focused on idiomatic modern Python to help fix some of the above. While it may not be a 1:1 drop-in replacement due to changed method names and call patterns, the wire format is identical.\n\n## Installation\n\nFirst, install the package. Note that the `[compiler]` feature flag tells it to install extra dependencies only needed by the `protoc` plugin:\n\n```sh\n# Install both the library and compiler\npip install \"betterproto[compiler]\"\n\n# Install just the library (to use the generated code output)\npip install betterproto\n```\n\n*Betterproto* is under active development. To install the latest beta version, use `pip install --pre betterproto`.\n\n## Getting Started\n\n### Compiling proto files\n\nGiven you installed the compiler and have a proto file, e.g `example.proto`:\n\n```protobuf\nsyntax = \"proto3\";\n\npackage hello;\n\n// Greeting represents a message you can tell a user.\nmessage Greeting {\n string message = 1;\n}\n```\n\nYou can run the following to invoke protoc directly:\n\n```sh\nmkdir lib\nprotoc -I . --python_betterproto_out=lib example.proto\n```\n\nor run the following to invoke protoc via grpcio-tools:\n\n```sh\npip install grpcio-tools\npython -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto\n```\n\nThis will generate `lib/hello/__init__.py` which looks like:\n\n```python\n# Generated by the protocol buffer compiler. DO NOT EDIT!\n# sources: example.proto\n# plugin: python-betterproto\nfrom dataclasses import dataclass\n\nimport betterproto\n\n\n@dataclass\nclass Greeting(betterproto.Message):\n \"\"\"Greeting represents a message you can tell a user.\"\"\"\n\n message: str = betterproto.string_field(1)\n```\n\nNow you can use it!\n\n```python\n>>> from lib.hello import Greeting\n>>> test = Greeting()\n>>> test\nGreeting(message='')\n\n>>> test.message = \"Hey!\"\n>>> test\nGreeting(message=\"Hey!\")\n\n>>> serialized = bytes(test)\n>>> serialized\nb'\\n\\x04Hey!'\n\n>>> another = Greeting().parse(serialized)\n>>> another\nGreeting(message=\"Hey!\")\n\n>>> another.to_dict()\n{\"message\": \"Hey!\"}\n>>> another.to_json(indent=2)\n'{\\n \"message\": \"Hey!\"\\n}'\n```\n\n### Async gRPC Support\n\nThe generated Protobuf `Message` classes are compatible with [grpclib](https://github.com/vmagamedov/grpclib) so you are free to use it if you like. That said, this project also includes support for async gRPC stub generation with better static type checking and code completion support. It is enabled by default.\n\nGiven an example service definition:\n\n```protobuf\nsyntax = \"proto3\";\n\npackage echo;\n\nmessage EchoRequest {\n string value = 1;\n // Number of extra times to echo\n uint32 extra_times = 2;\n}\n\nmessage EchoResponse {\n repeated string values = 1;\n}\n\nmessage EchoStreamResponse {\n string value = 1;\n}\n\nservice Echo {\n rpc Echo(EchoRequest) returns (EchoResponse);\n rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse);\n}\n```\n\nGenerate echo proto file:\n\n```\npython -m grpc_tools.protoc -I . --python_betterproto_out=. echo.proto\n```\n\nA client can be implemented as follows:\n```python\nimport asyncio\nimport echo\n\nfrom grpclib.client import Channel\n\n\nasync def main():\n channel = Channel(host=\"127.0.0.1\", port=50051)\n service = echo.EchoStub(channel)\n response = await service.echo(echo.EchoRequest(value=\"hello\", extra_times=1))\n print(response)\n\n async for response in service.echo_stream(echo.EchoRequest(value=\"hello\", extra_times=1)):\n print(response)\n\n # don't forget to close the channel when done!\n channel.close()\n\n\nif __name__ == \"__main__\":\n loop = asyncio.get_event_loop()\n loop.run_until_complete(main())\n\n```\n\nwhich would output\n```python\nEchoResponse(values=['hello', 'hello'])\nEchoStreamResponse(value='hello')\nEchoStreamResponse(value='hello')\n```\n\nThis project also produces server-facing stubs that can be used to implement a Python\ngRPC server.\nTo use them, simply subclass the base class in the generated files and override the\nservice methods:\n\n```python\nimport asyncio\nfrom echo import EchoBase, EchoRequest, EchoResponse, EchoStreamResponse\nfrom grpclib.server import Server\nfrom typing import AsyncIterator\n\n\nclass EchoService(EchoBase):\n async def echo(self, echo_request: \"EchoRequest\") -> \"EchoResponse\":\n return EchoResponse([echo_request.value for _ in range(echo_request.extra_times)])\n\n async def echo_stream(self, echo_request: \"EchoRequest\") -> AsyncIterator[\"EchoStreamResponse\"]:\n for _ in range(echo_request.extra_times):\n yield EchoStreamResponse(echo_request.value)\n\n\nasync def main():\n server = Server([EchoService()])\n await server.start(\"127.0.0.1\", 50051)\n await server.wait_closed()\n\nif __name__ == '__main__':\n loop = asyncio.get_event_loop()\n loop.run_until_complete(main())\n```\n\n### JSON\n\nBoth serializing and parsing are supported to/from JSON and Python dictionaries using the following methods:\n\n- Dicts: `Message().to_dict()`, `Message().from_dict(...)`\n- JSON: `Message().to_json()`, `Message().from_json(...)`\n\nFor compatibility the default is to convert field names to `camelCase`. You can control this behavior by passing a casing value, e.g:\n\n```python\nMyMessage().to_dict(casing=betterproto.Casing.SNAKE)\n```\n\n### One-of Support\n\nProtobuf supports grouping fields in a `oneof` clause. Only one of the fields in the group may be set at a given time. For example, given the proto:\n\n```protobuf\nsyntax = \"proto3\";\n\nmessage Test {\n oneof foo {\n bool on = 1;\n int32 count = 2;\n string name = 3;\n }\n}\n```\n\nOn Python 3.10 and later, you can use a `match` statement to access the provided one-of field, which supports type-checking:\n\n```py\ntest = Test()\nmatch test:\n case Test(on=bool(value)):\n print(value) # value: bool\n case Test(count=int(value)):\n print(value) # value: int\n case Test(name=str(value)):\n print(value) # value: str\n case _:\n print(\"No value provided\")\n```\n\nYou can also use `betterproto.which_one_of(message, group_name)` to determine which of the fields was set. It returns a tuple of the field name and value, or a blank string and `None` if unset.\n\n```py\n>>> test = Test()\n>>> betterproto.which_one_of(test, \"foo\")\n[\"\", None]\n\n>>> test.on = True\n>>> betterproto.which_one_of(test, \"foo\")\n[\"on\", True]\n\n# Setting one member of the group resets the others.\n>>> test.count = 57\n>>> betterproto.which_one_of(test, \"foo\")\n[\"count\", 57]\n\n# Default (zero) values also work.\n>>> test.name = \"\"\n>>> betterproto.which_one_of(test, \"foo\")\n[\"name\", \"\"]\n```\n\nAgain this is a little different than the official Google code generator:\n\n```py\n# Old way (official Google protobuf package)\n>>> message.WhichOneof(\"group\")\n\"foo\"\n\n# New way (this project)\n>>> betterproto.which_one_of(message, \"group\")\n[\"foo\", \"foo's value\"]\n```\n\n### Well-Known Google Types\n\nGoogle provides several well-known message types like a timestamp, duration, and several wrappers used to provide optional zero value support. Each of these has a special JSON representation and is handled a little differently from normal messages. The Python mapping for these is as follows:\n\n| Google Message | Python Type | Default |\n| --------------------------- | ---------------------------------------- | ---------------------- |\n| `google.protobuf.duration` | [`datetime.timedelta`][td] | `0` |\n| `google.protobuf.timestamp` | Timezone-aware [`datetime.datetime`][dt] | `1970-01-01T00:00:00Z` |\n| `google.protobuf.*Value` | `Optional[...]` | `None` |\n| `google.protobuf.*` | `betterproto.lib.google.protobuf.*` | `None` |\n\n[td]: https://docs.python.org/3/library/datetime.html#timedelta-objects\n[dt]: https://docs.python.org/3/library/datetime.html#datetime.datetime\n\nFor the wrapper types, the Python type corresponds to the wrapped type, e.g. `google.protobuf.BoolValue` becomes `Optional[bool]` while `google.protobuf.Int32Value` becomes `Optional[int]`. All of the optional values default to `None`, so don't forget to check for that possible state. Given:\n\n```protobuf\nsyntax = \"proto3\";\n\nimport \"google/protobuf/duration.proto\";\nimport \"google/protobuf/timestamp.proto\";\nimport \"google/protobuf/wrappers.proto\";\n\nmessage Test {\n google.protobuf.BoolValue maybe = 1;\n google.protobuf.Timestamp ts = 2;\n google.protobuf.Duration duration = 3;\n}\n```\n\nYou can do stuff like:\n\n```py\n>>> t = Test().from_dict({\"maybe\": True, \"ts\": \"2019-01-01T12:00:00Z\", \"duration\": \"1.200s\"})\n>>> t\nTest(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000))\n\n>>> t.ts - t.duration\ndatetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc)\n\n>>> t.ts.isoformat()\n'2019-01-01T12:00:00+00:00'\n\n>>> t.maybe = None\n>>> t.to_dict()\n{'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'}\n```\n\n## Generating Pydantic Models\n\nYou can use python-betterproto to generate pydantic based models, using\npydantic dataclasses. This means the results of the protobuf unmarshalling will\nbe typed checked. The usage is the same, but you need to add a custom option\nwhen calling the protobuf compiler:\n\n\n```\nprotoc -I . --python_betterproto_opt=pydantic_dataclasses --python_betterproto_out=lib example.proto\n```\n\nWith the important change being `--python_betterproto_opt=pydantic_dataclasses`. This will\nswap the dataclass implementation from the builtin python dataclass to the\npydantic dataclass. You must have pydantic as a dependency in your project for\nthis to work.\n\n## Configuration typing imports\n\nBy default typing types will be imported directly from typing. This sometimes can lead to issues in generation if types that are being generated conflict with the name. In this case you can configure the way types are imported from 3 different options:\n\n### Direct\n```\nprotoc -I . --python_betterproto_opt=typing.direct --python_betterproto_out=lib example.proto\n```\nthis configuration is the default, and will import types as follows:\n```\nfrom typing import (\n List,\n Optional,\n Union\n)\n...\nvalue: List[str] = []\nvalue2: Optional[str] = None\nvalue3: Union[str, int] = 1\n```\n### Root\n```\nprotoc -I . --python_betterproto_opt=typing.root --python_betterproto_out=lib example.proto\n```\nthis configuration loads the root typing module, and then access the types off of it directly:\n```\nimport typing\n...\nvalue: typing.List[str] = []\nvalue2: typing.Optional[str] = None\nvalue3: typing.Union[str, int] = 1\n```\n\n### 310\n```\nprotoc -I . --python_betterproto_opt=typing.310 --python_betterproto_out=lib example.proto\n```\nthis configuration avoid loading typing all together if possible and uses the python 3.10 pattern:\n```\n...\nvalue: list[str] = []\nvalue2: str | None = None\nvalue3: str | int = 1\n```\n\n## Development\n\n- _Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!_\n- _See how you can help → [Contributing](.github/CONTRIBUTING.md)_\n\n### Requirements\n\n- Python (3.8 or higher)\n\n- [poetry](https://python-poetry.org/docs/#installation)\n *Needed to install dependencies in a virtual environment*\n\n- [poethepoet](https://github.com/nat-n/poethepoet) for running development tasks as defined in pyproject.toml\n - Can be installed to your host environment via `pip install poethepoet` then executed as simple `poe`\n - or run from the poetry venv as `poetry run poe`\n\n### Setup\n\n```sh\n# Get set up with the virtual env & dependencies\npoetry install -E compiler\n\n# Activate the poetry environment\npoetry shell\n```\n\n### Code style\n\nThis project enforces [black](https://github.com/psf/black) python code formatting.\n\nBefore committing changes run:\n\n```sh\npoe format\n```\n\nTo avoid merge conflicts later, non-black formatted python code will fail in CI.\n\n### Tests\n\nThere are two types of tests:\n\n1. Standard tests\n2. Custom tests\n\n#### Standard tests\n\nAdding a standard test case is easy.\n\n- Create a new directory `betterproto/tests/inputs/<name>`\n - add `<name>.proto` with a message called `Test`\n - add `<name>.json` with some test data (optional)\n\nIt will be picked up automatically when you run the tests.\n\n- See also: [Standard Tests Development Guide](tests/README.md)\n\n#### Custom tests\n\nCustom tests are found in `tests/test_*.py` and are run with pytest.\n\n#### Running\n\nHere's how to run the tests.\n\n```sh\n# Generate assets from sample .proto files required by the tests\npoe generate\n# Run the tests\npoe test\n```\n\nTo run tests as they are run in CI (with tox) run:\n\n```sh\npoe full-test\n```\n\n### (Re)compiling Google Well-known Types\n\nBetterproto includes compiled versions for Google's well-known types at [src/betterproto/lib/google](src/betterproto/lib/google).\nBe sure to regenerate these files when modifying the plugin output format, and validate by running the tests.\n\nNormally, the plugin does not compile any references to `google.protobuf`, since they are pre-compiled. To force compilation of `google.protobuf`, use the option `--custom_opt=INCLUDE_GOOGLE`.\n\nAssuming your `google.protobuf` source files (included with all releases of `protoc`) are located in `/usr/local/include`, you can regenerate them as follows:\n\n```sh\nprotoc \\\n --plugin=protoc-gen-custom=src/betterproto/plugin/main.py \\\n --custom_opt=INCLUDE_GOOGLE \\\n --custom_out=src/betterproto/lib \\\n -I /usr/local/include/ \\\n /usr/local/include/google/protobuf/*.proto\n```\n\n### TODO\n\n- [x] Fixed length fields\n - [x] Packed fixed-length\n- [x] Zig-zag signed fields (sint32, sint64)\n- [x] Don't encode zero values for nested types\n- [x] Enums\n- [x] Repeated message fields\n- [x] Maps\n - [x] Maps of message fields\n- [x] Support passthrough of unknown fields\n- [x] Refs to nested types\n- [x] Imports in proto files\n- [x] Well-known Google types\n - [ ] Support as request input\n - [ ] Support as response output\n - [ ] Automatically wrap/unwrap responses\n- [x] OneOf support\n - [x] Basic support on the wire\n - [x] Check which was set from the group\n - [x] Setting one unsets the others\n- [ ] JSON that isn't completely naive.\n - [x] 64-bit ints as strings\n - [x] Maps\n - [x] Lists\n - [x] Bytes as base64\n - [ ] Any support\n - [x] Enum strings\n - [x] Well known types support (timestamp, duration, wrappers)\n - [x] Support different casing (orig vs. camel vs. others?)\n- [x] Async service stubs\n - [x] Unary-unary\n - [x] Server streaming response\n - [x] Client streaming request\n- [x] Renaming messages and fields to conform to Python name standards\n- [x] Renaming clashes with language keywords\n- [x] Python package\n- [x] Automate running tests\n- [ ] Cleanup!\n\n## License\n\nCopyright \u00a9 2019 Daniel G. Taylor\n\nCopyright \u00a9 2024 The betterproto contributors\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A better Protobuf / gRPC generator & library",
"version": "0.0.2",
"project_urls": {
"Homepage": "https://github.com/betterproto/python-betterproto2",
"Repository": "https://github.com/betterproto/python-betterproto2"
},
"split_keywords": [
"protobuf",
" grpc"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "8ee245cbed548b91924abd0a97d1b616550958be3b73adc3ced914c6a712ae9d",
"md5": "e2b322dab0447e2ebfa4e33b6eba354c",
"sha256": "540d87a9ee30f12852827f9dc69483da7aeb9d17f46864c3b391f7f7fdb5ec07"
},
"downloads": -1,
"filename": "betterproto2-0.0.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e2b322dab0447e2ebfa4e33b6eba354c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 100147,
"upload_time": "2024-12-20T15:22:59",
"upload_time_iso_8601": "2024-12-20T15:22:59.868146Z",
"url": "https://files.pythonhosted.org/packages/8e/e2/45cbed548b91924abd0a97d1b616550958be3b73adc3ced914c6a712ae9d/betterproto2-0.0.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "b6a5608e7d8cb00c99f23fa9a43199c5f2ffef1504ed95569495a8a6594a34e3",
"md5": "752de53eacf546f13672f808d0743226",
"sha256": "f55b9069256e199d61b49230516baf850fe928256e57e2b4d3b9034162d72e14"
},
"downloads": -1,
"filename": "betterproto2-0.0.2.tar.gz",
"has_sig": false,
"md5_digest": "752de53eacf546f13672f808d0743226",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 95996,
"upload_time": "2024-12-20T15:23:02",
"upload_time_iso_8601": "2024-12-20T15:23:02.635858Z",
"url": "https://files.pythonhosted.org/packages/b6/a5/608e7d8cb00c99f23fa9a43199c5f2ffef1504ed95569495a8a6594a34e3/betterproto2-0.0.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-20 15:23:02",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "betterproto",
"github_project": "python-betterproto2",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "betterproto2"
}