Compare commits
12 Commits
285-semant
...
changelog1
Author | SHA1 | Date | |
---|---|---|---|
|
3eaff291c4 | ||
|
9b5594adbe | ||
|
d991040ff6 | ||
|
d260f071e0 | ||
|
6dd7baa26c | ||
|
573c7292a6 | ||
|
d77f44ebb7 | ||
|
671c0ff4ac | ||
|
9cecc8c3ff | ||
|
bc3cfc5562 | ||
|
b0a36d12e4 | ||
|
a4d2d39546 |
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +0,0 @@
|
||||
## Description
|
||||
|
||||
<!-- Thanks for contributing to betterproto! Add a thorough explanation of what your changes do below this line: -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] This PR targets the `rc` branch (**not** `master`).
|
||||
- [ ] [If this should release a new version to PyPI when merged] The title of the PR follows the [Angular Conventional Commit](https://www.conventionalcommits.org/) syntax (`feat:` or `fix:`, with `BREAKING CHANGE:` in the commit message body if appropriate), and clearly describes the fix or feature.
|
||||
- [ ] Documentation is updated (`README.md` and docstrings).
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Ubuntu, MacOS, Windows]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
python-version: ['3.6.7', '3.7', '3.8', '3.9', '3.10']
|
||||
exclude:
|
||||
- os: Windows
|
||||
python-version: 3.6
|
||||
@@ -66,4 +66,4 @@ jobs:
|
||||
|
||||
- name: Execute test suite
|
||||
shell: bash
|
||||
run: poetry run pytest tests/
|
||||
run: poetry run python -m pytest tests/
|
||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -16,18 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install poetry
|
||||
run: python -m pip install poetry
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build package
|
||||
run: poetry build
|
||||
- name: Publish package to PyPI
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@
|
||||
.pytest_cache
|
||||
.python-version
|
||||
build/
|
||||
node_modules/
|
||||
tests/output_*
|
||||
**/__pycache__
|
||||
dist
|
||||
@@ -18,3 +17,4 @@ output
|
||||
.venv
|
||||
.asv
|
||||
venv
|
||||
.devcontainer
|
||||
|
42
CHANGELOG.md
42
CHANGELOG.md
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Versions suffixed with `b*` are in `beta` and can be installed with `pip install --pre betterproto`.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- fix: Format field comments also as docstrings (#304)
|
||||
- fix: Fix message text in NotImplementedError (#325)
|
||||
- **Breaking**: Client and Service Stubs take 1 request parameter, not one for each field (#311)
|
||||
Client and Service Stubs no longer pack and unpack the input message fields as parameters.
|
||||
|
||||
Update your client calls and server handlers as follows:
|
||||
|
||||
Clients before:
|
||||
```py
|
||||
response = await service.echo(value="hello", extra_times=1)
|
||||
```
|
||||
Clients after:
|
||||
```py
|
||||
response = await service.echo(EchoRequest(value="hello", extra_times=1))
|
||||
```
|
||||
Servers before:
|
||||
```py
|
||||
async def echo(self, value: str, extra_times: int) -> EchoResponse:
|
||||
```
|
||||
Servers after:
|
||||
```py
|
||||
async def echo(self, echo_request: EchoRequest) -> EchoResponse:
|
||||
# Use echo_request.value
|
||||
# Use echo_request.extra_times
|
||||
```
|
||||
|
||||
|
||||
## [2.0.0b4] - 2022-01-03
|
||||
|
||||
- **Breaking**: the minimum Python version has been bumped to `3.6.2`
|
||||
|
||||
- Always add `AsyncIterator` to imports if there are services [#264](https://github.com/danielgtaylor/python-betterproto/pull/264)
|
||||
- Allow parsing of messages from `ByteStrings` [#266](https://github.com/danielgtaylor/python-betterproto/pull/266)
|
||||
- Add support for proto3 optional [#281](https://github.com/danielgtaylor/python-betterproto/pull/281)
|
||||
|
||||
- Fix compilation of fields with names identical to builtin types [#294](https://github.com/danielgtaylor/python-betterproto/pull/294)
|
||||
- Fix default values for enum service args [#299](https://github.com/danielgtaylor/python-betterproto/pull/299)
|
||||
|
||||
## [2.0.0b3] - 2021-04-07
|
||||
|
||||
- Generate grpclib service stubs [#170](https://github.com/danielgtaylor/python-betterproto/pull/170)
|
||||
@@ -54,7 +94,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [2.0.0b1] - 2020-07-04
|
||||
|
||||
[Upgrade Guide](./docs/upgrading.md)
|
||||
[Upgrade Guide](./docs/upgrading.md)
|
||||
|
||||
> Several bugfixes and improvements required or will require small breaking changes, necessitating a new version.
|
||||
> `2.0.0` will be released once the interface is stable.
|
||||
|
26
README.md
26
README.md
@@ -177,10 +177,10 @@ 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(value="hello", extra_times=1)
|
||||
response = await service.echo(echo.EchoRequest(value="hello", extra_times=1))
|
||||
print(response)
|
||||
|
||||
async for response in service.echo_stream(value="hello", extra_times=1):
|
||||
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!
|
||||
@@ -206,18 +206,18 @@ service methods:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from echo import EchoBase, EchoResponse, EchoStreamResponse
|
||||
from echo import EchoBase, EchoRequest, EchoResponse, EchoStreamResponse
|
||||
from grpclib.server import Server
|
||||
from typing import AsyncIterator
|
||||
|
||||
|
||||
class EchoService(EchoBase):
|
||||
async def echo(self, value: str, extra_times: int) -> "EchoResponse":
|
||||
return EchoResponse([value for _ in range(extra_times)])
|
||||
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, value: str, extra_times: int) -> AsyncIterator["EchoStreamResponse"]:
|
||||
for _ in range(extra_times):
|
||||
yield EchoStreamResponse(value)
|
||||
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():
|
||||
@@ -498,16 +498,6 @@ protoc \
|
||||
- [x] Automate running tests
|
||||
- [ ] Cleanup!
|
||||
|
||||
|
||||
## Release
|
||||
|
||||
New versions are versioned and released using [Semantic Release](https://github.com/semantic-release/semantic-release). When new commits
|
||||
using the Angular Conventional Commits syntax land on `master` or `rc`, those commits are used to determine what new version to release.
|
||||
|
||||
All Pull Requests must target the `rc` branch; when merged into `rc` they will publish new release candidate (`rc`) versions to PyPI
|
||||
automatically. When maintainers want to publish a new full release, they simply merge `rc` into `master`. This flow ensures that features
|
||||
and fixes are published quickly and continuously rather than awaiting a manual release process.
|
||||
|
||||
## Community
|
||||
|
||||
Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!
|
||||
|
52
package.json
52
package.json
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "python-betterproto-semantic-release",
|
||||
"version": "1.0.0",
|
||||
"description": "Encapsulate dependencies needed to use semantic-release",
|
||||
"dependencies": {
|
||||
"@semantic-release/exec": "^5.0.0",
|
||||
"@semantic-release/git": "^9.0.0",
|
||||
"@semantic-release/gitlab": "^6.0.4",
|
||||
"conventional-changelog-eslint": "^3.0.8",
|
||||
"semantic-release": "^17.1.1"
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"master",
|
||||
{
|
||||
"name": "rc",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"preset": "angular"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"preset": "angular"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "poetry version ${nextRelease.version}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"pyproject.toml"
|
||||
],
|
||||
"message": "Release v${nextRelease.version} [skip ci]"
|
||||
}
|
||||
]
|
||||
],
|
||||
"repositoryUrl": "ssh://git@github.com/danielgtaylor/python-betterproto.git"
|
||||
}
|
||||
}
|
1330
poetry.lock
generated
1330
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "betterproto"
|
||||
version = "2.0.0b3"
|
||||
version = "2.0.0b4"
|
||||
description = "A better Protobuf / gRPC generator & library"
|
||||
authors = ["Daniel G. Taylor <danielgtaylor@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -12,7 +12,7 @@ packages = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
python = ">=3.6.2,<4.0"
|
||||
black = { version = ">=19.3b0", optional = true }
|
||||
dataclasses = { version = "^0.7", python = ">=3.6, <3.7" }
|
||||
grpclib = "^0.4.1"
|
||||
@@ -21,14 +21,14 @@ python-dateutil = "^2.8"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
asv = "^0.4.2"
|
||||
black = "^20.8b1"
|
||||
black = "^21.11b0"
|
||||
bpython = "^0.19"
|
||||
grpcio-tools = "^1.30.0"
|
||||
grpcio-tools = "^1.40.0"
|
||||
jinja2 = "^2.11.2"
|
||||
mypy = "^0.770"
|
||||
mypy = "^0.930"
|
||||
poethepoet = ">=0.9.0"
|
||||
protobuf = "^3.12.2"
|
||||
pytest = "^5.4.2"
|
||||
pytest = "^6.2.5"
|
||||
pytest-asyncio = "^0.12.0"
|
||||
pytest-cov = "^2.9.0"
|
||||
pytest-mock = "^3.1.1"
|
||||
@@ -111,7 +111,7 @@ omit = ["betterproto/tests/*"]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist = py36, py37, py38
|
||||
envlist = py36, py37, py38, py310
|
||||
|
||||
[testenv]
|
||||
whitelist_externals = poetry
|
||||
|
@@ -145,6 +145,8 @@ class FieldMetadata:
|
||||
group: Optional[str] = None
|
||||
# Describes the wrapped type (e.g. when using google.protobuf.BoolValue)
|
||||
wraps: Optional[str] = None
|
||||
# Is the field optional
|
||||
optional: Optional[bool] = False
|
||||
|
||||
@staticmethod
|
||||
def get(field: dataclasses.Field) -> "FieldMetadata":
|
||||
@@ -159,12 +161,15 @@ def dataclass_field(
|
||||
map_types: Optional[Tuple[str, str]] = None,
|
||||
group: Optional[str] = None,
|
||||
wraps: Optional[str] = None,
|
||||
optional: bool = False,
|
||||
) -> dataclasses.Field:
|
||||
"""Creates a dataclass field with attached protobuf metadata."""
|
||||
return dataclasses.field(
|
||||
default=PLACEHOLDER,
|
||||
default=None if optional else PLACEHOLDER,
|
||||
metadata={
|
||||
"betterproto": FieldMetadata(number, proto_type, map_types, group, wraps)
|
||||
"betterproto": FieldMetadata(
|
||||
number, proto_type, map_types, group, wraps, optional
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -174,74 +179,107 @@ def dataclass_field(
|
||||
# out at runtime. The generated dataclass variables are still typed correctly.
|
||||
|
||||
|
||||
def enum_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_ENUM, group=group)
|
||||
def enum_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any:
|
||||
return dataclass_field(number, TYPE_ENUM, group=group, optional=optional)
|
||||
|
||||
|
||||
def bool_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_BOOL, group=group)
|
||||
def bool_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any:
|
||||
return dataclass_field(number, TYPE_BOOL, group=group, optional=optional)
|
||||
|
||||
|
||||
def int32_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_INT32, group=group)
|
||||
def int32_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_INT32, group=group, optional=optional)
|
||||
|
||||
|
||||
def int64_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_INT64, group=group)
|
||||
def int64_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_INT64, group=group, optional=optional)
|
||||
|
||||
|
||||
def uint32_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_UINT32, group=group)
|
||||
def uint32_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_UINT32, group=group, optional=optional)
|
||||
|
||||
|
||||
def uint64_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_UINT64, group=group)
|
||||
def uint64_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_UINT64, group=group, optional=optional)
|
||||
|
||||
|
||||
def sint32_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_SINT32, group=group)
|
||||
def sint32_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_SINT32, group=group, optional=optional)
|
||||
|
||||
|
||||
def sint64_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_SINT64, group=group)
|
||||
def sint64_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_SINT64, group=group, optional=optional)
|
||||
|
||||
|
||||
def float_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_FLOAT, group=group)
|
||||
def float_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_FLOAT, group=group, optional=optional)
|
||||
|
||||
|
||||
def double_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_DOUBLE, group=group)
|
||||
def double_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_DOUBLE, group=group, optional=optional)
|
||||
|
||||
|
||||
def fixed32_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_FIXED32, group=group)
|
||||
def fixed32_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_FIXED32, group=group, optional=optional)
|
||||
|
||||
|
||||
def fixed64_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_FIXED64, group=group)
|
||||
def fixed64_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_FIXED64, group=group, optional=optional)
|
||||
|
||||
|
||||
def sfixed32_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_SFIXED32, group=group)
|
||||
def sfixed32_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_SFIXED32, group=group, optional=optional)
|
||||
|
||||
|
||||
def sfixed64_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_SFIXED64, group=group)
|
||||
def sfixed64_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_SFIXED64, group=group, optional=optional)
|
||||
|
||||
|
||||
def string_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_STRING, group=group)
|
||||
def string_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_STRING, group=group, optional=optional)
|
||||
|
||||
|
||||
def bytes_field(number: int, group: Optional[str] = None) -> Any:
|
||||
return dataclass_field(number, TYPE_BYTES, group=group)
|
||||
def bytes_field(
|
||||
number: int, group: Optional[str] = None, optional: bool = False
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_BYTES, group=group, optional=optional)
|
||||
|
||||
|
||||
def message_field(
|
||||
number: int, group: Optional[str] = None, wraps: Optional[str] = None
|
||||
number: int,
|
||||
group: Optional[str] = None,
|
||||
wraps: Optional[str] = None,
|
||||
optional: bool = False,
|
||||
) -> Any:
|
||||
return dataclass_field(number, TYPE_MESSAGE, group=group, wraps=wraps)
|
||||
return dataclass_field(
|
||||
number, TYPE_MESSAGE, group=group, wraps=wraps, optional=optional
|
||||
)
|
||||
|
||||
|
||||
def map_field(
|
||||
@@ -586,7 +624,8 @@ class Message(ABC):
|
||||
if meta.group:
|
||||
group_current.setdefault(meta.group)
|
||||
|
||||
if self.__raw_get(field_name) != PLACEHOLDER:
|
||||
value = self.__raw_get(field_name)
|
||||
if value != PLACEHOLDER and not (meta.optional and value is None):
|
||||
# Found a non-sentinel value
|
||||
all_sentinel = False
|
||||
|
||||
@@ -701,12 +740,16 @@ class Message(ABC):
|
||||
|
||||
if value is None:
|
||||
# Optional items should be skipped. This is used for the Google
|
||||
# wrapper types.
|
||||
# wrapper types and proto3 field presence/optional fields.
|
||||
continue
|
||||
|
||||
# Being selected in a a group means this field is the one that is
|
||||
# currently set in a `oneof` group, so it must be serialized even
|
||||
# if the value is the default zero value.
|
||||
#
|
||||
# Note that proto3 field presence/optional fields are put in a
|
||||
# synthetic single-item oneof by protoc, which helps us ensure we
|
||||
# send the value even if the value is the default zero value.
|
||||
selected_in_group = (
|
||||
meta.group and self._group_current[meta.group] == field_name
|
||||
)
|
||||
@@ -803,7 +846,7 @@ class Message(ABC):
|
||||
@classmethod
|
||||
def _type_hints(cls) -> Dict[str, Type]:
|
||||
module = sys.modules[cls.__module__]
|
||||
return get_type_hints(cls, vars(module))
|
||||
return get_type_hints(cls, module.__dict__, {})
|
||||
|
||||
@classmethod
|
||||
def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type:
|
||||
@@ -829,8 +872,9 @@ class Message(ABC):
|
||||
# This is some kind of list (repeated) field.
|
||||
return list
|
||||
elif t.__origin__ is Union and t.__args__[1] is type(None):
|
||||
# This is an optional (wrapped) field. For setting the default we
|
||||
# really don't care what kind of field it is.
|
||||
# This is an optional field (either wrapped, or using proto3
|
||||
# field presence). For setting the default we really don't care
|
||||
# what kind of field it is.
|
||||
return type(None)
|
||||
else:
|
||||
return t
|
||||
@@ -1041,6 +1085,9 @@ class Message(ABC):
|
||||
]
|
||||
if value or include_default_values:
|
||||
output[cased_name] = value
|
||||
elif value is None:
|
||||
if include_default_values:
|
||||
output[cased_name] = value
|
||||
elif (
|
||||
value._serialized_on_wire
|
||||
or include_default_values
|
||||
@@ -1066,6 +1113,9 @@ class Message(ABC):
|
||||
if meta.proto_type in INT_64_TYPES:
|
||||
if field_is_repeated:
|
||||
output[cased_name] = [str(n) for n in value]
|
||||
elif value is None:
|
||||
if include_default_values:
|
||||
output[cased_name] = value
|
||||
else:
|
||||
output[cased_name] = str(value)
|
||||
elif meta.proto_type == TYPE_BYTES:
|
||||
@@ -1073,6 +1123,8 @@ class Message(ABC):
|
||||
output[cased_name] = [
|
||||
b64encode(b).decode("utf8") for b in value
|
||||
]
|
||||
elif value is None and include_default_values:
|
||||
output[cased_name] = value
|
||||
else:
|
||||
output[cased_name] = b64encode(value).decode("utf8")
|
||||
elif meta.proto_type == TYPE_ENUM:
|
||||
@@ -1085,6 +1137,12 @@ class Message(ABC):
|
||||
else:
|
||||
# transparently upgrade single value to repeated
|
||||
output[cased_name] = [enum_class(value).name]
|
||||
elif value is None:
|
||||
if include_default_values:
|
||||
output[cased_name] = value
|
||||
elif meta.optional:
|
||||
enum_class = field_types[field_name].__args__[0]
|
||||
output[cased_name] = enum_class(value).name
|
||||
else:
|
||||
enum_class = field_types[field_name] # noqa
|
||||
output[cased_name] = enum_class(value).name
|
||||
@@ -1141,6 +1199,9 @@ class Message(ABC):
|
||||
setattr(self, field_name, v)
|
||||
elif meta.wraps:
|
||||
setattr(self, field_name, value[key])
|
||||
elif v is None:
|
||||
cls = self._betterproto.cls_by_field[field_name]
|
||||
setattr(self, field_name, cls().from_dict(value[key]))
|
||||
else:
|
||||
# NOTE: `from_dict` mutates the underlying message, so no
|
||||
# assignment here is necessary.
|
||||
|
@@ -133,16 +133,6 @@ def lowercase_first(value: str) -> str:
|
||||
return value[0:1].lower() + value[1:]
|
||||
|
||||
|
||||
def is_reserved_name(value: str) -> bool:
|
||||
if keyword.iskeyword(value):
|
||||
return True
|
||||
|
||||
if value in ("bytes", "str"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_name(value: str) -> str:
|
||||
# https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles
|
||||
return f"{value}_" if is_reserved_name(value) else value
|
||||
return f"{value}_" if keyword.iskeyword(value) else value
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from abc import ABC
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Callable, Any, Dict
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
import grpclib
|
||||
import grpclib.server
|
||||
@@ -15,10 +15,10 @@ class ServiceBase(ABC):
|
||||
self,
|
||||
handler: Callable,
|
||||
stream: grpclib.server.Stream,
|
||||
request_kwargs: Dict[str, Any],
|
||||
request: Any,
|
||||
) -> None:
|
||||
|
||||
response_iter = handler(**request_kwargs)
|
||||
response_iter = handler(request)
|
||||
# check if response is actually an AsyncIterator
|
||||
# this might be false if the method just returns without
|
||||
# yielding at least once
|
||||
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
||||
from typing import Dict, List
|
||||
|
||||
import betterproto
|
||||
from betterproto.grpc.grpclib_server import ServiceBase
|
||||
|
||||
|
||||
class Syntax(betterproto.Enum):
|
||||
@@ -46,17 +47,6 @@ class FieldCardinality(betterproto.Enum):
|
||||
CARDINALITY_REPEATED = 3
|
||||
|
||||
|
||||
class NullValue(betterproto.Enum):
|
||||
"""
|
||||
`NullValue` is a singleton enumeration to represent the null value for the
|
||||
`Value` type union. The JSON representation for `NullValue` is JSON
|
||||
`null`.
|
||||
"""
|
||||
|
||||
# Null value.
|
||||
NULL_VALUE = 0
|
||||
|
||||
|
||||
class FieldDescriptorProtoType(betterproto.Enum):
|
||||
TYPE_DOUBLE = 1
|
||||
TYPE_FLOAT = 2
|
||||
@@ -108,165 +98,15 @@ class MethodOptionsIdempotencyLevel(betterproto.Enum):
|
||||
IDEMPOTENT = 2
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Timestamp(betterproto.Message):
|
||||
class NullValue(betterproto.Enum):
|
||||
"""
|
||||
A Timestamp represents a point in time independent of any time zone or
|
||||
local calendar, encoded as a count of seconds and fractions of seconds at
|
||||
nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
Gregorian calendar backwards to year one. All minutes are 60 seconds long.
|
||||
Leap seconds are "smeared" so that no leap second table is needed for
|
||||
interpretation, using a [24-hour linear
|
||||
smear](https://developers.google.com/time/smear). The range is from
|
||||
0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By restricting to
|
||||
that range, we ensure that we can convert to and from [RFC
|
||||
3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. # Examples
|
||||
Example 1: Compute Timestamp from POSIX `time()`. Timestamp timestamp;
|
||||
timestamp.set_seconds(time(NULL)); timestamp.set_nanos(0); Example 2:
|
||||
Compute Timestamp from POSIX `gettimeofday()`. struct timeval tv;
|
||||
gettimeofday(&tv, NULL); Timestamp timestamp;
|
||||
timestamp.set_seconds(tv.tv_sec); timestamp.set_nanos(tv.tv_usec *
|
||||
1000); Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
FILETIME ft; GetSystemTimeAsFileTime(&ft); UINT64 ticks =
|
||||
(((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; // A Windows
|
||||
tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z // is
|
||||
11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. Timestamp
|
||||
timestamp; timestamp.set_seconds((INT64) ((ticks / 10000000) -
|
||||
11644473600LL)); timestamp.set_nanos((INT32) ((ticks % 10000000) *
|
||||
100)); Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
long millis = System.currentTimeMillis(); Timestamp timestamp =
|
||||
Timestamp.newBuilder().setSeconds(millis / 1000) .setNanos((int)
|
||||
((millis % 1000) * 1000000)).build(); Example 5: Compute Timestamp from
|
||||
current time in Python. timestamp = Timestamp()
|
||||
timestamp.GetCurrentTime() # JSON Mapping In JSON format, the Timestamp
|
||||
type is encoded as a string in the [RFC
|
||||
3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the format is
|
||||
"{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where {year} is
|
||||
always expressed using four digits while {month}, {day}, {hour}, {min}, and
|
||||
{sec} are zero-padded to two digits each. The fractional seconds, which can
|
||||
go up to 9 digits (i.e. up to 1 nanosecond resolution), are optional. The
|
||||
"Z" suffix indicates the timezone ("UTC"); the timezone is required. A
|
||||
proto3 JSON serializer should always use UTC (as indicated by "Z") when
|
||||
printing the Timestamp type and a proto3 JSON parser should be able to
|
||||
accept both UTC and other timezones (as indicated by an offset). For
|
||||
example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 01:30 UTC on
|
||||
January 15, 2017. In JavaScript, one can convert a Date object to this
|
||||
format using the standard [toISOString()](https://developer.mozilla.org/en-
|
||||
US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) method.
|
||||
In Python, a standard `datetime.datetime` object can be converted to this
|
||||
format using
|
||||
[`strftime`](https://docs.python.org/2/library/time.html#time.strftime)
|
||||
with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one
|
||||
can use the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
http://www.joda.org/joda-
|
||||
time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D )
|
||||
to obtain a formatter capable of generating timestamps in this format.
|
||||
`NullValue` is a singleton enumeration to represent the null value for the
|
||||
`Value` type union. The JSON representation for `NullValue` is JSON
|
||||
`null`.
|
||||
"""
|
||||
|
||||
# Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must
|
||||
# be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
|
||||
seconds: int = betterproto.int64_field(1)
|
||||
# Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
# second values with fractions must still have non-negative nanos values that
|
||||
# count forward in time. Must be from 0 to 999,999,999 inclusive.
|
||||
nanos: int = betterproto.int32_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class FieldMask(betterproto.Message):
|
||||
"""
|
||||
`FieldMask` represents a set of symbolic field paths, for example:
|
||||
paths: "f.a" paths: "f.b.d" Here `f` represents a field in some root
|
||||
message, `a` and `b` fields in the message found in `f`, and `d` a field
|
||||
found in the message in `f.b`. Field masks are used to specify a subset of
|
||||
fields that should be returned by a get operation or modified by an update
|
||||
operation. Field masks also have a custom JSON encoding (see below). #
|
||||
Field Masks in Projections When used in the context of a projection, a
|
||||
response message or sub-message is filtered by the API to only contain
|
||||
those fields as specified in the mask. For example, if the mask in the
|
||||
previous example is applied to a response message as follows: f {
|
||||
a : 22 b { d : 1 x : 2 } y : 13 }
|
||||
z: 8 The result will not contain specific values for fields x,y and z
|
||||
(their value will be set to the default, and omitted in proto text output):
|
||||
f { a : 22 b { d : 1 } } A repeated field is
|
||||
not allowed except at the last position of a paths string. If a FieldMask
|
||||
object is not present in a get operation, the operation applies to all
|
||||
fields (as if a FieldMask of all fields had been specified). Note that a
|
||||
field mask does not necessarily apply to the top-level response message. In
|
||||
case of a REST get operation, the field mask applies directly to the
|
||||
response, but in case of a REST list operation, the mask instead applies to
|
||||
each individual message in the returned resource list. In case of a REST
|
||||
custom method, other definitions may be used. Where the mask applies will
|
||||
be clearly documented together with its declaration in the API. In any
|
||||
case, the effect on the returned resource/resources is required behavior
|
||||
for APIs. # Field Masks in Update Operations A field mask in update
|
||||
operations specifies which fields of the targeted resource are going to be
|
||||
updated. The API is required to only change the values of the fields as
|
||||
specified in the mask and leave the others untouched. If a resource is
|
||||
passed in to describe the updated values, the API ignores the values of all
|
||||
fields not covered by the mask. If a repeated field is specified for an
|
||||
update operation, new values will be appended to the existing repeated
|
||||
field in the target resource. Note that a repeated field is only allowed in
|
||||
the last position of a `paths` string. If a sub-message is specified in the
|
||||
last position of the field mask for an update operation, then new value
|
||||
will be merged into the existing sub-message in the target resource. For
|
||||
example, given the target message: f { b { d: 1
|
||||
x: 2 } c: [1] } And an update message: f { b {
|
||||
d: 10 } c: [2] } then if the field mask is: paths: ["f.b",
|
||||
"f.c"] then the result will be: f { b { d: 10 x:
|
||||
2 } c: [1, 2] } An implementation may provide options to
|
||||
override this default behavior for repeated and message fields. In order to
|
||||
reset a field's value to the default, the field must be in the mask and set
|
||||
to the default value in the provided resource. Hence, in order to reset all
|
||||
fields of a resource, provide a default instance of the resource and set
|
||||
all fields in the mask, or do not provide a mask as described below. If a
|
||||
field mask is not present on update, the operation applies to all fields
|
||||
(as if a field mask of all fields has been specified). Note that in the
|
||||
presence of schema evolution, this may mean that fields the client does not
|
||||
know and has therefore not filled into the request will be reset to their
|
||||
default. If this is unwanted behavior, a specific service may require a
|
||||
client to always specify a field mask, producing an error if not. As with
|
||||
get operations, the location of the resource which describes the updated
|
||||
values in the request message depends on the operation kind. In any case,
|
||||
the effect of the field mask is required to be honored by the API. ##
|
||||
Considerations for HTTP REST The HTTP kind of an update operation which
|
||||
uses a field mask must be set to PATCH instead of PUT in order to satisfy
|
||||
HTTP semantics (PUT must only be used for full updates). # JSON Encoding of
|
||||
Field Masks In JSON, a field mask is encoded as a single string where paths
|
||||
are separated by a comma. Fields name in each path are converted to/from
|
||||
lower-camel naming conventions. As an example, consider the following
|
||||
message declarations: message Profile { User user = 1;
|
||||
Photo photo = 2; } message User { string display_name = 1;
|
||||
string address = 2; } In proto a field mask for `Profile` may look as
|
||||
such: mask { paths: "user.display_name" paths: "photo"
|
||||
} In JSON, the same mask is represented as below: { mask:
|
||||
"user.displayName,photo" } # Field Masks and Oneof Fields Field masks
|
||||
treat fields in oneofs just as regular fields. Consider the following
|
||||
message: message SampleMessage { oneof test_oneof {
|
||||
string name = 4; SubMessage sub_message = 9; } } The
|
||||
field mask can be: mask { paths: "name" } Or: mask {
|
||||
paths: "sub_message" } Note that oneof type names ("test_oneof" in this
|
||||
case) cannot be used in paths. ## Field Mask Verification The
|
||||
implementation of any API method which has a FieldMask type field in the
|
||||
request should verify the included field paths, and return an
|
||||
`INVALID_ARGUMENT` error if any path is unmappable.
|
||||
"""
|
||||
|
||||
# The set of field mask paths.
|
||||
paths: List[str] = betterproto.string_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class SourceContext(betterproto.Message):
|
||||
"""
|
||||
`SourceContext` represents information about the source of a protobuf
|
||||
element, like the file in which it is defined.
|
||||
"""
|
||||
|
||||
# The path-qualified name of the .proto file that contained the associated
|
||||
# protobuf element. For example: `"google/protobuf/source_context.proto"`.
|
||||
file_name: str = betterproto.string_field(1)
|
||||
# Null value.
|
||||
NULL_VALUE = 0
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
@@ -283,24 +123,25 @@ class Any(betterproto.Message):
|
||||
Example 3: Pack and unpack a message in Python. foo = Foo(...) any
|
||||
= Any() any.Pack(foo) ... if any.Is(Foo.DESCRIPTOR):
|
||||
any.Unpack(foo) ... Example 4: Pack and unpack a message in Go
|
||||
foo := &pb.Foo{...} any, err := ptypes.MarshalAny(foo) ...
|
||||
foo := &pb.Foo{} if err := ptypes.UnmarshalAny(any, foo); err != nil {
|
||||
... } The pack methods provided by protobuf library will by default
|
||||
use 'type.googleapis.com/full.type.name' as the type URL and the unpack
|
||||
methods only use the fully qualified type name after the last '/' in the
|
||||
type URL, for example "foo.bar.com/x/y.z" will yield type name "y.z". JSON
|
||||
==== The JSON representation of an `Any` value uses the regular
|
||||
representation of the deserialized, embedded message, with an additional
|
||||
field `@type` which contains the type URL. Example: package
|
||||
google.profile; message Person { string first_name = 1;
|
||||
string last_name = 2; } { "@type":
|
||||
"type.googleapis.com/google.profile.Person", "firstName": <string>,
|
||||
"lastName": <string> } If the embedded message type is well-known and
|
||||
has a custom JSON representation, that representation will be embedded
|
||||
adding a field `value` which holds the custom JSON in addition to the
|
||||
`@type` field. Example (for message [google.protobuf.Duration][]): {
|
||||
"@type": "type.googleapis.com/google.protobuf.Duration", "value":
|
||||
"1.212s" }
|
||||
foo := &pb.Foo{...} any, err := anypb.New(foo) if err != nil {
|
||||
... } ... foo := &pb.Foo{} if err :=
|
||||
any.UnmarshalTo(foo); err != nil { ... } The pack methods
|
||||
provided by protobuf library will by default use
|
||||
'type.googleapis.com/full.type.name' as the type URL and the unpack methods
|
||||
only use the fully qualified type name after the last '/' in the type URL,
|
||||
for example "foo.bar.com/x/y.z" will yield type name "y.z". JSON ==== The
|
||||
JSON representation of an `Any` value uses the regular representation of
|
||||
the deserialized, embedded message, with an additional field `@type` which
|
||||
contains the type URL. Example: package google.profile; message
|
||||
Person { string first_name = 1; string last_name = 2; }
|
||||
{ "@type": "type.googleapis.com/google.profile.Person",
|
||||
"firstName": <string>, "lastName": <string> } If the embedded
|
||||
message type is well-known and has a custom JSON representation, that
|
||||
representation will be embedded adding a field `value` which holds the
|
||||
custom JSON in addition to the `@type` field. Example (for message
|
||||
[google.protobuf.Duration][]): { "@type":
|
||||
"type.googleapis.com/google.protobuf.Duration", "value": "1.212s"
|
||||
}
|
||||
"""
|
||||
|
||||
# A URL/resource name that uniquely identifies the type of the serialized
|
||||
@@ -327,6 +168,18 @@ class Any(betterproto.Message):
|
||||
value: bytes = betterproto.bytes_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class SourceContext(betterproto.Message):
|
||||
"""
|
||||
`SourceContext` represents information about the source of a protobuf
|
||||
element, like the file in which it is defined.
|
||||
"""
|
||||
|
||||
# The path-qualified name of the .proto file that contained the associated
|
||||
# protobuf element. For example: `"google/protobuf/source_context.proto"`.
|
||||
file_name: str = betterproto.string_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Type(betterproto.Message):
|
||||
"""A protocol buffer message type."""
|
||||
@@ -510,7 +363,7 @@ class Mixin(betterproto.Message):
|
||||
implies that all methods in `AccessControl` are also declared with same
|
||||
name and request/response types in `Storage`. A documentation generator or
|
||||
annotation processor will see the effective `Storage.GetAcl` method after
|
||||
inherting documentation and annotations as follows: service Storage {
|
||||
inheriting documentation and annotations as follows: service Storage {
|
||||
// Get the underlying ACL object. rpc GetAcl(GetAclRequest) returns
|
||||
(Acl) { option (google.api.http).get = "/v2/{resource=**}:getAcl";
|
||||
} ... } Note how the version in the path pattern changed from
|
||||
@@ -530,215 +383,6 @@ class Mixin(betterproto.Message):
|
||||
root: str = betterproto.string_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Duration(betterproto.Message):
|
||||
"""
|
||||
A Duration represents a signed, fixed-length span of time represented as a
|
||||
count of seconds and fractions of seconds at nanosecond resolution. It is
|
||||
independent of any calendar and concepts like "day" or "month". It is
|
||||
related to Timestamp in that the difference between two Timestamp values is
|
||||
a Duration and it can be added or subtracted from a Timestamp. Range is
|
||||
approximately +-10,000 years. # Examples Example 1: Compute Duration from
|
||||
two Timestamps in pseudo code. Timestamp start = ...; Timestamp end
|
||||
= ...; Duration duration = ...; duration.seconds = end.seconds -
|
||||
start.seconds; duration.nanos = end.nanos - start.nanos; if
|
||||
(duration.seconds < 0 && duration.nanos > 0) { duration.seconds += 1;
|
||||
duration.nanos -= 1000000000; } else if (duration.seconds > 0 &&
|
||||
duration.nanos < 0) { duration.seconds -= 1; duration.nanos +=
|
||||
1000000000; } Example 2: Compute Timestamp from Timestamp + Duration in
|
||||
pseudo code. Timestamp start = ...; Duration duration = ...;
|
||||
Timestamp end = ...; end.seconds = start.seconds + duration.seconds;
|
||||
end.nanos = start.nanos + duration.nanos; if (end.nanos < 0) {
|
||||
end.seconds -= 1; end.nanos += 1000000000; } else if (end.nanos
|
||||
>= 1000000000) { end.seconds += 1; end.nanos -= 1000000000;
|
||||
} Example 3: Compute Duration from datetime.timedelta in Python. td =
|
||||
datetime.timedelta(days=3, minutes=10) duration = Duration()
|
||||
duration.FromTimedelta(td) # JSON Mapping In JSON format, the Duration type
|
||||
is encoded as a string rather than an object, where the string ends in the
|
||||
suffix "s" (indicating seconds) and is preceded by the number of seconds,
|
||||
with nanoseconds expressed as fractional seconds. For example, 3 seconds
|
||||
with 0 nanoseconds should be encoded in JSON format as "3s", while 3
|
||||
seconds and 1 nanosecond should be expressed in JSON format as
|
||||
"3.000000001s", and 3 seconds and 1 microsecond should be expressed in JSON
|
||||
format as "3.000001s".
|
||||
"""
|
||||
|
||||
# Signed seconds of the span of time. Must be from -315,576,000,000 to
|
||||
# +315,576,000,000 inclusive. Note: these bounds are computed from: 60
|
||||
# sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
|
||||
seconds: int = betterproto.int64_field(1)
|
||||
# Signed fractions of a second at nanosecond resolution of the span of time.
|
||||
# Durations less than one second are represented with a 0 `seconds` field and
|
||||
# a positive or negative `nanos` field. For durations of one second or more,
|
||||
# a non-zero value for the `nanos` field must be of the same sign as the
|
||||
# `seconds` field. Must be from -999,999,999 to +999,999,999 inclusive.
|
||||
nanos: int = betterproto.int32_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Struct(betterproto.Message):
|
||||
"""
|
||||
`Struct` represents a structured data value, consisting of fields which map
|
||||
to dynamically typed values. In some languages, `Struct` might be supported
|
||||
by a native representation. For example, in scripting languages like JS a
|
||||
struct is represented as an object. The details of that representation are
|
||||
described together with the proto support for the language. The JSON
|
||||
representation for `Struct` is JSON object.
|
||||
"""
|
||||
|
||||
# Unordered map of dynamically typed values.
|
||||
fields: Dict[str, "Value"] = betterproto.map_field(
|
||||
1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE
|
||||
)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Value(betterproto.Message):
|
||||
"""
|
||||
`Value` represents a dynamically typed value which can be either null, a
|
||||
number, a string, a boolean, a recursive struct value, or a list of values.
|
||||
A producer of value is expected to set one of that variants, absence of any
|
||||
variant indicates an error. The JSON representation for `Value` is JSON
|
||||
value.
|
||||
"""
|
||||
|
||||
# Represents a null value.
|
||||
null_value: "NullValue" = betterproto.enum_field(1, group="kind")
|
||||
# Represents a double value.
|
||||
number_value: float = betterproto.double_field(2, group="kind")
|
||||
# Represents a string value.
|
||||
string_value: str = betterproto.string_field(3, group="kind")
|
||||
# Represents a boolean value.
|
||||
bool_value: bool = betterproto.bool_field(4, group="kind")
|
||||
# Represents a structured value.
|
||||
struct_value: "Struct" = betterproto.message_field(5, group="kind")
|
||||
# Represents a repeated `Value`.
|
||||
list_value: "ListValue" = betterproto.message_field(6, group="kind")
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class ListValue(betterproto.Message):
|
||||
"""
|
||||
`ListValue` is a wrapper around a repeated field of values. The JSON
|
||||
representation for `ListValue` is JSON array.
|
||||
"""
|
||||
|
||||
# Repeated field of dynamically typed values.
|
||||
values: List["Value"] = betterproto.message_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class DoubleValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `double`. The JSON representation for `DoubleValue` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The double value.
|
||||
value: float = betterproto.double_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class FloatValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `float`. The JSON representation for `FloatValue` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The float value.
|
||||
value: float = betterproto.float_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Int64Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `int64`. The JSON representation for `Int64Value` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The int64 value.
|
||||
value: int = betterproto.int64_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class UInt64Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `uint64`. The JSON representation for `UInt64Value` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The uint64 value.
|
||||
value: int = betterproto.uint64_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Int32Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `int32`. The JSON representation for `Int32Value` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The int32 value.
|
||||
value: int = betterproto.int32_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class UInt32Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `uint32`. The JSON representation for `UInt32Value` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The uint32 value.
|
||||
value: int = betterproto.uint32_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class BoolValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `bool`. The JSON representation for `BoolValue` is JSON
|
||||
`true` and `false`.
|
||||
"""
|
||||
|
||||
# The bool value.
|
||||
value: bool = betterproto.bool_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class StringValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `string`. The JSON representation for `StringValue` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The string value.
|
||||
value: str = betterproto.string_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class BytesValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `bytes`. The JSON representation for `BytesValue` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The bytes value.
|
||||
value: bytes = betterproto.bytes_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Empty(betterproto.Message):
|
||||
"""
|
||||
A generic empty message that you can re-use to avoid defining duplicated
|
||||
empty messages in your APIs. A typical example is to use it as the request
|
||||
or the response type of an API method. For instance: service Foo {
|
||||
rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } The
|
||||
JSON representation for `Empty` is empty JSON object `{}`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class FileDescriptorSet(betterproto.Message):
|
||||
"""
|
||||
@@ -855,6 +499,23 @@ class FieldDescriptorProto(betterproto.Message):
|
||||
# camelCase.
|
||||
json_name: str = betterproto.string_field(10)
|
||||
options: "FieldOptions" = betterproto.message_field(8)
|
||||
# If true, this is a proto3 "optional". When a proto3 field is optional, it
|
||||
# tracks presence regardless of field type. When proto3_optional is true,
|
||||
# this field must be belong to a oneof to signal to old proto3 clients that
|
||||
# presence is tracked for this field. This oneof is known as a "synthetic"
|
||||
# oneof, and this field must be its sole member (each proto3 optional field
|
||||
# gets its own synthetic oneof). Synthetic oneofs exist in the descriptor
|
||||
# only, and do not generate any API. Synthetic oneofs must be ordered after
|
||||
# all "real" oneofs. For message fields, proto3_optional doesn't create any
|
||||
# semantic change, since non-repeated message fields always track presence.
|
||||
# However it still indicates the semantic detail of whether the user wrote
|
||||
# "optional" or not. This can be useful for round-tripping the .proto file.
|
||||
# For consistency we give message fields a synthetic oneof also, even though
|
||||
# it is not required to track presence. This is especially important because
|
||||
# the parser can't tell if a field is a message or an enum, so it must always
|
||||
# create a synthetic oneof. Proto2 optional fields do not set this flag,
|
||||
# because they already indicate optional with `LABEL_OPTIONAL`.
|
||||
proto3_optional: bool = betterproto.bool_field(17)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
@@ -937,17 +598,18 @@ class FileOptions(betterproto.Message):
|
||||
# inappropriate because proto packages do not normally start with backwards
|
||||
# domain names.
|
||||
java_package: str = betterproto.string_field(1)
|
||||
# If set, all the classes from the .proto file are wrapped in a single outer
|
||||
# class with the given name. This applies to both Proto1 (equivalent to the
|
||||
# old "--one_java_file" option) and Proto2 (where a .proto always translates
|
||||
# to a single class, but you may want to explicitly choose the class name).
|
||||
# Controls the name of the wrapper Java class generated for the .proto file.
|
||||
# That class will always contain the .proto file's getDescriptor() method as
|
||||
# well as any top-level extensions defined in the .proto file. If
|
||||
# java_multiple_files is disabled, then all the other classes from the .proto
|
||||
# file will be nested inside the single wrapper outer class.
|
||||
java_outer_classname: str = betterproto.string_field(8)
|
||||
# If set true, then the Java code generator will generate a separate .java
|
||||
# If enabled, then the Java code generator will generate a separate .java
|
||||
# file for each top-level message, enum, and service defined in the .proto
|
||||
# file. Thus, these types will *not* be nested inside the outer class named
|
||||
# by java_outer_classname. However, the outer class will still be generated
|
||||
# to contain the file's getDescriptor() method as well as any top-level
|
||||
# extensions defined in the file.
|
||||
# file. Thus, these types will *not* be nested inside the wrapper class
|
||||
# named by java_outer_classname. However, the wrapper class will still be
|
||||
# generated to contain the file's getDescriptor() method as well as any top-
|
||||
# level extensions defined in the file.
|
||||
java_multiple_files: bool = betterproto.bool_field(10)
|
||||
# This option does nothing.
|
||||
java_generate_equals_and_hash: bool = betterproto.bool_field(20)
|
||||
@@ -1315,3 +977,363 @@ class GeneratedCodeInfoAnnotation(betterproto.Message):
|
||||
# the identified offset. The end offset should be one past the last relevant
|
||||
# byte (so the length of the text = end - begin).
|
||||
end: int = betterproto.int32_field(4)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Duration(betterproto.Message):
|
||||
"""
|
||||
A Duration represents a signed, fixed-length span of time represented as a
|
||||
count of seconds and fractions of seconds at nanosecond resolution. It is
|
||||
independent of any calendar and concepts like "day" or "month". It is
|
||||
related to Timestamp in that the difference between two Timestamp values is
|
||||
a Duration and it can be added or subtracted from a Timestamp. Range is
|
||||
approximately +-10,000 years. # Examples Example 1: Compute Duration from
|
||||
two Timestamps in pseudo code. Timestamp start = ...; Timestamp end
|
||||
= ...; Duration duration = ...; duration.seconds = end.seconds -
|
||||
start.seconds; duration.nanos = end.nanos - start.nanos; if
|
||||
(duration.seconds < 0 && duration.nanos > 0) { duration.seconds += 1;
|
||||
duration.nanos -= 1000000000; } else if (duration.seconds > 0 &&
|
||||
duration.nanos < 0) { duration.seconds -= 1; duration.nanos +=
|
||||
1000000000; } Example 2: Compute Timestamp from Timestamp + Duration in
|
||||
pseudo code. Timestamp start = ...; Duration duration = ...;
|
||||
Timestamp end = ...; end.seconds = start.seconds + duration.seconds;
|
||||
end.nanos = start.nanos + duration.nanos; if (end.nanos < 0) {
|
||||
end.seconds -= 1; end.nanos += 1000000000; } else if (end.nanos
|
||||
>= 1000000000) { end.seconds += 1; end.nanos -= 1000000000;
|
||||
} Example 3: Compute Duration from datetime.timedelta in Python. td =
|
||||
datetime.timedelta(days=3, minutes=10) duration = Duration()
|
||||
duration.FromTimedelta(td) # JSON Mapping In JSON format, the Duration type
|
||||
is encoded as a string rather than an object, where the string ends in the
|
||||
suffix "s" (indicating seconds) and is preceded by the number of seconds,
|
||||
with nanoseconds expressed as fractional seconds. For example, 3 seconds
|
||||
with 0 nanoseconds should be encoded in JSON format as "3s", while 3
|
||||
seconds and 1 nanosecond should be expressed in JSON format as
|
||||
"3.000000001s", and 3 seconds and 1 microsecond should be expressed in JSON
|
||||
format as "3.000001s".
|
||||
"""
|
||||
|
||||
# Signed seconds of the span of time. Must be from -315,576,000,000 to
|
||||
# +315,576,000,000 inclusive. Note: these bounds are computed from: 60
|
||||
# sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
|
||||
seconds: int = betterproto.int64_field(1)
|
||||
# Signed fractions of a second at nanosecond resolution of the span of time.
|
||||
# Durations less than one second are represented with a 0 `seconds` field and
|
||||
# a positive or negative `nanos` field. For durations of one second or more,
|
||||
# a non-zero value for the `nanos` field must be of the same sign as the
|
||||
# `seconds` field. Must be from -999,999,999 to +999,999,999 inclusive.
|
||||
nanos: int = betterproto.int32_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Empty(betterproto.Message):
|
||||
"""
|
||||
A generic empty message that you can re-use to avoid defining duplicated
|
||||
empty messages in your APIs. A typical example is to use it as the request
|
||||
or the response type of an API method. For instance: service Foo {
|
||||
rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } The
|
||||
JSON representation for `Empty` is empty JSON object `{}`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class FieldMask(betterproto.Message):
|
||||
"""
|
||||
`FieldMask` represents a set of symbolic field paths, for example:
|
||||
paths: "f.a" paths: "f.b.d" Here `f` represents a field in some root
|
||||
message, `a` and `b` fields in the message found in `f`, and `d` a field
|
||||
found in the message in `f.b`. Field masks are used to specify a subset of
|
||||
fields that should be returned by a get operation or modified by an update
|
||||
operation. Field masks also have a custom JSON encoding (see below). #
|
||||
Field Masks in Projections When used in the context of a projection, a
|
||||
response message or sub-message is filtered by the API to only contain
|
||||
those fields as specified in the mask. For example, if the mask in the
|
||||
previous example is applied to a response message as follows: f {
|
||||
a : 22 b { d : 1 x : 2 } y : 13 }
|
||||
z: 8 The result will not contain specific values for fields x,y and z
|
||||
(their value will be set to the default, and omitted in proto text output):
|
||||
f { a : 22 b { d : 1 } } A repeated field is
|
||||
not allowed except at the last position of a paths string. If a FieldMask
|
||||
object is not present in a get operation, the operation applies to all
|
||||
fields (as if a FieldMask of all fields had been specified). Note that a
|
||||
field mask does not necessarily apply to the top-level response message. In
|
||||
case of a REST get operation, the field mask applies directly to the
|
||||
response, but in case of a REST list operation, the mask instead applies to
|
||||
each individual message in the returned resource list. In case of a REST
|
||||
custom method, other definitions may be used. Where the mask applies will
|
||||
be clearly documented together with its declaration in the API. In any
|
||||
case, the effect on the returned resource/resources is required behavior
|
||||
for APIs. # Field Masks in Update Operations A field mask in update
|
||||
operations specifies which fields of the targeted resource are going to be
|
||||
updated. The API is required to only change the values of the fields as
|
||||
specified in the mask and leave the others untouched. If a resource is
|
||||
passed in to describe the updated values, the API ignores the values of all
|
||||
fields not covered by the mask. If a repeated field is specified for an
|
||||
update operation, new values will be appended to the existing repeated
|
||||
field in the target resource. Note that a repeated field is only allowed in
|
||||
the last position of a `paths` string. If a sub-message is specified in the
|
||||
last position of the field mask for an update operation, then new value
|
||||
will be merged into the existing sub-message in the target resource. For
|
||||
example, given the target message: f { b { d: 1
|
||||
x: 2 } c: [1] } And an update message: f { b {
|
||||
d: 10 } c: [2] } then if the field mask is: paths: ["f.b",
|
||||
"f.c"] then the result will be: f { b { d: 10 x:
|
||||
2 } c: [1, 2] } An implementation may provide options to
|
||||
override this default behavior for repeated and message fields. In order to
|
||||
reset a field's value to the default, the field must be in the mask and set
|
||||
to the default value in the provided resource. Hence, in order to reset all
|
||||
fields of a resource, provide a default instance of the resource and set
|
||||
all fields in the mask, or do not provide a mask as described below. If a
|
||||
field mask is not present on update, the operation applies to all fields
|
||||
(as if a field mask of all fields has been specified). Note that in the
|
||||
presence of schema evolution, this may mean that fields the client does not
|
||||
know and has therefore not filled into the request will be reset to their
|
||||
default. If this is unwanted behavior, a specific service may require a
|
||||
client to always specify a field mask, producing an error if not. As with
|
||||
get operations, the location of the resource which describes the updated
|
||||
values in the request message depends on the operation kind. In any case,
|
||||
the effect of the field mask is required to be honored by the API. ##
|
||||
Considerations for HTTP REST The HTTP kind of an update operation which
|
||||
uses a field mask must be set to PATCH instead of PUT in order to satisfy
|
||||
HTTP semantics (PUT must only be used for full updates). # JSON Encoding of
|
||||
Field Masks In JSON, a field mask is encoded as a single string where paths
|
||||
are separated by a comma. Fields name in each path are converted to/from
|
||||
lower-camel naming conventions. As an example, consider the following
|
||||
message declarations: message Profile { User user = 1;
|
||||
Photo photo = 2; } message User { string display_name = 1;
|
||||
string address = 2; } In proto a field mask for `Profile` may look as
|
||||
such: mask { paths: "user.display_name" paths: "photo"
|
||||
} In JSON, the same mask is represented as below: { mask:
|
||||
"user.displayName,photo" } # Field Masks and Oneof Fields Field masks
|
||||
treat fields in oneofs just as regular fields. Consider the following
|
||||
message: message SampleMessage { oneof test_oneof {
|
||||
string name = 4; SubMessage sub_message = 9; } } The
|
||||
field mask can be: mask { paths: "name" } Or: mask {
|
||||
paths: "sub_message" } Note that oneof type names ("test_oneof" in this
|
||||
case) cannot be used in paths. ## Field Mask Verification The
|
||||
implementation of any API method which has a FieldMask type field in the
|
||||
request should verify the included field paths, and return an
|
||||
`INVALID_ARGUMENT` error if any path is unmappable.
|
||||
"""
|
||||
|
||||
# The set of field mask paths.
|
||||
paths: List[str] = betterproto.string_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Struct(betterproto.Message):
|
||||
"""
|
||||
`Struct` represents a structured data value, consisting of fields which map
|
||||
to dynamically typed values. In some languages, `Struct` might be supported
|
||||
by a native representation. For example, in scripting languages like JS a
|
||||
struct is represented as an object. The details of that representation are
|
||||
described together with the proto support for the language. The JSON
|
||||
representation for `Struct` is JSON object.
|
||||
"""
|
||||
|
||||
# Unordered map of dynamically typed values.
|
||||
fields: Dict[str, "Value"] = betterproto.map_field(
|
||||
1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE
|
||||
)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Value(betterproto.Message):
|
||||
"""
|
||||
`Value` represents a dynamically typed value which can be either null, a
|
||||
number, a string, a boolean, a recursive struct value, or a list of values.
|
||||
A producer of value is expected to set one of that variants, absence of any
|
||||
variant indicates an error. The JSON representation for `Value` is JSON
|
||||
value.
|
||||
"""
|
||||
|
||||
# Represents a null value.
|
||||
null_value: "NullValue" = betterproto.enum_field(1, group="kind")
|
||||
# Represents a double value.
|
||||
number_value: float = betterproto.double_field(2, group="kind")
|
||||
# Represents a string value.
|
||||
string_value: str = betterproto.string_field(3, group="kind")
|
||||
# Represents a boolean value.
|
||||
bool_value: bool = betterproto.bool_field(4, group="kind")
|
||||
# Represents a structured value.
|
||||
struct_value: "Struct" = betterproto.message_field(5, group="kind")
|
||||
# Represents a repeated `Value`.
|
||||
list_value: "ListValue" = betterproto.message_field(6, group="kind")
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class ListValue(betterproto.Message):
|
||||
"""
|
||||
`ListValue` is a wrapper around a repeated field of values. The JSON
|
||||
representation for `ListValue` is JSON array.
|
||||
"""
|
||||
|
||||
# Repeated field of dynamically typed values.
|
||||
values: List["Value"] = betterproto.message_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Timestamp(betterproto.Message):
|
||||
"""
|
||||
A Timestamp represents a point in time independent of any time zone or
|
||||
local calendar, encoded as a count of seconds and fractions of seconds at
|
||||
nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
Gregorian calendar backwards to year one. All minutes are 60 seconds long.
|
||||
Leap seconds are "smeared" so that no leap second table is needed for
|
||||
interpretation, using a [24-hour linear
|
||||
smear](https://developers.google.com/time/smear). The range is from
|
||||
0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By restricting to
|
||||
that range, we ensure that we can convert to and from [RFC
|
||||
3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. # Examples
|
||||
Example 1: Compute Timestamp from POSIX `time()`. Timestamp timestamp;
|
||||
timestamp.set_seconds(time(NULL)); timestamp.set_nanos(0); Example 2:
|
||||
Compute Timestamp from POSIX `gettimeofday()`. struct timeval tv;
|
||||
gettimeofday(&tv, NULL); Timestamp timestamp;
|
||||
timestamp.set_seconds(tv.tv_sec); timestamp.set_nanos(tv.tv_usec *
|
||||
1000); Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
FILETIME ft; GetSystemTimeAsFileTime(&ft); UINT64 ticks =
|
||||
(((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; // A Windows
|
||||
tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z // is
|
||||
11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. Timestamp
|
||||
timestamp; timestamp.set_seconds((INT64) ((ticks / 10000000) -
|
||||
11644473600LL)); timestamp.set_nanos((INT32) ((ticks % 10000000) *
|
||||
100)); Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
long millis = System.currentTimeMillis(); Timestamp timestamp =
|
||||
Timestamp.newBuilder().setSeconds(millis / 1000) .setNanos((int)
|
||||
((millis % 1000) * 1000000)).build(); Example 5: Compute Timestamp from
|
||||
Java `Instant.now()`. Instant now = Instant.now(); Timestamp
|
||||
timestamp = Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||
.setNanos(now.getNano()).build(); Example 6: Compute Timestamp from current
|
||||
time in Python. timestamp = Timestamp() timestamp.GetCurrentTime()
|
||||
# JSON Mapping In JSON format, the Timestamp type is encoded as a string in
|
||||
the [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||
format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where
|
||||
{year} is always expressed using four digits while {month}, {day}, {hour},
|
||||
{min}, and {sec} are zero-padded to two digits each. The fractional
|
||||
seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||
are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||
is required. A proto3 JSON serializer should always use UTC (as indicated
|
||||
by "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||
able to accept both UTC and other timezones (as indicated by an offset).
|
||||
For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 01:30 UTC
|
||||
on January 15, 2017. In JavaScript, one can convert a Date object to this
|
||||
format using the standard [toISOString()](https://developer.mozilla.org/en-
|
||||
US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) method.
|
||||
In Python, a standard `datetime.datetime` object can be converted to this
|
||||
format using
|
||||
[`strftime`](https://docs.python.org/2/library/time.html#time.strftime)
|
||||
with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one
|
||||
can use the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
http://www.joda.org/joda-
|
||||
time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D )
|
||||
to obtain a formatter capable of generating timestamps in this format.
|
||||
"""
|
||||
|
||||
# Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must
|
||||
# be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
|
||||
seconds: int = betterproto.int64_field(1)
|
||||
# Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
# second values with fractions must still have non-negative nanos values that
|
||||
# count forward in time. Must be from 0 to 999,999,999 inclusive.
|
||||
nanos: int = betterproto.int32_field(2)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class DoubleValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `double`. The JSON representation for `DoubleValue` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The double value.
|
||||
value: float = betterproto.double_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class FloatValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `float`. The JSON representation for `FloatValue` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The float value.
|
||||
value: float = betterproto.float_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Int64Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `int64`. The JSON representation for `Int64Value` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The int64 value.
|
||||
value: int = betterproto.int64_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class UInt64Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `uint64`. The JSON representation for `UInt64Value` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The uint64 value.
|
||||
value: int = betterproto.uint64_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class Int32Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `int32`. The JSON representation for `Int32Value` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The int32 value.
|
||||
value: int = betterproto.int32_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class UInt32Value(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `uint32`. The JSON representation for `UInt32Value` is
|
||||
JSON number.
|
||||
"""
|
||||
|
||||
# The uint32 value.
|
||||
value: int = betterproto.uint32_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class BoolValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `bool`. The JSON representation for `BoolValue` is JSON
|
||||
`true` and `false`.
|
||||
"""
|
||||
|
||||
# The bool value.
|
||||
value: bool = betterproto.bool_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class StringValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `string`. The JSON representation for `StringValue` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The string value.
|
||||
value: str = betterproto.string_field(1)
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
class BytesValue(betterproto.Message):
|
||||
"""
|
||||
Wrapper message for `bytes`. The JSON representation for `BytesValue` is
|
||||
JSON string.
|
||||
"""
|
||||
|
||||
# The bytes value.
|
||||
value: bytes = betterproto.bytes_field(1)
|
||||
|
@@ -5,6 +5,12 @@ from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
import betterproto
|
||||
from betterproto.grpc.grpclib_server import ServiceBase
|
||||
|
||||
|
||||
class CodeGeneratorResponseFeature(betterproto.Enum):
|
||||
FEATURE_NONE = 0
|
||||
FEATURE_PROTO3_OPTIONAL = 1
|
||||
|
||||
|
||||
@dataclass(eq=False, repr=False)
|
||||
@@ -59,6 +65,9 @@ class CodeGeneratorResponse(betterproto.Message):
|
||||
# unparseable -- should be reported by writing a message to stderr and
|
||||
# exiting with a non-zero status code.
|
||||
error: str = betterproto.string_field(1)
|
||||
# A bitmask of supported features that the code generator supports. This is a
|
||||
# bitwise "or" of values from the Feature enum.
|
||||
supported_features: int = betterproto.uint64_field(2)
|
||||
file: List["CodeGeneratorResponseFile"] = betterproto.message_field(15)
|
||||
|
||||
|
||||
@@ -108,6 +117,12 @@ class CodeGeneratorResponseFile(betterproto.Message):
|
||||
insertion_point: str = betterproto.string_field(2)
|
||||
# The file contents.
|
||||
content: str = betterproto.string_field(15)
|
||||
# Information describing the file content being inserted. If an insertion
|
||||
# point is used, this information will be appropriately offset and inserted
|
||||
# into the code generation metadata for the generated files.
|
||||
generated_code_info: "betterproto_lib_google_protobuf.GeneratedCodeInfo" = (
|
||||
betterproto.message_field(16)
|
||||
)
|
||||
|
||||
|
||||
import betterproto.lib.google.protobuf as betterproto_lib_google_protobuf
|
||||
|
@@ -33,5 +33,5 @@ def outputfile_compiler(output_file: OutputTemplate) -> str:
|
||||
|
||||
return black.format_str(
|
||||
template.render(output_file=output_file),
|
||||
mode=black.FileMode(target_versions={black.TargetVersion.PY37}),
|
||||
mode=black.Mode(),
|
||||
)
|
||||
|
@@ -28,11 +28,8 @@ def main() -> None:
|
||||
if dump_file:
|
||||
dump_request(dump_file, request)
|
||||
|
||||
# Create response
|
||||
response = CodeGeneratorResponse()
|
||||
|
||||
# Generate code
|
||||
generate_code(request, response)
|
||||
response = generate_code(request)
|
||||
|
||||
# Serialise response message
|
||||
output = response.SerializeToString()
|
||||
|
@@ -30,13 +30,16 @@ reference to `A` to `B`'s `fields` attribute.
|
||||
"""
|
||||
|
||||
|
||||
import builtins
|
||||
import re
|
||||
import textwrap
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Set, Type, Union
|
||||
|
||||
import betterproto
|
||||
from betterproto import which_one_of
|
||||
from betterproto.casing import sanitize_name
|
||||
from betterproto.compile.importing import (
|
||||
get_type_reference,
|
||||
parse_source_type_name,
|
||||
)
|
||||
from betterproto.compile.importing import get_type_reference, parse_source_type_name
|
||||
from betterproto.compile.naming import (
|
||||
pythonize_class_name,
|
||||
pythonize_field_name,
|
||||
@@ -45,22 +48,15 @@ from betterproto.compile.naming import (
|
||||
from betterproto.lib.google.protobuf import (
|
||||
DescriptorProto,
|
||||
EnumDescriptorProto,
|
||||
FileDescriptorProto,
|
||||
MethodDescriptorProto,
|
||||
Field,
|
||||
FieldDescriptorProto,
|
||||
FieldDescriptorProtoType,
|
||||
FieldDescriptorProtoLabel,
|
||||
FieldDescriptorProtoType,
|
||||
FileDescriptorProto,
|
||||
MethodDescriptorProto,
|
||||
)
|
||||
from betterproto.lib.google.protobuf.compiler import CodeGeneratorRequest
|
||||
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Set, Text, Type, Union
|
||||
import sys
|
||||
|
||||
from ..casing import sanitize_name
|
||||
from ..compile.importing import get_type_reference, parse_source_type_name
|
||||
from ..compile.naming import (
|
||||
@@ -69,7 +65,6 @@ from ..compile.naming import (
|
||||
pythonize_method_name,
|
||||
)
|
||||
|
||||
|
||||
# Create a unique placeholder to deal with
|
||||
# https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses
|
||||
PLACEHOLDER = object()
|
||||
@@ -147,17 +142,13 @@ def get_comment(
|
||||
sci_loc.leading_comments.strip().replace("\n", ""), width=79 - indent
|
||||
)
|
||||
|
||||
if path[-2] == 2 and path[-4] != 6:
|
||||
# This is a field
|
||||
return f"{pad}# " + f"\n{pad}# ".join(lines)
|
||||
# This is a field, message, enum, service, or method
|
||||
if len(lines) == 1 and len(lines[0]) < 79 - indent - 6:
|
||||
lines[0] = lines[0].strip('"')
|
||||
return f'{pad}"""{lines[0]}"""'
|
||||
else:
|
||||
# This is a message, enum, service, or method
|
||||
if len(lines) == 1 and len(lines[0]) < 79 - indent - 6:
|
||||
lines[0] = lines[0].strip('"')
|
||||
return f'{pad}"""{lines[0]}"""'
|
||||
else:
|
||||
joined = f"\n{pad}".join(lines)
|
||||
return f'{pad}"""\n{pad}{joined}\n{pad}"""'
|
||||
joined = f"\n{pad}".join(lines)
|
||||
return f'{pad}"""\n{pad}{joined}\n{pad}"""'
|
||||
|
||||
return ""
|
||||
|
||||
@@ -237,6 +228,7 @@ class OutputTemplate:
|
||||
imports: Set[str] = field(default_factory=set)
|
||||
datetime_imports: Set[str] = field(default_factory=set)
|
||||
typing_imports: Set[str] = field(default_factory=set)
|
||||
builtins_import: bool = False
|
||||
messages: List["MessageCompiler"] = field(default_factory=list)
|
||||
enums: List["EnumDefinitionCompiler"] = field(default_factory=list)
|
||||
services: List["ServiceCompiler"] = field(default_factory=list)
|
||||
@@ -268,6 +260,8 @@ class OutputTemplate:
|
||||
imports = set()
|
||||
if any(x for x in self.messages if any(x.deprecated_fields)):
|
||||
imports.add("warnings")
|
||||
if self.builtins_import:
|
||||
imports.add("builtins")
|
||||
return imports
|
||||
|
||||
|
||||
@@ -283,6 +277,7 @@ class MessageCompiler(ProtoContentBase):
|
||||
default_factory=list
|
||||
)
|
||||
deprecated: bool = field(default=False, init=False)
|
||||
builtins_types: Set[str] = field(default_factory=set)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Add message to output file
|
||||
@@ -376,6 +371,8 @@ class FieldCompiler(MessageCompiler):
|
||||
betterproto_field_type = (
|
||||
f"betterproto.{self.field_type}_field({self.proto_obj.number}{field_args})"
|
||||
)
|
||||
if self.py_name in dir(builtins):
|
||||
self.parent.builtins_types.add(self.py_name)
|
||||
return f"{name}{annotations} = {betterproto_field_type}"
|
||||
|
||||
@property
|
||||
@@ -383,6 +380,8 @@ class FieldCompiler(MessageCompiler):
|
||||
args = []
|
||||
if self.field_wraps:
|
||||
args.append(f"wraps={self.field_wraps}")
|
||||
if self.optional:
|
||||
args.append(f"optional=True")
|
||||
return args
|
||||
|
||||
@property
|
||||
@@ -408,9 +407,16 @@ class FieldCompiler(MessageCompiler):
|
||||
imports.add("Dict")
|
||||
return imports
|
||||
|
||||
@property
|
||||
def use_builtins(self) -> bool:
|
||||
return self.py_type in self.parent.builtins_types or (
|
||||
self.py_type == self.py_name and self.py_name in dir(builtins)
|
||||
)
|
||||
|
||||
def add_imports_to(self, output_file: OutputTemplate) -> None:
|
||||
output_file.datetime_imports.update(self.datetime_imports)
|
||||
output_file.typing_imports.update(self.typing_imports)
|
||||
output_file.builtins_import = output_file.builtins_import or self.use_builtins
|
||||
|
||||
@property
|
||||
def field_wraps(self) -> Optional[str]:
|
||||
@@ -431,6 +437,10 @@ class FieldCompiler(MessageCompiler):
|
||||
and not is_map(self.proto_obj, self.parent)
|
||||
)
|
||||
|
||||
@property
|
||||
def optional(self) -> bool:
|
||||
return self.proto_obj.proto3_optional
|
||||
|
||||
@property
|
||||
def mutable(self) -> bool:
|
||||
"""True if the field is a mutable type, otherwise False."""
|
||||
@@ -446,10 +456,12 @@ class FieldCompiler(MessageCompiler):
|
||||
)
|
||||
|
||||
@property
|
||||
def default_value_string(self) -> Union[Text, None, float, int]:
|
||||
def default_value_string(self) -> str:
|
||||
"""Python representation of the default proto value."""
|
||||
if self.repeated:
|
||||
return "[]"
|
||||
if self.optional:
|
||||
return "None"
|
||||
if self.py_type == "int":
|
||||
return "0"
|
||||
if self.py_type == "float":
|
||||
@@ -460,6 +472,14 @@ class FieldCompiler(MessageCompiler):
|
||||
return '""'
|
||||
elif self.py_type == "bytes":
|
||||
return 'b""'
|
||||
elif self.field_type == "enum":
|
||||
enum_proto_obj_name = self.proto_obj.type_name.split(".").pop()
|
||||
enum = next(
|
||||
e
|
||||
for e in self.output_file.enums
|
||||
if e.proto_obj.name == enum_proto_obj_name
|
||||
)
|
||||
return enum.default_value_string
|
||||
else:
|
||||
# Message type
|
||||
return "None"
|
||||
@@ -500,13 +520,18 @@ class FieldCompiler(MessageCompiler):
|
||||
source_type=self.proto_obj.type_name,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown type {field.type}")
|
||||
raise NotImplementedError(f"Unknown type {self.proto_obj.type}")
|
||||
|
||||
@property
|
||||
def annotation(self) -> str:
|
||||
py_type = self.py_type
|
||||
if self.use_builtins:
|
||||
py_type = f"builtins.{py_type}"
|
||||
if self.repeated:
|
||||
return f"List[{self.py_type}]"
|
||||
return self.py_type
|
||||
return f"List[{py_type}]"
|
||||
if self.optional:
|
||||
return f"Optional[{py_type}]"
|
||||
return py_type
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -641,12 +666,8 @@ class ServiceMethodCompiler(ProtoContentBase):
|
||||
self.parent.methods.append(self)
|
||||
|
||||
# Check for imports
|
||||
if self.py_input_message:
|
||||
for f in self.py_input_message.fields:
|
||||
f.add_imports_to(self.output_file)
|
||||
if "Optional" in self.py_output_message_type:
|
||||
self.output_file.typing_imports.add("Optional")
|
||||
self.mutable_default_args # ensure this is called before rendering
|
||||
|
||||
# Check for Async imports
|
||||
if self.client_streaming:
|
||||
@@ -660,37 +681,6 @@ class ServiceMethodCompiler(ProtoContentBase):
|
||||
|
||||
super().__post_init__() # check for unset fields
|
||||
|
||||
@property
|
||||
def mutable_default_args(self) -> Dict[str, str]:
|
||||
"""Handle mutable default arguments.
|
||||
|
||||
Returns a list of tuples containing the name and default value
|
||||
for arguments to this message who's default value is mutable.
|
||||
The defaults are swapped out for None and replaced back inside
|
||||
the method's body.
|
||||
Reference:
|
||||
https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, str]
|
||||
Name and actual default value (as a string)
|
||||
for each argument with mutable default values.
|
||||
"""
|
||||
mutable_default_args = {}
|
||||
|
||||
if self.py_input_message:
|
||||
for f in self.py_input_message.fields:
|
||||
if (
|
||||
not self.client_streaming
|
||||
and f.default_value_string != "None"
|
||||
and f.mutable
|
||||
):
|
||||
mutable_default_args[f.py_name] = f.default_value_string
|
||||
self.output_file.typing_imports.add("Optional")
|
||||
|
||||
return mutable_default_args
|
||||
|
||||
@property
|
||||
def py_name(self) -> str:
|
||||
"""Pythonized method name."""
|
||||
@@ -748,6 +738,17 @@ class ServiceMethodCompiler(ProtoContentBase):
|
||||
source_type=self.proto_obj.input_type,
|
||||
).strip('"')
|
||||
|
||||
@property
|
||||
def py_input_message_param(self) -> str:
|
||||
"""Param name corresponding to py_input_message_type.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Param name corresponding to py_input_message_type.
|
||||
"""
|
||||
return pythonize_field_name(self.py_input_message_type)
|
||||
|
||||
@property
|
||||
def py_output_message_type(self) -> str:
|
||||
"""String representation of the Python type corresponding to the
|
||||
|
@@ -8,6 +8,7 @@ from betterproto.lib.google.protobuf import (
|
||||
from betterproto.lib.google.protobuf.compiler import (
|
||||
CodeGeneratorRequest,
|
||||
CodeGeneratorResponse,
|
||||
CodeGeneratorResponseFeature,
|
||||
CodeGeneratorResponseFile,
|
||||
)
|
||||
import itertools
|
||||
@@ -60,10 +61,11 @@ def traverse(
|
||||
)
|
||||
|
||||
|
||||
def generate_code(
|
||||
request: CodeGeneratorRequest, response: CodeGeneratorResponse
|
||||
) -> None:
|
||||
def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse:
|
||||
response = CodeGeneratorResponse()
|
||||
|
||||
plugin_options = request.parameter.split(",") if request.parameter else []
|
||||
response.supported_features = CodeGeneratorResponseFeature.FEATURE_PROTO3_OPTIONAL
|
||||
|
||||
request_data = PluginRequestCompiler(plugin_request_obj=request)
|
||||
# Gather output packages
|
||||
@@ -133,6 +135,8 @@ def generate_code(
|
||||
for output_package_name in sorted(output_paths.union(init_files)):
|
||||
print(f"Writing {output_package_name}", file=sys.stderr)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def read_protobuf_type(
|
||||
item: DescriptorProto,
|
||||
|
@@ -28,10 +28,11 @@ class {{ enum.py_name }}(betterproto.Enum):
|
||||
|
||||
{% endif %}
|
||||
{% for entry in enum.entries %}
|
||||
{{ entry.name }} = {{ entry.value }}
|
||||
{% if entry.comment %}
|
||||
{{ entry.comment }}
|
||||
|
||||
{% endif %}
|
||||
{{ entry.name }} = {{ entry.value }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
@@ -45,10 +46,11 @@ class {{ message.py_name }}(betterproto.Message):
|
||||
|
||||
{% endif %}
|
||||
{% for field in message.fields %}
|
||||
{{ field.get_field_string() }}
|
||||
{% if field.comment %}
|
||||
{{ field.comment }}
|
||||
|
||||
{% endif %}
|
||||
{{ field.get_field_string() }}
|
||||
{% endfor %}
|
||||
{% if not message.fields %}
|
||||
pass
|
||||
@@ -79,51 +81,21 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub):
|
||||
{% for method in service.methods %}
|
||||
async def {{ method.py_name }}(self
|
||||
{%- if not method.client_streaming -%}
|
||||
{%- if method.py_input_message and method.py_input_message.fields -%}, *,
|
||||
{%- for field in method.py_input_message.fields -%}
|
||||
{{ field.py_name }}: {% if field.py_name in method.mutable_default_args and not field.annotation.startswith("Optional[") -%}
|
||||
Optional[{{ field.annotation }}]
|
||||
{%- else -%}
|
||||
{{ field.annotation }}
|
||||
{%- endif -%} =
|
||||
{%- if field.py_name not in method.mutable_default_args -%}
|
||||
{{ field.default_value_string }}
|
||||
{%- else -%}
|
||||
None
|
||||
{% endif -%}
|
||||
{%- if not loop.last %}, {% endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%}
|
||||
{%- else -%}
|
||||
{# Client streaming: need a request iterator instead #}
|
||||
, request_iterator: Union[AsyncIterable["{{ method.py_input_message_type }}"], Iterable["{{ method.py_input_message_type }}"]]
|
||||
, {{ method.py_input_message_param }}_iterator: Union[AsyncIterable["{{ method.py_input_message_type }}"], Iterable["{{ method.py_input_message_type }}"]]
|
||||
{%- endif -%}
|
||||
) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}:
|
||||
{% if method.comment %}
|
||||
{{ method.comment }}
|
||||
|
||||
{% endif %}
|
||||
{%- for py_name, zero in method.mutable_default_args.items() %}
|
||||
{{ py_name }} = {{ py_name }} or {{ zero }}
|
||||
{% endfor %}
|
||||
|
||||
{% if not method.client_streaming %}
|
||||
request = {{ method.py_input_message_type }}()
|
||||
{% for field in method.py_input_message.fields %}
|
||||
{% if field.field_type == 'message' %}
|
||||
if {{ field.py_name }} is not None:
|
||||
request.{{ field.py_name }} = {{ field.py_name }}
|
||||
{% else %}
|
||||
request.{{ field.py_name }} = {{ field.py_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if method.server_streaming %}
|
||||
{% if method.client_streaming %}
|
||||
async for response in self._stream_stream(
|
||||
"{{ method.route }}",
|
||||
request_iterator,
|
||||
{{ method.py_input_message_param }}_iterator,
|
||||
{{ method.py_input_message_type }},
|
||||
{{ method.py_output_message_type.strip('"') }},
|
||||
):
|
||||
@@ -131,7 +103,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub):
|
||||
{% else %}{# i.e. not client streaming #}
|
||||
async for response in self._unary_stream(
|
||||
"{{ method.route }}",
|
||||
request,
|
||||
{{ method.py_input_message_param }},
|
||||
{{ method.py_output_message_type.strip('"') }},
|
||||
):
|
||||
yield response
|
||||
@@ -141,14 +113,14 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub):
|
||||
{% if method.client_streaming %}
|
||||
return await self._stream_unary(
|
||||
"{{ method.route }}",
|
||||
request_iterator,
|
||||
{{ method.py_input_message_param }}_iterator,
|
||||
{{ method.py_input_message_type }},
|
||||
{{ method.py_output_message_type.strip('"') }}
|
||||
)
|
||||
{% else %}{# i.e. not client streaming #}
|
||||
return await self._unary_unary(
|
||||
"{{ method.route }}",
|
||||
request,
|
||||
{{ method.py_input_message_param }},
|
||||
{{ method.py_output_message_type.strip('"') }}
|
||||
)
|
||||
{% endif %}{# client streaming #}
|
||||
@@ -167,19 +139,10 @@ class {{ service.py_name }}Base(ServiceBase):
|
||||
{% for method in service.methods %}
|
||||
async def {{ method.py_name }}(self
|
||||
{%- if not method.client_streaming -%}
|
||||
{%- if method.py_input_message and method.py_input_message.fields -%},
|
||||
{%- for field in method.py_input_message.fields -%}
|
||||
{{ field.py_name }}: {% if field.py_name in method.mutable_default_args and not field.annotation.startswith("Optional[") -%}
|
||||
Optional[{{ field.annotation }}]
|
||||
{%- else -%}
|
||||
{{ field.annotation }}
|
||||
{%- endif -%}
|
||||
{%- if not loop.last %}, {% endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%}
|
||||
{%- else -%}
|
||||
{# Client streaming: need a request iterator instead #}
|
||||
, request_iterator: AsyncIterator["{{ method.py_input_message_type }}"]
|
||||
, {{ method.py_input_message_param }}_iterator: AsyncIterator["{{ method.py_input_message_type }}"]
|
||||
{%- endif -%}
|
||||
) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}:
|
||||
{% if method.comment %}
|
||||
@@ -194,25 +157,17 @@ class {{ service.py_name }}Base(ServiceBase):
|
||||
async def __rpc_{{ method.py_name }}(self, stream: grpclib.server.Stream) -> None:
|
||||
{% if not method.client_streaming %}
|
||||
request = await stream.recv_message()
|
||||
|
||||
request_kwargs = {
|
||||
{% for field in method.py_input_message.fields %}
|
||||
"{{ field.py_name }}": request.{{ field.py_name }},
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{% else %}
|
||||
request_kwargs = {"request_iterator": stream.__aiter__()}
|
||||
request = stream.__aiter__()
|
||||
{% endif %}
|
||||
|
||||
{% if not method.server_streaming %}
|
||||
response = await self.{{ method.py_name }}(**request_kwargs)
|
||||
response = await self.{{ method.py_name }}(request)
|
||||
await stream.send_message(response)
|
||||
{% else %}
|
||||
await self._call_rpc_handler_server_stream(
|
||||
self.{{ method.py_name }},
|
||||
stream,
|
||||
request_kwargs,
|
||||
request,
|
||||
)
|
||||
{% endif %}
|
||||
|
||||
|
@@ -60,13 +60,15 @@ async def generate(whitelist: Set[str], verbose: bool):
|
||||
if result != 0:
|
||||
failed_test_cases.append(test_case_name)
|
||||
|
||||
if failed_test_cases:
|
||||
if len(failed_test_cases) > 0:
|
||||
sys.stderr.write(
|
||||
"\n\033[31;1;4mFailed to generate the following test cases:\033[0m\n"
|
||||
)
|
||||
for failed_test_case in failed_test_cases:
|
||||
sys.stderr.write(f"- {failed_test_case}\n")
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
async def generate_test_case_output(
|
||||
test_case_input_path: Path, test_case_name: str, verbose: bool
|
||||
@@ -92,21 +94,41 @@ async def generate_test_case_output(
|
||||
protoc(test_case_input_path, test_case_output_path_betterproto, False),
|
||||
)
|
||||
|
||||
message = f"Generated output for {test_case_name!r}"
|
||||
if verbose:
|
||||
print(f"\033[31;1;4m{message}\033[0m")
|
||||
if ref_out:
|
||||
sys.stdout.buffer.write(ref_out)
|
||||
if ref_err:
|
||||
sys.stderr.buffer.write(ref_err)
|
||||
if plg_out:
|
||||
sys.stdout.buffer.write(plg_out)
|
||||
if plg_err:
|
||||
sys.stderr.buffer.write(plg_err)
|
||||
sys.stdout.buffer.flush()
|
||||
sys.stderr.buffer.flush()
|
||||
if ref_code == 0:
|
||||
print(f"\033[31;1;4mGenerated reference output for {test_case_name!r}\033[0m")
|
||||
else:
|
||||
print(message)
|
||||
print(
|
||||
f"\033[31;1;4mFailed to generate reference output for {test_case_name!r}\033[0m"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
if ref_out:
|
||||
print("Reference stdout:")
|
||||
sys.stdout.buffer.write(ref_out)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
if ref_err:
|
||||
print("Reference stderr:")
|
||||
sys.stderr.buffer.write(ref_err)
|
||||
sys.stderr.buffer.flush()
|
||||
|
||||
if plg_code == 0:
|
||||
print(f"\033[31;1;4mGenerated plugin output for {test_case_name!r}\033[0m")
|
||||
else:
|
||||
print(
|
||||
f"\033[31;1;4mFailed to generate plugin output for {test_case_name!r}\033[0m"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
if plg_out:
|
||||
print("Plugin stdout:")
|
||||
sys.stdout.buffer.write(plg_out)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
if plg_err:
|
||||
print("Plugin stderr:")
|
||||
sys.stderr.buffer.write(plg_err)
|
||||
sys.stderr.buffer.flush()
|
||||
|
||||
return max(ref_code, plg_code)
|
||||
|
||||
|
@@ -1,23 +1,24 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
import grpclib
|
||||
import grpclib.metadata
|
||||
import grpclib.server
|
||||
import pytest
|
||||
from betterproto.grpc.util.async_channel import AsyncChannel
|
||||
from grpclib.testing import ChannelFor
|
||||
from tests.output_betterproto.service.service import (
|
||||
DoThingRequest,
|
||||
DoThingResponse,
|
||||
GetThingRequest,
|
||||
TestStub as ThingServiceClient,
|
||||
)
|
||||
import grpclib
|
||||
import grpclib.metadata
|
||||
import grpclib.server
|
||||
from grpclib.testing import ChannelFor
|
||||
import pytest
|
||||
from betterproto.grpc.util.async_channel import AsyncChannel
|
||||
from tests.output_betterproto.service.service import TestStub as ThingServiceClient
|
||||
|
||||
from .thing_service import ThingService
|
||||
|
||||
|
||||
async def _test_client(client, name="clean room", **kwargs):
|
||||
response = await client.do_thing(name=name)
|
||||
async def _test_client(client: ThingServiceClient, name="clean room", **kwargs):
|
||||
response = await client.do_thing(DoThingRequest(name=name))
|
||||
assert response.names == [name]
|
||||
|
||||
|
||||
@@ -62,7 +63,7 @@ async def test_trailer_only_error_unary_unary(
|
||||
)
|
||||
async with ChannelFor([service]) as channel:
|
||||
with pytest.raises(grpclib.exceptions.GRPCError) as e:
|
||||
await ThingServiceClient(channel).do_thing(name="something")
|
||||
await ThingServiceClient(channel).do_thing(DoThingRequest(name="something"))
|
||||
assert e.value.status == grpclib.Status.UNAUTHENTICATED
|
||||
|
||||
|
||||
@@ -80,7 +81,7 @@ async def test_trailer_only_error_stream_unary(
|
||||
async with ChannelFor([service]) as channel:
|
||||
with pytest.raises(grpclib.exceptions.GRPCError) as e:
|
||||
await ThingServiceClient(channel).do_many_things(
|
||||
request_iterator=[DoThingRequest(name="something")]
|
||||
do_thing_request_iterator=[DoThingRequest(name="something")]
|
||||
)
|
||||
await _test_client(ThingServiceClient(channel))
|
||||
assert e.value.status == grpclib.Status.UNAUTHENTICATED
|
||||
@@ -178,7 +179,9 @@ async def test_async_gen_for_unary_stream_request():
|
||||
async with ChannelFor([ThingService()]) as channel:
|
||||
client = ThingServiceClient(channel)
|
||||
expected_versions = [5, 4, 3, 2, 1]
|
||||
async for response in client.get_thing_versions(name=thing_name):
|
||||
async for response in client.get_thing_versions(
|
||||
GetThingRequest(name=thing_name)
|
||||
):
|
||||
assert response.name == thing_name
|
||||
assert response.version == expected_versions.pop()
|
||||
|
||||
|
@@ -1,49 +1,48 @@
|
||||
from typing import AsyncIterator, AsyncIterable
|
||||
from typing import AsyncIterable, AsyncIterator
|
||||
|
||||
import pytest
|
||||
from grpclib.testing import ChannelFor
|
||||
|
||||
from tests.output_betterproto.example_service.example_service import (
|
||||
TestBase,
|
||||
TestStub,
|
||||
ExampleRequest,
|
||||
ExampleResponse,
|
||||
TestBase,
|
||||
TestStub,
|
||||
)
|
||||
|
||||
|
||||
class ExampleService(TestBase):
|
||||
async def example_unary_unary(
|
||||
self, example_string: str, example_integer: int
|
||||
self, example_request: ExampleRequest
|
||||
) -> "ExampleResponse":
|
||||
return ExampleResponse(
|
||||
example_string=example_string,
|
||||
example_integer=example_integer,
|
||||
example_string=example_request.example_string,
|
||||
example_integer=example_request.example_integer,
|
||||
)
|
||||
|
||||
async def example_unary_stream(
|
||||
self, example_string: str, example_integer: int
|
||||
self, example_request: ExampleRequest
|
||||
) -> AsyncIterator["ExampleResponse"]:
|
||||
response = ExampleResponse(
|
||||
example_string=example_string,
|
||||
example_integer=example_integer,
|
||||
example_string=example_request.example_string,
|
||||
example_integer=example_request.example_integer,
|
||||
)
|
||||
yield response
|
||||
yield response
|
||||
yield response
|
||||
|
||||
async def example_stream_unary(
|
||||
self, request_iterator: AsyncIterator["ExampleRequest"]
|
||||
self, example_request_iterator: AsyncIterator["ExampleRequest"]
|
||||
) -> "ExampleResponse":
|
||||
async for example_request in request_iterator:
|
||||
async for example_request in example_request_iterator:
|
||||
return ExampleResponse(
|
||||
example_string=example_request.example_string,
|
||||
example_integer=example_request.example_integer,
|
||||
)
|
||||
|
||||
async def example_stream_stream(
|
||||
self, request_iterator: AsyncIterator["ExampleRequest"]
|
||||
self, example_request_iterator: AsyncIterator["ExampleRequest"]
|
||||
) -> AsyncIterator["ExampleResponse"]:
|
||||
async for example_request in request_iterator:
|
||||
async for example_request in example_request_iterator:
|
||||
yield ExampleResponse(
|
||||
example_string=example_request.example_string,
|
||||
example_integer=example_request.example_integer,
|
||||
@@ -52,44 +51,32 @@ class ExampleService(TestBase):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calls_with_different_cardinalities():
|
||||
test_string = "test string"
|
||||
test_int = 42
|
||||
example_request = ExampleRequest("test string", 42)
|
||||
|
||||
async with ChannelFor([ExampleService()]) as channel:
|
||||
stub = TestStub(channel)
|
||||
|
||||
# unary unary
|
||||
response = await stub.example_unary_unary(
|
||||
example_string="test string",
|
||||
example_integer=42,
|
||||
)
|
||||
assert response.example_string == test_string
|
||||
assert response.example_integer == test_int
|
||||
response = await stub.example_unary_unary(example_request)
|
||||
assert response.example_string == example_request.example_string
|
||||
assert response.example_integer == example_request.example_integer
|
||||
|
||||
# unary stream
|
||||
async for response in stub.example_unary_stream(
|
||||
example_string="test string",
|
||||
example_integer=42,
|
||||
):
|
||||
assert response.example_string == test_string
|
||||
assert response.example_integer == test_int
|
||||
async for response in stub.example_unary_stream(example_request):
|
||||
assert response.example_string == example_request.example_string
|
||||
assert response.example_integer == example_request.example_integer
|
||||
|
||||
# stream unary
|
||||
request = ExampleRequest(
|
||||
example_string=test_string,
|
||||
example_integer=42,
|
||||
)
|
||||
|
||||
async def request_iterator():
|
||||
yield request
|
||||
yield request
|
||||
yield request
|
||||
yield example_request
|
||||
yield example_request
|
||||
yield example_request
|
||||
|
||||
response = await stub.example_stream_unary(request_iterator())
|
||||
assert response.example_string == test_string
|
||||
assert response.example_integer == test_int
|
||||
assert response.example_string == example_request.example_string
|
||||
assert response.example_integer == example_request.example_integer
|
||||
|
||||
# stream stream
|
||||
async for response in stub.example_stream_stream(request_iterator()):
|
||||
assert response.example_string == test_string
|
||||
assert response.example_integer == test_int
|
||||
assert response.example_string == example_request.example_string
|
||||
assert response.example_integer == example_request.example_integer
|
||||
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"int": 26,
|
||||
"float": 26.0,
|
||||
"str": "value-for-str",
|
||||
"bytes": "001a",
|
||||
"bool": true
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// Tests that messages may contain fields with names that are identical to their python types (PR #294)
|
||||
|
||||
message Test {
|
||||
int32 int = 1;
|
||||
float float = 2;
|
||||
string str = 3;
|
||||
bytes bytes = 4;
|
||||
bool bool = 5;
|
||||
}
|
@@ -2,9 +2,8 @@ from typing import Any, Callable, Optional
|
||||
|
||||
import betterproto.lib.google.protobuf as protobuf
|
||||
import pytest
|
||||
|
||||
from tests.mocks import MockChannel
|
||||
from tests.output_betterproto.googletypes_response import TestStub
|
||||
from tests.output_betterproto.googletypes_response import Input, TestStub
|
||||
|
||||
test_cases = [
|
||||
(TestStub.get_double, protobuf.DoubleValue, 2.5),
|
||||
@@ -22,14 +21,15 @@ test_cases = [
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases)
|
||||
async def test_channel_receives_wrapped_type(
|
||||
service_method: Callable[[TestStub], Any], wrapper_class: Callable, value
|
||||
service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value
|
||||
):
|
||||
wrapped_value = wrapper_class()
|
||||
wrapped_value.value = value
|
||||
channel = MockChannel(responses=[wrapped_value])
|
||||
service = TestStub(channel)
|
||||
method_param = Input()
|
||||
|
||||
await service_method(service)
|
||||
await service_method(service, method_param)
|
||||
|
||||
assert channel.requests[0]["response_type"] != Optional[type(value)]
|
||||
assert channel.requests[0]["response_type"] == type(wrapped_value)
|
||||
@@ -39,7 +39,7 @@ async def test_channel_receives_wrapped_type(
|
||||
@pytest.mark.xfail
|
||||
@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases)
|
||||
async def test_service_unwraps_response(
|
||||
service_method: Callable[[TestStub], Any], wrapper_class: Callable, value
|
||||
service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value
|
||||
):
|
||||
"""
|
||||
grpclib does not unwrap wrapper values returned by services
|
||||
@@ -47,8 +47,9 @@ async def test_service_unwraps_response(
|
||||
wrapped_value = wrapper_class()
|
||||
wrapped_value.value = value
|
||||
service = TestStub(MockChannel(responses=[wrapped_value]))
|
||||
method_param = Input()
|
||||
|
||||
response_value = await service_method(service)
|
||||
response_value = await service_method(service, method_param)
|
||||
|
||||
assert response_value == value
|
||||
assert type(response_value) == type(value)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from tests.mocks import MockChannel
|
||||
from tests.output_betterproto.googletypes_response_embedded import (
|
||||
Input,
|
||||
Output,
|
||||
TestStub,
|
||||
)
|
||||
@@ -26,7 +26,7 @@ async def test_service_passes_through_unwrapped_values_embedded_in_response():
|
||||
)
|
||||
|
||||
service = TestStub(MockChannel(responses=[output]))
|
||||
response = await service.get_output()
|
||||
response = await service.get_output(Input())
|
||||
|
||||
assert response.double_value == 10.0
|
||||
assert response.float_value == 12.0
|
||||
|
@@ -1,17 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from tests.mocks import MockChannel
|
||||
from tests.output_betterproto.import_service_input_message import (
|
||||
NestedRequestMessage,
|
||||
RequestMessage,
|
||||
RequestResponse,
|
||||
TestStub,
|
||||
)
|
||||
from tests.output_betterproto.import_service_input_message.child import (
|
||||
ChildRequestMessage,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_correctly_imports_reference_message():
|
||||
mock_response = RequestResponse(value=10)
|
||||
service = TestStub(MockChannel([mock_response]))
|
||||
response = await service.do_thing(argument=1)
|
||||
response = await service.do_thing(RequestMessage(1))
|
||||
assert mock_response == response
|
||||
|
||||
|
||||
@@ -19,7 +23,7 @@ async def test_service_correctly_imports_reference_message():
|
||||
async def test_service_correctly_imports_reference_message_from_child_package():
|
||||
mock_response = RequestResponse(value=10)
|
||||
service = TestStub(MockChannel([mock_response]))
|
||||
response = await service.do_thing2(child_argument=1)
|
||||
response = await service.do_thing2(ChildRequestMessage(1))
|
||||
assert mock_response == response
|
||||
|
||||
|
||||
@@ -27,5 +31,5 @@ async def test_service_correctly_imports_reference_message_from_child_package():
|
||||
async def test_service_correctly_imports_nested_reference():
|
||||
mock_response = RequestResponse(value=10)
|
||||
service = TestStub(MockChannel([mock_response]))
|
||||
response = await service.do_thing3(nested_argument=1)
|
||||
response = await service.do_thing3(NestedRequestMessage(1))
|
||||
assert mock_response == response
|
||||
|
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"test1": 128,
|
||||
"test2": true,
|
||||
"test3": "A value",
|
||||
"test4": "aGVsbG8=",
|
||||
"test5": {
|
||||
"test": "Hello"
|
||||
},
|
||||
"test6": "B",
|
||||
"test7": "8589934592",
|
||||
"test8": 2.5
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message InnerTest {
|
||||
string test = 1;
|
||||
}
|
||||
|
||||
message Test {
|
||||
optional uint32 test1 = 1;
|
||||
optional bool test2 = 2;
|
||||
optional string test3 = 3;
|
||||
optional bytes test4 = 4;
|
||||
optional InnerTest test5 = 5;
|
||||
optional TestEnum test6 = 6;
|
||||
optional uint64 test7 = 7;
|
||||
optional float test8 = 8;
|
||||
}
|
||||
|
||||
enum TestEnum {
|
||||
A = 0;
|
||||
B = 1;
|
||||
}
|
@@ -0,0 +1 @@
|
||||
{}
|
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"test1": 0,
|
||||
"test2": false,
|
||||
"test3": "",
|
||||
"test4": "",
|
||||
"test6": "A",
|
||||
"test7": "0",
|
||||
"test8": 0
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
import json
|
||||
|
||||
from tests.output_betterproto.proto3_field_presence import Test, InnerTest, TestEnum
|
||||
|
||||
|
||||
def test_null_fields_json():
|
||||
"""Ensure that using "null" in JSON is equivalent to not specifying a
|
||||
field, for fields with explicit presence"""
|
||||
|
||||
def test_json(ref_json: str, obj_json: str) -> None:
|
||||
"""`ref_json` and `obj_json` are JSON strings describing a `Test` object.
|
||||
Test that deserializing both leads to the same object, and that
|
||||
`ref_json` is the normalized format."""
|
||||
ref_obj = Test().from_json(ref_json)
|
||||
obj = Test().from_json(obj_json)
|
||||
|
||||
assert obj == ref_obj
|
||||
assert json.loads(obj.to_json(0)) == json.loads(ref_json)
|
||||
|
||||
test_json("{}", '{ "test1": null, "test2": null, "test3": null }')
|
||||
test_json("{}", '{ "test4": null, "test5": null, "test6": null }')
|
||||
test_json("{}", '{ "test7": null, "test8": null }')
|
||||
test_json('{ "test5": {} }', '{ "test3": null, "test5": {} }')
|
||||
|
||||
# Make sure that if include_default_values is set, None values are
|
||||
# exported.
|
||||
obj = Test()
|
||||
assert obj.to_dict() == {}
|
||||
assert obj.to_dict(include_default_values=True) == {
|
||||
"test1": None,
|
||||
"test2": None,
|
||||
"test3": None,
|
||||
"test4": None,
|
||||
"test5": None,
|
||||
"test6": None,
|
||||
"test7": None,
|
||||
"test8": None,
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"nested": {}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Test {
|
||||
oneof kind {
|
||||
Nested nested = 1;
|
||||
WithOptional with_optional = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message InnerNested {
|
||||
optional bool a = 1;
|
||||
}
|
||||
|
||||
message Nested {
|
||||
InnerNested inner = 1;
|
||||
}
|
||||
|
||||
message WithOptional {
|
||||
optional bool b = 2;
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
from tests.output_betterproto.proto3_field_presence_oneof import (
|
||||
Test,
|
||||
InnerNested,
|
||||
Nested,
|
||||
WithOptional,
|
||||
)
|
||||
|
||||
|
||||
def test_serialization():
|
||||
"""Ensure that serialization of fields unset but with explicit field
|
||||
presence do not bloat the serialized payload with length-delimited fields
|
||||
with length 0"""
|
||||
|
||||
def test_empty_nested(message: Test) -> None:
|
||||
# '0a' => tag 1, length delimited
|
||||
# '00' => length: 0
|
||||
assert bytes(message) == bytearray.fromhex("0a 00")
|
||||
|
||||
test_empty_nested(Test(nested=Nested()))
|
||||
test_empty_nested(Test(nested=Nested(inner=None)))
|
||||
test_empty_nested(Test(nested=Nested(inner=InnerNested(a=None))))
|
||||
|
||||
def test_empty_with_optional(message: Test) -> None:
|
||||
# '12' => tag 2, length delimited
|
||||
# '00' => length: 0
|
||||
assert bytes(message) == bytearray.fromhex("12 00")
|
||||
|
||||
test_empty_with_optional(Test(with_optional=WithOptional()))
|
||||
test_empty_with_optional(Test(with_optional=WithOptional(b=None)))
|
@@ -2,9 +2,16 @@ syntax = "proto3";
|
||||
|
||||
package service;
|
||||
|
||||
enum ThingType {
|
||||
UNKNOWN = 0;
|
||||
LIVING = 1;
|
||||
DEAD = 2;
|
||||
}
|
||||
|
||||
message DoThingRequest {
|
||||
string name = 1;
|
||||
repeated string comments = 2;
|
||||
ThingType type = 3;
|
||||
}
|
||||
|
||||
message DoThingResponse {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import betterproto
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from inspect import Parameter, signature
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import betterproto
|
||||
|
||||
|
||||
def test_has_field():
|
||||
@@ -348,10 +350,8 @@ def test_recursive_message():
|
||||
|
||||
|
||||
def test_recursive_message_defaults():
|
||||
from tests.output_betterproto.recursivemessage import (
|
||||
Test as RecursiveMessage,
|
||||
Intermediate,
|
||||
)
|
||||
from tests.output_betterproto.recursivemessage import Intermediate
|
||||
from tests.output_betterproto.recursivemessage import Test as RecursiveMessage
|
||||
|
||||
msg = RecursiveMessage(name="bob", intermediate=Intermediate(42))
|
||||
|
||||
@@ -476,3 +476,12 @@ def test_iso_datetime_list():
|
||||
|
||||
msg.from_dict({"timestamps": iso_candidates})
|
||||
assert all([isinstance(item, datetime) for item in msg.timestamps])
|
||||
|
||||
|
||||
def test_service_argument__expected_parameter():
|
||||
from tests.output_betterproto.service.service import TestStub
|
||||
|
||||
sig = signature(TestStub.do_thing)
|
||||
do_thing_request_parameter = sig.parameters["do_thing_request"]
|
||||
assert do_thing_request_parameter.default is Parameter.empty
|
||||
assert do_thing_request_parameter.annotation == "DoThingRequest"
|
||||
|
Reference in New Issue
Block a user