protoplasm6 6.1.0__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.
- protoplasm6-6.1.0/LICENSE +22 -0
- protoplasm6-6.1.0/PKG-INFO +402 -0
- protoplasm6-6.1.0/README.md +366 -0
- protoplasm6-6.1.0/protoplasm6/__init__.py +5 -0
- protoplasm6-6.1.0/protoplasm6/bases/__init__.py +3 -0
- protoplasm6-6.1.0/protoplasm6/bases/_client.py +83 -0
- protoplasm6-6.1.0/protoplasm6/bases/_exwrap.py +113 -0
- protoplasm6-6.1.0/protoplasm6/bases/_metadata.py +89 -0
- protoplasm6-6.1.0/protoplasm6/bases/_methwrap.py +220 -0
- protoplasm6-6.1.0/protoplasm6/bases/_remotewrap.py +268 -0
- protoplasm6-6.1.0/protoplasm6/bases/_servicer.py +46 -0
- protoplasm6-6.1.0/protoplasm6/bases/dataclass_bases.py +2 -0
- protoplasm6-6.1.0/protoplasm6/bases/grpc_bases.py +4 -0
- protoplasm6-6.1.0/protoplasm6/casting/__init__.py +5 -0
- protoplasm6-6.1.0/protoplasm6/casting/casters.py +89 -0
- protoplasm6-6.1.0/protoplasm6/casting/castutils.py +379 -0
- protoplasm6-6.1.0/protoplasm6/casting/converters/__init__.py +128 -0
- protoplasm6-6.1.0/protoplasm6/casting/converters/_base.py +39 -0
- protoplasm6-6.1.0/protoplasm6/casting/converters/_dict.py +39 -0
- protoplasm6-6.1.0/protoplasm6/casting/converters/_direct.py +461 -0
- protoplasm6-6.1.0/protoplasm6/casting/dictator.py +85 -0
- protoplasm6-6.1.0/protoplasm6/casting/dictators/__init__.py +375 -0
- protoplasm6-6.1.0/protoplasm6/casting/objectifier.py +100 -0
- protoplasm6-6.1.0/protoplasm6/decorators/__init__.py +1 -0
- protoplasm6-6.1.0/protoplasm6/decorators/apihelpers.py +100 -0
- protoplasm6-6.1.0/protoplasm6/errors/__init__.py +4 -0
- protoplasm6-6.1.0/protoplasm6/errors/_api.py +238 -0
- protoplasm6-6.1.0/protoplasm6/errors/_base.py +31 -0
- protoplasm6-6.1.0/protoplasm6/grpcserver.py +53 -0
- protoplasm6-6.1.0/protoplasm6/loader/__init__.py +17 -0
- protoplasm6-6.1.0/protoplasm6/plasm/__init__.py +7 -0
- protoplasm6-6.1.0/protoplasm6/structs/__init__.py +8 -0
- protoplasm6-6.1.0/protoplasm6/structs/_base.py +4 -0
- protoplasm6-6.1.0/protoplasm6/structs/_ctx/__init__.py +2 -0
- protoplasm6-6.1.0/protoplasm6/structs/_ctx/_basectx.py +15 -0
- protoplasm6-6.1.0/protoplasm6/structs/_ctx/_grpcctx.py +53 -0
- protoplasm6-6.1.0/protoplasm6/structs/_methodtype.py +33 -0
- protoplasm6-6.1.0/protoplasm6/structs/_oneofs.py +95 -0
- protoplasm6-6.1.0/protoplasm6/structs/_reqestsiter.py +32 -0
- protoplasm6-6.1.0/protoplasm6/structs/_responseiter.py +172 -0
- protoplasm6-6.1.0/protoplasm6/structs/dataclassbase.py +228 -0
- protoplasm6-6.1.0/protoplasm6.egg-info/PKG-INFO +402 -0
- protoplasm6-6.1.0/protoplasm6.egg-info/SOURCES.txt +57 -0
- protoplasm6-6.1.0/protoplasm6.egg-info/dependency_links.txt +1 -0
- protoplasm6-6.1.0/protoplasm6.egg-info/requires.txt +6 -0
- protoplasm6-6.1.0/protoplasm6.egg-info/top_level.txt +1 -0
- protoplasm6-6.1.0/pyproject.toml +61 -0
- protoplasm6-6.1.0/setup.cfg +4 -0
- protoplasm6-6.1.0/setup.py +6 -0
- protoplasm6-6.1.0/tests/test_call_metadata.py +117 -0
- protoplasm6-6.1.0/tests/test_casting.py +675 -0
- protoplasm6-6.1.0/tests/test_castutils.py +803 -0
- protoplasm6-6.1.0/tests/test_converters.py +341 -0
- protoplasm6-6.1.0/tests/test_dataclasses.py +136 -0
- protoplasm6-6.1.0/tests/test_dictator.py +506 -0
- protoplasm6-6.1.0/tests/test_dictators.py +390 -0
- protoplasm6-6.1.0/tests/test_objectifier.py +754 -0
- protoplasm6-6.1.0/tests/test_oneofs_and_dicts.py +135 -0
- protoplasm6-6.1.0/tests/test_services.py +263 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thordur Matthiasson
|
|
4
|
+
Copyright (c) 2019-2024 CCP Games
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: protoplasm6
|
|
3
|
+
Version: 6.1.0
|
|
4
|
+
Summary: Utilities for working with Protobuf & gRPC in Python (a continuation fork of CCP Games' protoplasm).
|
|
5
|
+
Author-email: Thordur Matthiasson <doddi79@gmail.com>, Daniel Maxson <dmaxson@ccpgames.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/doddi79/protoplasm6
|
|
8
|
+
Project-URL: Documentation, https://github.com/doddi79/protoplasm6/blob/main/README.md
|
|
9
|
+
Project-URL: Repository, https://github.com/doddi79/protoplasm6.git
|
|
10
|
+
Project-URL: Issues, https://github.com/doddi79/protoplasm6/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/doddi79/protoplasm6/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: protobuf,proto,grpc,dataclasses,neobuf,protoplasm,tools,utils
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: batutils<3,>=2.2
|
|
31
|
+
Requires-Dist: protobuf<8,>=7.34
|
|
32
|
+
Requires-Dist: grpcio<2,>=1.80
|
|
33
|
+
Provides-Extra: googleapis
|
|
34
|
+
Requires-Dist: googleapis-common-protos<2,>=1.70; extra == "googleapis"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Protoplasm6
|
|
38
|
+
|
|
39
|
+
Utilities for working with Protobuf & gRPC in Python: Python 3 dataclasses
|
|
40
|
+
generated from compiled protobuf messages and casting between the two, quickly
|
|
41
|
+
initializing large/nested Protobuf objects, and simplified gRPC service
|
|
42
|
+
interfaces where you implement plain Python methods with plain Python arguments
|
|
43
|
+
and return values instead of fiddling with request/response messages.
|
|
44
|
+
|
|
45
|
+
See the **[Public API index](docs/api-index.md)** for a task-oriented reference of
|
|
46
|
+
everything protoplasm6 offers.
|
|
47
|
+
|
|
48
|
+
## Note
|
|
49
|
+
|
|
50
|
+
This is a fork of the [`protoplasm`](https://github.com/ccpgames/protoplasm)
|
|
51
|
+
package (v5.3.1) by CCP Games, made in order to continue its development and
|
|
52
|
+
maintenance. The package _and_ module are now called `protoplasm6` (starting
|
|
53
|
+
at version 6.0.0) so it can live side-by-side with the original.
|
|
54
|
+
|
|
55
|
+
## Protoplasm6 & Neobuilder6
|
|
56
|
+
|
|
57
|
+
Protoplasm6 is the **runtime** half of a pair. The other half is
|
|
58
|
+
[`neobuilder6`](https://github.com/doddi79/neobuilder6), the **build-time**
|
|
59
|
+
code generator:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
neobuilder6 (build time) protoplasm6 (run time)
|
|
63
|
+
foo.proto ──protoc──► foo_pb2.py, foo_pb2_grpc.py
|
|
64
|
+
└─► foo_dc.py (dataclasses) ──► protoplasm6.plasm.DataclassBase
|
|
65
|
+
└─► foo_api.py (interfaces) ──► protoplasm6.plasm errors/decorators
|
|
66
|
+
└─► foo_grpc_receiver.py (servicers) ──► protoplasm6.plasm.BaseGrpcServicer
|
|
67
|
+
└─► foo_grpc_sender.py (clients) ──► protoplasm6.plasm.BaseGrpcClientImplementation
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Neobuilder6 depends on Protoplasm6 (it uses some of its utilities, and the
|
|
71
|
+
code it generates imports `protoplasm6`).
|
|
72
|
+
- Protoplasm6 does **not** depend on Neobuilder6 at runtime, but its unit
|
|
73
|
+
tests use Neobuilder6 to build the test protos (so Neobuilder6 is installed
|
|
74
|
+
with `--no-deps` for testing).
|
|
75
|
+
|
|
76
|
+
The major and minor versions of the two go hand in hand, so code built with
|
|
77
|
+
Neobuilder6 `6.x` should be run with Protoplasm6 `6.x` (where Protoplasm6's
|
|
78
|
+
minor version is the same or higher).
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install protoplasm6
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
...although you'll most likely want [`neobuilder6`](https://github.com/doddi79/neobuilder6)
|
|
87
|
+
as well to build your proto files, which pulls in protoplasm6 for you:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
pip install neobuilder6
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Protoplasm6 requires Python 3.10+ and depends on `protobuf` 7.x, `grpcio` and
|
|
94
|
+
[`batutils`](https://github.com/doddi79/batutils).
|
|
95
|
+
|
|
96
|
+
Note that `*_pb2.py` files generated by `protoc` (i.e. Neobuilder6) check
|
|
97
|
+
the version of the `protobuf` runtime when imported, so the `protobuf` version
|
|
98
|
+
used to run a generated package must be the same major version and at least
|
|
99
|
+
the same minor version as the `grpcio-tools` that generated it.
|
|
100
|
+
|
|
101
|
+
## The idea
|
|
102
|
+
|
|
103
|
+
Design your whole system of services and messages in proto files, and
|
|
104
|
+
implement each service as a plain Python class with plain Python methods,
|
|
105
|
+
arguments and return values. The generated client for a service implements the
|
|
106
|
+
exact same interface as the service itself, so the code calling a service
|
|
107
|
+
can't tell (and doesn't care) whether it's talking to a local object in the
|
|
108
|
+
same process or a remote gRPC service.
|
|
109
|
+
|
|
110
|
+
That means you can start with a monolith where everything runs in the same
|
|
111
|
+
runtime, and later pick out any service "wholesale" and move it to its own
|
|
112
|
+
remote process, swapping the local implementation for the generated gRPC
|
|
113
|
+
client, without changing the "core code" on either side. No refactoring
|
|
114
|
+
needed, and nothing in your business logic needs to know what a "Request",
|
|
115
|
+
"Response", protobuf or even protoplasm is.
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
The examples below assume a proto file like this one, at
|
|
120
|
+
`./proto/myshop/orders/orders.proto`, built by Neobuilder6 (see its README):
|
|
121
|
+
|
|
122
|
+
```protobuf
|
|
123
|
+
syntax = "proto3";
|
|
124
|
+
|
|
125
|
+
package myshop.orders;
|
|
126
|
+
|
|
127
|
+
import "google/protobuf/timestamp.proto";
|
|
128
|
+
|
|
129
|
+
service OrderService {
|
|
130
|
+
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) {}
|
|
131
|
+
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
message Customer {
|
|
135
|
+
string customer_id = 1;
|
|
136
|
+
string name = 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
message Order {
|
|
140
|
+
string order_id = 1;
|
|
141
|
+
Customer customer = 2;
|
|
142
|
+
repeated string items = 3;
|
|
143
|
+
google.protobuf.Timestamp placed_at = 4;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
message GetOrderRequest { string order_id = 1; }
|
|
147
|
+
message GetOrderResponse { Order order = 1; }
|
|
148
|
+
|
|
149
|
+
message ListOrdersRequest { string customer_id = 1; }
|
|
150
|
+
message ListOrdersResponse { repeated Order orders = 1; }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That gives you the modules `orders_pb2`, `orders_pb2_grpc`, `orders_dc`,
|
|
154
|
+
`orders_api`, `orders_grpc_receiver` and `orders_grpc_sender` in the
|
|
155
|
+
`myshop.orders` package.
|
|
156
|
+
|
|
157
|
+
### Dataclasses
|
|
158
|
+
|
|
159
|
+
Every proto message gets a dataclass in the `*_dc.py` module that extends
|
|
160
|
+
`protoplasm6.plasm.DataclassBase`:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import datetime
|
|
164
|
+
from myshop.orders.orders_dc import *
|
|
165
|
+
|
|
166
|
+
order = Order(order_id='o1', customer=Customer(customer_id='c1', name='Jane'),
|
|
167
|
+
items=['apple', 'pear'], placed_at=datetime.datetime.now())
|
|
168
|
+
|
|
169
|
+
order.to_dict() # Nested dict of JSON serializable values
|
|
170
|
+
order.to_json()
|
|
171
|
+
order.to_proto() # The orders_pb2.Order protobuf message
|
|
172
|
+
order.to_protostr() # Serialized protobuf bytes
|
|
173
|
+
order.to_base64() # Serialized protobuf as base64
|
|
174
|
+
|
|
175
|
+
Order.from_dict({'order_id': 'o1', 'customer': {'customer_id': 'c1'}})
|
|
176
|
+
Order.from_json(...)
|
|
177
|
+
Order.from_proto(...)
|
|
178
|
+
Order.from_protostr(...)
|
|
179
|
+
Order.from_base64(...)
|
|
180
|
+
Order.from_kwdict(order_id='o1', customer__name='Jane') # Double underscores for nesting
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Nested messages can be given as dicts, lists/tuples (as positional args) or
|
|
184
|
+
dataclass instances and will be forced into the correct dataclass type.
|
|
185
|
+
`google.protobuf.Timestamp` and `Duration` fields are `datetime.datetime` and
|
|
186
|
+
`datetime.timedelta`, `Any` fields hold any dataclass, and `Struct`/`Value`
|
|
187
|
+
fields hold plain dicts/values.
|
|
188
|
+
|
|
189
|
+
Dataclasses of messages with `oneof` groups behave like protobuf messages:
|
|
190
|
+
setting one member resets the others, and you can ask which one is set:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
payment.which_oneof('method') # e.g. 'card_number' or None
|
|
194
|
+
payment.get_oneof('method') # The value of the member that's set (or None)
|
|
195
|
+
payment.clear_oneof('method')
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Casting utilities
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from protoplasm6 import casting
|
|
202
|
+
from myshop.orders import orders_pb2
|
|
203
|
+
|
|
204
|
+
casting.mkproto(orders_pb2.Order, order_id='o1', customer__name='Jane')
|
|
205
|
+
casting.mkdataclass(Order, order_id='o1', customer__name='Jane')
|
|
206
|
+
casting.proto_to_dataclass(some_proto)
|
|
207
|
+
casting.dataclass_to_proto(some_dataclass)
|
|
208
|
+
casting.proto_to_dict(some_proto)
|
|
209
|
+
casting.dict_to_proto(orders_pb2.Order, {...})
|
|
210
|
+
casting.dict_to_dataclass(Order, {...})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Converters (protobuf <-> dataclass)
|
|
214
|
+
|
|
215
|
+
Whenever protoplasm6 casts between protobuf messages and dataclasses (e.g.
|
|
216
|
+
`to_proto()`, `from_proto()`, `casting.proto_to_dataclass()`,
|
|
217
|
+
`casting.dataclass_to_proto()` and in every gRPC call) it uses the current
|
|
218
|
+
**converter**. There are two:
|
|
219
|
+
|
|
220
|
+
| Name | Class | What it does |
|
|
221
|
+
|--------------------|-------------------|--------------------------------------------------------------------------------------------------------------|
|
|
222
|
+
| `direct` (default) | `DirectConverter` | Converts directly between messages and dataclasses using the protobuf message API |
|
|
223
|
+
| `dict` | `DictConverter` | The original (protoplasm 5.x) conversion: protobuf <-> `json_format` dict <-> dataclass |
|
|
224
|
+
|
|
225
|
+
The `direct` converter is roughly 4-7 times faster (both ways, for small and
|
|
226
|
+
large messages alike) and also gets a few things right that the `dict` one
|
|
227
|
+
gets wrong (see the CHANGELOG), e.g. `Dict[int, ...]` maps keep
|
|
228
|
+
their `int` keys, `Dict[bool, ...]` maps work, timezone aware `datetime`
|
|
229
|
+
values are converted to UTC properly and messages with `oneof` scalar fields
|
|
230
|
+
work.
|
|
231
|
+
|
|
232
|
+
Fields it doesn't know how to handle (e.g. ones with your own custom
|
|
233
|
+
dictators) fall back to the `dict` logic for that field only.
|
|
234
|
+
|
|
235
|
+
You can switch converters globally, temporarily or via an environment
|
|
236
|
+
variable:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from protoplasm6.casting import converters
|
|
240
|
+
|
|
241
|
+
converters.set_converter('dict') # Globally
|
|
242
|
+
|
|
243
|
+
with converters.use_converter('dict'): # Only within this block (and context/thread)
|
|
244
|
+
proto = order.to_proto()
|
|
245
|
+
|
|
246
|
+
converters.get_converter() # The converter currently in use
|
|
247
|
+
|
|
248
|
+
converters.DictConverter().dataclass_to_proto(order) # Or use one explicitly
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
PROTOPLASM6_CONVERTER=dict python my_service.py
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
You can also roll your own by extending `converters.BaseConverter` and
|
|
256
|
+
registering it with `converters.register_converter(...)`.
|
|
257
|
+
|
|
258
|
+
### Services
|
|
259
|
+
|
|
260
|
+
For every `service` in a proto file you get a `*Interface` class in the
|
|
261
|
+
`*_api.py` module, with a method per RPC that takes the request message's
|
|
262
|
+
fields as arguments and returns the response message's field(s), i.e. the
|
|
263
|
+
request/response messages are "exploded" for you:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from protoplasm6 import plasm
|
|
267
|
+
from myshop.orders.orders_api import *
|
|
268
|
+
from myshop.orders.orders_dc import *
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class MyOrderService(OrderServiceInterface):
|
|
272
|
+
@plasm.require_params # Raises InvalidArgument if any params are missing
|
|
273
|
+
def get_order(self, order_id: str) -> Order:
|
|
274
|
+
order = my_database.get(order_id)
|
|
275
|
+
if not order:
|
|
276
|
+
raise plasm.NotFound() # Becomes a NOT_FOUND gRPC status code
|
|
277
|
+
return order
|
|
278
|
+
|
|
279
|
+
def list_orders(self, customer_id: str = '') -> list[Order]:
|
|
280
|
+
return my_database.find(customer_id=customer_id)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Unimplemented methods raise `plasm.Unimplemented` (gRPC `UNIMPLEMENTED`).
|
|
284
|
+
|
|
285
|
+
#### Serving
|
|
286
|
+
|
|
287
|
+
The interface knows its gRPC servicer class (from `*_grpc_receiver.py`) so
|
|
288
|
+
you can do:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from protoplasm6 import plasm
|
|
292
|
+
|
|
293
|
+
server = plasm.GrpcServer('[::]:50051')
|
|
294
|
+
server.add_servicer(OrderServiceInterface.__servicer_cls__(MyOrderService()))
|
|
295
|
+
server.serve()
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
...or add the servicer to your own `grpc.server(...)` with
|
|
299
|
+
`servicer.add_to_server(grpc_server)`.
|
|
300
|
+
|
|
301
|
+
#### Calling
|
|
302
|
+
|
|
303
|
+
The `*_grpc_sender.py` module has a client class (named after the service)
|
|
304
|
+
that implements the same interface over gRPC:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from myshop.orders.orders_grpc_sender import OrderService
|
|
308
|
+
|
|
309
|
+
orders: OrderServiceInterface = OrderService('localhost:50051') # credentials=True for SSL, options={...} for channel options
|
|
310
|
+
# ...or, while it's all still one monolith: orders = MyOrderService()
|
|
311
|
+
|
|
312
|
+
order = orders.get_order('o1')
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
orders.get_order('nope')
|
|
316
|
+
except plasm.NotFound:
|
|
317
|
+
... # gRPC status codes are raised as the corresponding plasm errors
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Call metadata (headers)
|
|
321
|
+
|
|
322
|
+
To send metadata (i.e. headers, e.g. for authorization) with calls, give the
|
|
323
|
+
client a `metadata` dict (or a callable returning one, which gets called for
|
|
324
|
+
every call, e.g. to refresh tokens) and/or use the `call_metadata` context
|
|
325
|
+
manager:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
orders = OrderService('localhost:50051', metadata={'authorization': f'Bearer {token}'})
|
|
329
|
+
|
|
330
|
+
with plasm.call_metadata({'x-request-id': 'r1'}): # For all calls in this block
|
|
331
|
+
orders.get_order('o1')
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
On the service side, decorate a method with `@plasm.takes_context` to get a
|
|
335
|
+
`context` argument and read headers with `context.get_meta('authorization')`.
|
|
336
|
+
|
|
337
|
+
Streaming RPCs (`stream` in the proto) are supported as well, with
|
|
338
|
+
`plasm.RequestIterator` / `plasm.ResponseIterator` on either side (see
|
|
339
|
+
`tests/servicetestutils` and the `river.proto` tests).
|
|
340
|
+
|
|
341
|
+
#### Decorators
|
|
342
|
+
|
|
343
|
+
- `plasm.require_params` - Raises `InvalidArgument` if required params are
|
|
344
|
+
`None`. Use `@plasm.require_params` for all params, or
|
|
345
|
+
`@plasm.require_params('foo', 'bar')` for specific ones, and
|
|
346
|
+
`typecheck=True` to also check the params against their type annotations
|
|
347
|
+
(both `typing` style annotations like `Optional[List[int]]` and modern ones
|
|
348
|
+
like `list[int] | None` are supported).
|
|
349
|
+
- `plasm.takes_context` - Marks a method as taking the gRPC call context as
|
|
350
|
+
a keyword argument (`context` by default).
|
|
351
|
+
|
|
352
|
+
#### Errors
|
|
353
|
+
|
|
354
|
+
`protoplasm6.plasm` (and `protoplasm6.errors`) has an exception for every gRPC
|
|
355
|
+
status code: `NotFound`, `InvalidArgument`, `Unimplemented`,
|
|
356
|
+
`PermissionDenied`, `Unauthenticated`, `Unavailable`, `DeadlineExceeded`,
|
|
357
|
+
`AlreadyExists`, `FailedPrecondition`, `DataLoss`, `ResourceExhausted`,
|
|
358
|
+
`OutOfRange`, `Internal`, `Cancelled`, `Aborted` and `Unknown`. Raise them in
|
|
359
|
+
your implementation and they're raised again on the client side.
|
|
360
|
+
|
|
361
|
+
## Migrating from `protoplasm` 5.x
|
|
362
|
+
|
|
363
|
+
1. Replace the `protoplasm` and `neobuilder` requirements with
|
|
364
|
+
`protoplasm6 >=6.0, <7` and `neobuilder6 >=6.0, <7` (and `ccptools` with
|
|
365
|
+
`batutils`, if you used it).
|
|
366
|
+
2. Rebuild your protos with Neobuilder6 (the generated code now imports
|
|
367
|
+
`protoplasm6`, and the `*_pb2.py` files need `protobuf` 7).
|
|
368
|
+
3. Replace `protoplasm` with `protoplasm6` in your own imports, e.g.
|
|
369
|
+
`from protoplasm import plasm` -> `from protoplasm6 import plasm`.
|
|
370
|
+
4. Make sure you're on Python 3.10 or later.
|
|
371
|
+
|
|
372
|
+
The API itself is unchanged.
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
The unit tests need Neobuilder6 to build the test protos. With both repos
|
|
377
|
+
checked out side by side:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
python -m venv .venv
|
|
381
|
+
.venv/Scripts/activate # or: source .venv/bin/activate
|
|
382
|
+
pip install -r requirements-unittesting.txt
|
|
383
|
+
pip install --no-deps -e . -e ../neobuilder6
|
|
384
|
+
python -m unittest discover -v -f ./tests
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### CI/CD
|
|
388
|
+
|
|
389
|
+
GitHub Actions run the unit tests (on Python 3.10 to 3.14) for pushes to
|
|
390
|
+
`main` and for all pull requests, and publish to PyPI (after running the
|
|
391
|
+
tests again and checking that the release tag, e.g. `v6.1.0` or `6.1.0`,
|
|
392
|
+
matches `__version__`) whenever a GitHub release (or pre-release) is created.
|
|
393
|
+
Publishing needs a `PYPI_UPLOAD_TOKEN` repository secret.
|
|
394
|
+
|
|
395
|
+
Since the tests of each package need the other one, CI installs
|
|
396
|
+
Neobuilder6 (without its dependencies) from the branch with the **same name** in its repo if there is
|
|
397
|
+
one (so changes spanning both repos can be tested together, e.g. with a pull
|
|
398
|
+
request from the same branch name in both) or from `main` otherwise. When
|
|
399
|
+
publishing, Neobuilder6 (without its dependencies) comes from PyPI if a matching release is there
|
|
400
|
+
(otherwise from `main`).
|
|
401
|
+
|
|
402
|
+
When releasing both packages, release Protoplasm6 first, then Neobuilder6.
|