aristaproto 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. aristaproto-0.1.3/LICENSE.md +23 -0
  2. aristaproto-0.1.3/PKG-INFO +493 -0
  3. aristaproto-0.1.3/README.md +463 -0
  4. aristaproto-0.1.3/pyproject.toml +159 -0
  5. aristaproto-0.1.3/src/aristaproto/__init__.py +2038 -0
  6. aristaproto-0.1.3/src/aristaproto/_types.py +14 -0
  7. aristaproto-0.1.3/src/aristaproto/_version.py +4 -0
  8. aristaproto-0.1.3/src/aristaproto/casing.py +143 -0
  9. aristaproto-0.1.3/src/aristaproto/compile/__init__.py +0 -0
  10. aristaproto-0.1.3/src/aristaproto/compile/importing.py +176 -0
  11. aristaproto-0.1.3/src/aristaproto/compile/naming.py +21 -0
  12. aristaproto-0.1.3/src/aristaproto/enum.py +195 -0
  13. aristaproto-0.1.3/src/aristaproto/grpc/__init__.py +0 -0
  14. aristaproto-0.1.3/src/aristaproto/grpc/grpclib_client.py +177 -0
  15. aristaproto-0.1.3/src/aristaproto/grpc/grpclib_server.py +33 -0
  16. aristaproto-0.1.3/src/aristaproto/grpc/util/__init__.py +0 -0
  17. aristaproto-0.1.3/src/aristaproto/grpc/util/async_channel.py +193 -0
  18. aristaproto-0.1.3/src/aristaproto/lib/__init__.py +0 -0
  19. aristaproto-0.1.3/src/aristaproto/lib/google/__init__.py +0 -0
  20. aristaproto-0.1.3/src/aristaproto/lib/google/protobuf/__init__.py +1 -0
  21. aristaproto-0.1.3/src/aristaproto/lib/google/protobuf/compiler/__init__.py +1 -0
  22. aristaproto-0.1.3/src/aristaproto/lib/pydantic/__init__.py +0 -0
  23. aristaproto-0.1.3/src/aristaproto/lib/pydantic/google/__init__.py +0 -0
  24. aristaproto-0.1.3/src/aristaproto/lib/pydantic/google/protobuf/__init__.py +2589 -0
  25. aristaproto-0.1.3/src/aristaproto/lib/pydantic/google/protobuf/compiler/__init__.py +210 -0
  26. aristaproto-0.1.3/src/aristaproto/lib/std/__init__.py +0 -0
  27. aristaproto-0.1.3/src/aristaproto/lib/std/google/__init__.py +0 -0
  28. aristaproto-0.1.3/src/aristaproto/lib/std/google/protobuf/__init__.py +2526 -0
  29. aristaproto-0.1.3/src/aristaproto/lib/std/google/protobuf/compiler/__init__.py +198 -0
  30. aristaproto-0.1.3/src/aristaproto/plugin/__init__.py +1 -0
  31. aristaproto-0.1.3/src/aristaproto/plugin/__main__.py +4 -0
  32. aristaproto-0.1.3/src/aristaproto/plugin/compiler.py +50 -0
  33. aristaproto-0.1.3/src/aristaproto/plugin/main.py +52 -0
  34. aristaproto-0.1.3/src/aristaproto/plugin/models.py +856 -0
  35. aristaproto-0.1.3/src/aristaproto/plugin/parser.py +221 -0
  36. aristaproto-0.1.3/src/aristaproto/plugin/plugin.bat +2 -0
  37. aristaproto-0.1.3/src/aristaproto/py.typed +0 -0
  38. aristaproto-0.1.3/src/aristaproto/templates/template.py.j2 +257 -0
  39. aristaproto-0.1.3/src/aristaproto/utils.py +56 -0
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Arista Networks
4
+
5
+ Copyright (c) 2019-2023 Daniel G. Taylor
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,493 @@
1
+ Metadata-Version: 2.1
2
+ Name: aristaproto
3
+ Version: 0.1.3
4
+ Summary: Arista Protobuf / Python gRPC bindings generator & library
5
+ Home-page: https://github.com/aristanetworks/python-aristaproto
6
+ License: MIT
7
+ Keywords: protobuf,gRPC,aristanetworks,arista
8
+ Author: Arista Networks
9
+ Author-email: ansible@arista.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Provides-Extra: compiler
19
+ Provides-Extra: rust-codec
20
+ Requires-Dist: betterproto-rust-codec (==0.1.1) ; extra == "rust-codec"
21
+ Requires-Dist: black (>=23.1.0) ; extra == "compiler"
22
+ Requires-Dist: grpclib (>=0.4.1,<0.5.0)
23
+ Requires-Dist: isort (>=5.11.5,<6.0.0) ; extra == "compiler"
24
+ Requires-Dist: jinja2 (>=3.0.3) ; extra == "compiler"
25
+ Requires-Dist: python-dateutil (>=2.8,<3.0)
26
+ Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
27
+ Project-URL: Repository, https://github.com/aristanetworks/python-aristaproto
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Arista Protobuf / Python gRPC bindings generator & library
31
+
32
+ This was originally forked from <https://github.com/danielgtaylor/python-betterproto> @ [b8a091ae7055dd949d193695a06c9536ad51eea8](https://github.com/danielgtaylor/python-betterproto/commit/b8a091ae7055dd949d193695a06c9536ad51eea8).
33
+
34
+ Afterwards commits up to `1f88b67eeb9871d33da154fd2c859b9d1aed62c1` on `python-betterproto` have been cherry-picked.
35
+
36
+ Changes in this project compared with the base project:
37
+
38
+ - Renamed to `aristaproto`.
39
+ - Cut support for Python < 3.9.
40
+ - Updating various CI actions and dependencies.
41
+ - Merged docs from multiple `rst` files to MarkDown.
42
+ - Keep nanosecond precision for `Timestamp`.
43
+ - Subclass `datetime` to store the original nano-second value when converting from `Timestamp` to `datetime`.
44
+ - On conversion from the subclass of `datetime` to `Timestamp` the original nano-second value is restored.
45
+
46
+ ## Installation
47
+
48
+ First, install the package. Note that the `[compiler]` feature flag tells it to install extra dependencies only needed by the `protoc` plugin:
49
+
50
+ ```sh
51
+ # Install both the library and compiler
52
+ pip install "aristaproto[compiler]"
53
+
54
+ # Install just the library (to use the generated code output)
55
+ pip install aristaproto
56
+ ```
57
+
58
+ ## Getting Started
59
+
60
+ ### Compiling proto files
61
+
62
+ Given you installed the compiler and have a proto file, e.g `example.proto`:
63
+
64
+ ```protobuf
65
+ syntax = "proto3";
66
+
67
+ package hello;
68
+
69
+ // Greeting represents a message you can tell a user.
70
+ message Greeting {
71
+ string message = 1;
72
+ }
73
+ ```
74
+
75
+ You can run the following to invoke protoc directly:
76
+
77
+ ```sh
78
+ mkdir lib
79
+ protoc -I . --python_aristaproto_out=lib example.proto
80
+ ```
81
+
82
+ or run the following to invoke protoc via grpcio-tools:
83
+
84
+ ```sh
85
+ pip install grpcio-tools
86
+ python -m grpc_tools.protoc -I . --python_aristaproto_out=lib example.proto
87
+ ```
88
+
89
+ This will generate `lib/hello/__init__.py` which looks like:
90
+
91
+ ```python
92
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
93
+ # sources: example.proto
94
+ # plugin: python-aristaproto
95
+ from dataclasses import dataclass
96
+
97
+ import aristaproto
98
+
99
+
100
+ @dataclass
101
+ class Greeting(aristaproto.Message):
102
+ """Greeting represents a message you can tell a user."""
103
+
104
+ message: str = aristaproto.string_field(1)
105
+ ```
106
+
107
+ Now you can use it!
108
+
109
+ ```python
110
+ >>> from lib.hello import Greeting
111
+ >>> test = Greeting()
112
+ >>> test
113
+ Greeting(message='')
114
+
115
+ >>> test.message = "Hey!"
116
+ >>> test
117
+ Greeting(message="Hey!")
118
+
119
+ >>> serialized = bytes(test)
120
+ >>> serialized
121
+ b'\n\x04Hey!'
122
+
123
+ >>> another = Greeting().parse(serialized)
124
+ >>> another
125
+ Greeting(message="Hey!")
126
+
127
+ >>> another.to_dict()
128
+ {"message": "Hey!"}
129
+ >>> another.to_json(indent=2)
130
+ '{\n "message": "Hey!"\n}'
131
+ ```
132
+
133
+ ### Async gRPC Support
134
+
135
+ The generated Protobuf `Message` classes are compatible with [grpclib](https://github.com/vmagamedov/grpclib) so you are free to use it if you like. That said, this project also includes support for async gRPC stub generation with better static type checking and code completion support. It is enabled by default.
136
+
137
+ Given an example service definition:
138
+
139
+ ```protobuf
140
+ syntax = "proto3";
141
+
142
+ package echo;
143
+
144
+ message EchoRequest {
145
+ string value = 1;
146
+ // Number of extra times to echo
147
+ uint32 extra_times = 2;
148
+ }
149
+
150
+ message EchoResponse {
151
+ repeated string values = 1;
152
+ }
153
+
154
+ message EchoStreamResponse {
155
+ string value = 1;
156
+ }
157
+
158
+ service Echo {
159
+ rpc Echo(EchoRequest) returns (EchoResponse);
160
+ rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse);
161
+ }
162
+ ```
163
+
164
+ Generate echo proto file:
165
+
166
+ ```sh
167
+ python -m grpc_tools.protoc -I . --python_aristaproto_out=. echo.proto
168
+ ```
169
+
170
+ A client can be implemented as follows:
171
+
172
+ ```python
173
+ import asyncio
174
+ import echo
175
+
176
+ from grpclib.client import Channel
177
+
178
+
179
+ async def main():
180
+ channel = Channel(host="127.0.0.1", port=50051)
181
+ service = echo.EchoStub(channel)
182
+ response = await service.echo(echo.EchoRequest(value="hello", extra_times=1))
183
+ print(response)
184
+
185
+ async for response in service.echo_stream(echo.EchoRequest(value="hello", extra_times=1)):
186
+ print(response)
187
+
188
+ # don't forget to close the channel when done!
189
+ channel.close()
190
+
191
+
192
+ if __name__ == "__main__":
193
+ loop = asyncio.get_event_loop()
194
+ loop.run_until_complete(main())
195
+
196
+ ```
197
+
198
+ which would output
199
+
200
+ ```python
201
+ EchoResponse(values=['hello', 'hello'])
202
+ EchoStreamResponse(value='hello')
203
+ EchoStreamResponse(value='hello')
204
+ ```
205
+
206
+ This project also produces server-facing stubs that can be used to implement a Python
207
+ gRPC server.
208
+ To use them, simply subclass the base class in the generated files and override the
209
+ service methods:
210
+
211
+ ```python
212
+ import asyncio
213
+ from echo import EchoBase, EchoRequest, EchoResponse, EchoStreamResponse
214
+ from grpclib.server import Server
215
+ from typing import AsyncIterator
216
+
217
+
218
+ class EchoService(EchoBase):
219
+ async def echo(self, echo_request: "EchoRequest") -> "EchoResponse":
220
+ return EchoResponse([echo_request.value for _ in range(echo_request.extra_times)])
221
+
222
+ async def echo_stream(self, echo_request: "EchoRequest") -> AsyncIterator["EchoStreamResponse"]:
223
+ for _ in range(echo_request.extra_times):
224
+ yield EchoStreamResponse(echo_request.value)
225
+
226
+
227
+ async def main():
228
+ server = Server([EchoService()])
229
+ await server.start("127.0.0.1", 50051)
230
+ await server.wait_closed()
231
+
232
+ if __name__ == '__main__':
233
+ loop = asyncio.get_event_loop()
234
+ loop.run_until_complete(main())
235
+ ```
236
+
237
+ ### JSON
238
+
239
+ Both serializing and parsing are supported to/from JSON and Python dictionaries using the following methods:
240
+
241
+ - Dicts: `Message().to_dict()`, `Message().from_dict(...)`
242
+ - JSON: `Message().to_json()`, `Message().from_json(...)`
243
+
244
+ For compatibility the default is to convert field names to `camelCase`. You can control this behavior by passing a casing value, e.g:
245
+
246
+ ```python
247
+ MyMessage().to_dict(casing=aristaproto.Casing.SNAKE)
248
+ ```
249
+
250
+ ### Determining if a message was sent
251
+
252
+ Sometimes it is useful to be able to determine whether a message has been sent on the wire. This is how the Google wrapper types work to let you know whether a value is unset, set as the default (zero value), or set as something else, for example.
253
+
254
+ Use `aristaproto.serialized_on_wire(message)` to determine if it was sent. This is a little bit different from the official Google generated Python code, and it lives outside the generated `Message` class to prevent name clashes. Note that it **only** supports Proto 3 and thus can **only** be used to check if `Message` fields are set. You cannot check if a scalar was sent on the wire.
255
+
256
+ ```py
257
+ # Old way (official Google Protobuf package)
258
+ >>> mymessage.HasField('myfield')
259
+
260
+ # New way (this project)
261
+ >>> aristaproto.serialized_on_wire(mymessage.myfield)
262
+ ```
263
+
264
+ ### One-of Support
265
+
266
+ Protobuf supports grouping fields in a `oneof` clause. Only one of the fields in the group may be set at a given time. For example, given the proto:
267
+
268
+ ```protobuf
269
+ syntax = "proto3";
270
+
271
+ message Test {
272
+ oneof foo {
273
+ bool on = 1;
274
+ int32 count = 2;
275
+ string name = 3;
276
+ }
277
+ }
278
+ ```
279
+
280
+ On Python 3.10 and later, you can use a `match` statement to access the provided one-of field, which supports type-checking:
281
+
282
+ ```py
283
+ test = Test()
284
+ match test:
285
+ case Test(on=value):
286
+ print(value) # value: bool
287
+ case Test(count=value):
288
+ print(value) # value: int
289
+ case Test(name=value):
290
+ print(value) # value: str
291
+ case _:
292
+ print("No value provided")
293
+ ```
294
+
295
+ You can also use `aristaproto.which_one_of(message, group_name)` to determine which of the fields was set. It returns a tuple of the field name and value, or a blank string and `None` if unset.
296
+
297
+ ```py
298
+ >>> test = Test()
299
+ >>> aristaproto.which_one_of(test, "foo")
300
+ ["", None]
301
+
302
+ >>> test.on = True
303
+ >>> aristaproto.which_one_of(test, "foo")
304
+ ["on", True]
305
+
306
+ # Setting one member of the group resets the others.
307
+ >>> test.count = 57
308
+ >>> aristaproto.which_one_of(test, "foo")
309
+ ["count", 57]
310
+
311
+ # Default (zero) values also work.
312
+ >>> test.name = ""
313
+ >>> aristaproto.which_one_of(test, "foo")
314
+ ["name", ""]
315
+ ```
316
+
317
+ Again this is a little different than the official Google code generator:
318
+
319
+ ```py
320
+ # Old way (official Google protobuf package)
321
+ >>> message.WhichOneof("group")
322
+ "foo"
323
+
324
+ # New way (this project)
325
+ >>> aristaproto.which_one_of(message, "group")
326
+ ["foo", "foo's value"]
327
+ ```
328
+
329
+ ### Well-Known Google Types
330
+
331
+ Google provides several well-known message types like a timestamp, duration, and several wrappers used to provide optional zero value support. Each of these has a special JSON representation and is handled a little differently from normal messages. The Python mapping for these is as follows:
332
+
333
+ | Google Message | Python Type | Default |
334
+ | --------------------------- | ---------------------------------------- | ---------------------- |
335
+ | `google.protobuf.duration` | [`datetime.timedelta`][td] | `0` |
336
+ | `google.protobuf.timestamp` | Timezone-aware [`datetime.datetime`][dt] | `1970-01-01T00:00:00Z` |
337
+ | `google.protobuf.*Value` | `Optional[...]` | `None` |
338
+ | `google.protobuf.*` | `aristaproto.lib.google.protobuf.*` | `None` |
339
+
340
+ [td]: https://docs.python.org/3/library/datetime.html#timedelta-objects
341
+ [dt]: https://docs.python.org/3/library/datetime.html#datetime.datetime
342
+
343
+ For the wrapper types, the Python type corresponds to the wrapped type, e.g. `google.protobuf.BoolValue` becomes `Optional[bool]` while `google.protobuf.Int32Value` becomes `Optional[int]`. All of the optional values default to `None`, so don't forget to check for that possible state. Given:
344
+
345
+ ```protobuf
346
+ syntax = "proto3";
347
+
348
+ import "google/protobuf/duration.proto";
349
+ import "google/protobuf/timestamp.proto";
350
+ import "google/protobuf/wrappers.proto";
351
+
352
+ message Test {
353
+ google.protobuf.BoolValue maybe = 1;
354
+ google.protobuf.Timestamp ts = 2;
355
+ google.protobuf.Duration duration = 3;
356
+ }
357
+ ```
358
+
359
+ You can do stuff like:
360
+
361
+ ```py
362
+ >>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"})
363
+ >>> t
364
+ Test(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000))
365
+
366
+ >>> t.ts - t.duration
367
+ datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc)
368
+
369
+ >>> t.ts.isoformat()
370
+ '2019-01-01T12:00:00+00:00'
371
+
372
+ >>> t.maybe = None
373
+ >>> t.to_dict()
374
+ {'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'}
375
+ ```
376
+
377
+ ## Generating Pydantic Models
378
+
379
+ You can use python-aristaproto to generate pydantic based models, using
380
+ pydantic dataclasses. This means the results of the protobuf unmarshalling will
381
+ be typed checked. The usage is the same, but you need to add a custom option
382
+ when calling the protobuf compiler:
383
+
384
+ ```sh
385
+ protoc -I . --python_aristaproto_opt=pydantic_dataclasses --python_aristaproto_out=lib example.proto
386
+ ```
387
+
388
+ With the important change being `--python_aristaproto_opt=pydantic_dataclasses`. This will
389
+ swap the dataclass implementation from the builtin python dataclass to the
390
+ pydantic dataclass. You must have pydantic as a dependency in your project for
391
+ this to work.
392
+
393
+ ## Development
394
+
395
+ ### Requirements
396
+
397
+ - Python (3.9 or higher)
398
+
399
+ - [poetry](https://python-poetry.org/docs/#installation)
400
+ *Needed to install dependencies in a virtual environment*
401
+
402
+ - [poethepoet](https://github.com/nat-n/poethepoet) for running development tasks as defined in pyproject.toml
403
+ - Can be installed to your host environment via `pip install poethepoet` then executed as simple `poe`
404
+ - or run from the poetry venv as `poetry run poe`
405
+
406
+ ### Setup
407
+
408
+ ```sh
409
+ # Get set up with the virtual env & dependencies
410
+ poetry install -E compiler
411
+
412
+ # Activate the poetry environment
413
+ poetry shell
414
+ ```
415
+
416
+ ### Code style
417
+
418
+ This project enforces [black](https://github.com/psf/black) python code formatting.
419
+
420
+ Before committing changes run:
421
+
422
+ ```sh
423
+ poe format
424
+ ```
425
+
426
+ To avoid merge conflicts later, non-black formatted python code will fail in CI.
427
+
428
+ ### Tests
429
+
430
+ There are two types of tests:
431
+
432
+ 1. Standard tests
433
+ 2. Custom tests
434
+
435
+ #### Standard tests
436
+
437
+ Adding a standard test case is easy.
438
+
439
+ - Create a new directory `aristaproto/tests/inputs/<name>`
440
+ - add `<name>.proto` with a message called `Test`
441
+ - add `<name>.json` with some test data (optional)
442
+
443
+ It will be picked up automatically when you run the tests.
444
+
445
+ - See also: [Standard Tests Development Guide](tests/README.md)
446
+
447
+ #### Custom tests
448
+
449
+ Custom tests are found in `tests/test_*.py` and are run with pytest.
450
+
451
+ #### Running
452
+
453
+ Here's how to run the tests.
454
+
455
+ ```sh
456
+ # Generate assets from sample .proto files required by the tests
457
+ poe generate
458
+ # Run the tests
459
+ poe test
460
+ ```
461
+
462
+ To run tests as they are run in CI (with tox) run:
463
+
464
+ ```sh
465
+ poe full-test
466
+ ```
467
+
468
+ ### (Re)compiling Google Well-known Types
469
+
470
+ Betterproto includes compiled versions for Google's well-known types at [src/aristaproto/lib/google](src/aristaproto/lib/google).
471
+ Be sure to regenerate these files when modifying the plugin output format, and validate by running the tests.
472
+
473
+ Normally, the plugin does not compile any references to `google.protobuf`, since they are pre-compiled. To force compilation of `google.protobuf`, use the option `--custom_opt=INCLUDE_GOOGLE`.
474
+
475
+ Assuming your `google.protobuf` source files (included with all releases of `protoc`) are located in `/usr/local/include`, you can regenerate them as follows:
476
+
477
+ ```sh
478
+ protoc \
479
+ --plugin=protoc-gen-custom=src/aristaproto/plugin/main.py \
480
+ --custom_opt=INCLUDE_GOOGLE \
481
+ --custom_out=src/aristaproto/lib \
482
+ -I /usr/local/include/ \
483
+ /usr/local/include/google/protobuf/*.proto
484
+ ```
485
+
486
+ ## License
487
+
488
+ Copyright 2023 Arista Networks
489
+
490
+ Copyright 2019-2023 Daniel G. Taylor
491
+
492
+ This software is free to use under the MIT license. See the [LICENSE](./LICENSE.md) file for license text.
493
+