%global _empty_manifest_terminate_build 0 Name: python-betterproto Version: 1.2.5 Release: 1 Summary: A better Protobuf / gRPC generator & library License: MIT URL: http://github.com/danielgtaylor/python-betterproto Source0: https://mirrors.nju.edu.cn/pypi/web/packages/ff/2e/abfed7a721928e14aeb900182ff695be474c4ee5f07ef0874cc5ecd5b0b1/betterproto-1.2.5.tar.gz BuildArch: noarch %description # Better Protobuf / gRPC Support for Python ![](https://github.com/danielgtaylor/python-betterproto/workflows/CI/badge.svg) 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.6+ making use of: - Enums - Dataclasses - `async`/`await` - Timezone-aware `datetime` and `timedelta` objects - Relative imports - Mypy type checking 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 & Getting Started 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 ``` Now, 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: ```sh $ protoc -I . --python_betterproto_out=. example.proto ``` This will generate `hello.py` which looks like: ```py # Generated by the protocol buffer compiler. DO NOT EDIT! # sources: hello.proto # plugin: python-betterproto from dataclasses import dataclass import betterproto @dataclass class Hello(betterproto.Message): """Greeting represents a message you can tell a user.""" message: str = betterproto.string_field(1) ``` Now you can use it! ```py >>> from hello import Hello >>> test = Hello() >>> test Hello(message='') >>> test.message = "Hey!" >>> test Hello(message="Hey!") >>> serialized = bytes(test) >>> serialized b'\n\x04Hey!' >>> another = Hello().parse(serialized) >>> another Hello(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 like: ```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); } ``` You can use it like so (enable async in the interactive shell first): ```py >>> import echo >>> from grpclib.client import Channel >>> channel = Channel(host="127.0.0.1", port=1234) >>> service = echo.EchoStub(channel) >>> await service.echo(value="hello", extra_times=1) EchoResponse(values=["hello", "hello"]) >>> async for response in service.echo_stream(value="hello", extra_times=1) print(response) EchoStreamResponse(value="hello") EchoStreamResponse(value="hello") ``` ### 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: ```py >>> MyMessage().to_dict(casing=betterproto.Casing.SNAKE) ``` ### Determining if a message was sent Sometimes it is useful to be able to determine whether a message has been sent on the wire. This is how the Google wrapper types work to let you know whether a value is unset, set as the default (zero value), or set as something else, for example. Use `betterproto.serialized_on_wire(message)` to determine if it was sent. This is a little bit different from the official Google generated Python code, and it lives outside the generated `Message` class to prevent name clashes. Note that it **only** supports Proto 3 and thus can **only** be used to check if `Message` fields are set. You cannot check if a scalar was sent on the wire. ```py # Old way (official Google Protobuf package) >>> mymessage.HasField('myfield') # New way (this project) >>> betterproto.serialized_on_wire(mymessage.myfield) ``` ### 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; } } ``` You can 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] >>> test.on False # Default (zero) values also work. >>> test.name = "" >>> betterproto.which_one_of(test, "foo") ["name", ""] >>> test.count 0 >>> test.on False ``` 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` | [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'} ``` ## Development First, make sure you have Python 3.6+ and `pipenv` installed, along with the official [Protobuf Compiler](https://github.com/protocolbuffers/protobuf/releases) for your platform. Then: ```sh # Get set up with the virtual env & dependencies $ pipenv install --dev # Link the local package $ pipenv shell $ pip install -e . ``` ### Code style This project enforces [black](https://github.com/psf/black) python code formatting. Before commiting changes run: ```bash pipenv run black . ``` 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/` - add `.proto` with a message called `Test` - add `.json` with some test data It will be picked up automatically when you run the tests. - See also: [Standard Tests Development Guide](betterproto/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 $ pipenv run generate # Run all tests $ pipenv run test ``` ### 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?) - [ ] Async service stubs - [x] Unary-unary - [x] Server streaming response - [ ] 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 http://dgt.mit-license.org/ %package -n python3-betterproto Summary: A better Protobuf / gRPC generator & library Provides: python-betterproto BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-pip %description -n python3-betterproto # Better Protobuf / gRPC Support for Python ![](https://github.com/danielgtaylor/python-betterproto/workflows/CI/badge.svg) 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.6+ making use of: - Enums - Dataclasses - `async`/`await` - Timezone-aware `datetime` and `timedelta` objects - Relative imports - Mypy type checking 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 & Getting Started 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 ``` Now, 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: ```sh $ protoc -I . --python_betterproto_out=. example.proto ``` This will generate `hello.py` which looks like: ```py # Generated by the protocol buffer compiler. DO NOT EDIT! # sources: hello.proto # plugin: python-betterproto from dataclasses import dataclass import betterproto @dataclass class Hello(betterproto.Message): """Greeting represents a message you can tell a user.""" message: str = betterproto.string_field(1) ``` Now you can use it! ```py >>> from hello import Hello >>> test = Hello() >>> test Hello(message='') >>> test.message = "Hey!" >>> test Hello(message="Hey!") >>> serialized = bytes(test) >>> serialized b'\n\x04Hey!' >>> another = Hello().parse(serialized) >>> another Hello(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 like: ```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); } ``` You can use it like so (enable async in the interactive shell first): ```py >>> import echo >>> from grpclib.client import Channel >>> channel = Channel(host="127.0.0.1", port=1234) >>> service = echo.EchoStub(channel) >>> await service.echo(value="hello", extra_times=1) EchoResponse(values=["hello", "hello"]) >>> async for response in service.echo_stream(value="hello", extra_times=1) print(response) EchoStreamResponse(value="hello") EchoStreamResponse(value="hello") ``` ### 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: ```py >>> MyMessage().to_dict(casing=betterproto.Casing.SNAKE) ``` ### Determining if a message was sent Sometimes it is useful to be able to determine whether a message has been sent on the wire. This is how the Google wrapper types work to let you know whether a value is unset, set as the default (zero value), or set as something else, for example. Use `betterproto.serialized_on_wire(message)` to determine if it was sent. This is a little bit different from the official Google generated Python code, and it lives outside the generated `Message` class to prevent name clashes. Note that it **only** supports Proto 3 and thus can **only** be used to check if `Message` fields are set. You cannot check if a scalar was sent on the wire. ```py # Old way (official Google Protobuf package) >>> mymessage.HasField('myfield') # New way (this project) >>> betterproto.serialized_on_wire(mymessage.myfield) ``` ### 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; } } ``` You can 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] >>> test.on False # Default (zero) values also work. >>> test.name = "" >>> betterproto.which_one_of(test, "foo") ["name", ""] >>> test.count 0 >>> test.on False ``` 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` | [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'} ``` ## Development First, make sure you have Python 3.6+ and `pipenv` installed, along with the official [Protobuf Compiler](https://github.com/protocolbuffers/protobuf/releases) for your platform. Then: ```sh # Get set up with the virtual env & dependencies $ pipenv install --dev # Link the local package $ pipenv shell $ pip install -e . ``` ### Code style This project enforces [black](https://github.com/psf/black) python code formatting. Before commiting changes run: ```bash pipenv run black . ``` 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/` - add `.proto` with a message called `Test` - add `.json` with some test data It will be picked up automatically when you run the tests. - See also: [Standard Tests Development Guide](betterproto/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 $ pipenv run generate # Run all tests $ pipenv run test ``` ### 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?) - [ ] Async service stubs - [x] Unary-unary - [x] Server streaming response - [ ] 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 http://dgt.mit-license.org/ %package help Summary: Development documents and examples for betterproto Provides: python3-betterproto-doc %description help # Better Protobuf / gRPC Support for Python ![](https://github.com/danielgtaylor/python-betterproto/workflows/CI/badge.svg) 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.6+ making use of: - Enums - Dataclasses - `async`/`await` - Timezone-aware `datetime` and `timedelta` objects - Relative imports - Mypy type checking 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 & Getting Started 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 ``` Now, 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: ```sh $ protoc -I . --python_betterproto_out=. example.proto ``` This will generate `hello.py` which looks like: ```py # Generated by the protocol buffer compiler. DO NOT EDIT! # sources: hello.proto # plugin: python-betterproto from dataclasses import dataclass import betterproto @dataclass class Hello(betterproto.Message): """Greeting represents a message you can tell a user.""" message: str = betterproto.string_field(1) ``` Now you can use it! ```py >>> from hello import Hello >>> test = Hello() >>> test Hello(message='') >>> test.message = "Hey!" >>> test Hello(message="Hey!") >>> serialized = bytes(test) >>> serialized b'\n\x04Hey!' >>> another = Hello().parse(serialized) >>> another Hello(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 like: ```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); } ``` You can use it like so (enable async in the interactive shell first): ```py >>> import echo >>> from grpclib.client import Channel >>> channel = Channel(host="127.0.0.1", port=1234) >>> service = echo.EchoStub(channel) >>> await service.echo(value="hello", extra_times=1) EchoResponse(values=["hello", "hello"]) >>> async for response in service.echo_stream(value="hello", extra_times=1) print(response) EchoStreamResponse(value="hello") EchoStreamResponse(value="hello") ``` ### 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: ```py >>> MyMessage().to_dict(casing=betterproto.Casing.SNAKE) ``` ### Determining if a message was sent Sometimes it is useful to be able to determine whether a message has been sent on the wire. This is how the Google wrapper types work to let you know whether a value is unset, set as the default (zero value), or set as something else, for example. Use `betterproto.serialized_on_wire(message)` to determine if it was sent. This is a little bit different from the official Google generated Python code, and it lives outside the generated `Message` class to prevent name clashes. Note that it **only** supports Proto 3 and thus can **only** be used to check if `Message` fields are set. You cannot check if a scalar was sent on the wire. ```py # Old way (official Google Protobuf package) >>> mymessage.HasField('myfield') # New way (this project) >>> betterproto.serialized_on_wire(mymessage.myfield) ``` ### 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; } } ``` You can 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] >>> test.on False # Default (zero) values also work. >>> test.name = "" >>> betterproto.which_one_of(test, "foo") ["name", ""] >>> test.count 0 >>> test.on False ``` 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` | [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'} ``` ## Development First, make sure you have Python 3.6+ and `pipenv` installed, along with the official [Protobuf Compiler](https://github.com/protocolbuffers/protobuf/releases) for your platform. Then: ```sh # Get set up with the virtual env & dependencies $ pipenv install --dev # Link the local package $ pipenv shell $ pip install -e . ``` ### Code style This project enforces [black](https://github.com/psf/black) python code formatting. Before commiting changes run: ```bash pipenv run black . ``` 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/` - add `.proto` with a message called `Test` - add `.json` with some test data It will be picked up automatically when you run the tests. - See also: [Standard Tests Development Guide](betterproto/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 $ pipenv run generate # Run all tests $ pipenv run test ``` ### 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?) - [ ] Async service stubs - [x] Unary-unary - [x] Server streaming response - [ ] 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 http://dgt.mit-license.org/ %prep %autosetup -n betterproto-1.2.5 %build %py3_build %install %py3_install install -d -m755 %{buildroot}/%{_pkgdocdir} if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi pushd %{buildroot} if [ -d usr/lib ]; then find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/lib64 ]; then find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/bin ]; then find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/sbin ]; then find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst fi touch doclist.lst if [ -d usr/share/man ]; then find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst fi popd mv %{buildroot}/filelist.lst . mv %{buildroot}/doclist.lst . %files -n python3-betterproto -f filelist.lst %dir %{python3_sitelib}/* %files help -f doclist.lst %{_docdir}/* %changelog * Tue Apr 11 2023 Python_Bot - 1.2.5-1 - Package Spec generated