pydantic-rpc 0.2.0__py3-none-any.whl
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.
- pydantic_rpc/__init__.py +8 -0
- pydantic_rpc/core.py +1197 -0
- pydantic_rpc/py.typed +0 -0
- pydantic_rpc-0.2.0.dist-info/METADATA +329 -0
- pydantic_rpc-0.2.0.dist-info/RECORD +7 -0
- pydantic_rpc-0.2.0.dist-info/WHEEL +4 -0
- pydantic_rpc-0.2.0.dist-info/licenses/LICENSE +21 -0
pydantic_rpc/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pydantic-rpc
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
|
+
Author: Yasushi Itoh
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: connecpy>=1.2.0
|
|
8
|
+
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
9
|
+
Requires-Dist: grpcio-reflection>=1.56.2
|
|
10
|
+
Requires-Dist: grpcio-tools>=1.56.2
|
|
11
|
+
Requires-Dist: pydantic>=2.1.1
|
|
12
|
+
Requires-Dist: sonora>=0.2.3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# 🚀 PydanticRPC
|
|
16
|
+
|
|
17
|
+
**PydanticRPC** is a Python library that enables you to rapidly expose Pydantic models via gRPC/ConnectRPC services without writing any protobuf files. Instead, it automatically generates protobuf files on the fly from the method signatures of your Python objects and the type signatures of your Pydantic models.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Below is an example of a simple gRPC service that exposes a PydanticAI agent:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import asyncio
|
|
24
|
+
|
|
25
|
+
from pydantic_ai import Agent
|
|
26
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# `Message` is just an alias for Pydantic's `BaseModel` class.
|
|
30
|
+
class CityLocation(Message):
|
|
31
|
+
city: str
|
|
32
|
+
country: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Olympics(Message):
|
|
36
|
+
year: int
|
|
37
|
+
|
|
38
|
+
def prompt(self):
|
|
39
|
+
return f"Where were the Olympics held in {self.year}?"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OlympicsLocationAgent:
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._agent = Agent("ollama:llama3.2", result_type=CityLocation)
|
|
45
|
+
|
|
46
|
+
async def ask(self, req: Olympics) -> CityLocation:
|
|
47
|
+
result = await self._agent.run(req.prompt())
|
|
48
|
+
return result.data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
s = AsyncIOServer()
|
|
53
|
+
loop = asyncio.get_event_loop()
|
|
54
|
+
loop.run_until_complete(s.run(OlympicsLocationAgent()))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## 💡 Key Features
|
|
59
|
+
|
|
60
|
+
- 🔄 **Automatic Protobuf Generation:** Automatically creates protobuf files matching the method signatures of your Python objects.
|
|
61
|
+
- ⚙️ **Dynamic Code Generation:** Generates server and client stubs using `grpcio-tools`.
|
|
62
|
+
- ✅ **Pydantic Integration:** Uses `pydantic` for robust type validation and serialization.
|
|
63
|
+
- **For gRPC:**
|
|
64
|
+
- 💚 **Health Checking:** Built-in support for gRPC health checks using `grpc_health.v1`.
|
|
65
|
+
- 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
|
|
66
|
+
- ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
|
|
67
|
+
- **For gRPC-Web:**
|
|
68
|
+
- 🌐 **WSGI/ASGI Support:** Create gRPC-Web services that can run as WSGI or ASGI applications powered by `Sonora`.
|
|
69
|
+
- **For Connect-RPC:**
|
|
70
|
+
- 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
|
|
71
|
+
|
|
72
|
+
## 📦 Installation
|
|
73
|
+
|
|
74
|
+
Install PydanticRPC via pip:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install pydantic-rpc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 🚀 Getting Started
|
|
81
|
+
|
|
82
|
+
### 🔧 Synchronous Service Example
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from pydantic_rpc import Server, Message
|
|
86
|
+
|
|
87
|
+
class HelloRequest(Message):
|
|
88
|
+
name: str
|
|
89
|
+
|
|
90
|
+
class HelloReply(Message):
|
|
91
|
+
message: str
|
|
92
|
+
|
|
93
|
+
class Greeter:
|
|
94
|
+
# Define methods that accepts a request and returns a response.
|
|
95
|
+
def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
96
|
+
return HelloReply(message=f"Hello, {request.name}!")
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
server = Server()
|
|
100
|
+
server.run(Greeter())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### ⚙️ Asynchronous Service Example
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
|
|
108
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class HelloRequest(Message):
|
|
112
|
+
name: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class HelloReply(Message):
|
|
116
|
+
message: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Greeter:
|
|
120
|
+
async def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
121
|
+
return HelloReply(message=f"Hello, {request.name}!")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
server = AsyncIOServer()
|
|
126
|
+
loop = asyncio.get_event_loop()
|
|
127
|
+
loop.run_until_complete(server.run(Greeter()))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 🌐 ASGI Application Example
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pydantic_rpc import ASGIApp, Message
|
|
134
|
+
|
|
135
|
+
class HelloRequest(Message):
|
|
136
|
+
name: str
|
|
137
|
+
|
|
138
|
+
class HelloReply(Message):
|
|
139
|
+
message: str
|
|
140
|
+
|
|
141
|
+
class Greeter:
|
|
142
|
+
def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
143
|
+
return HelloReply(message=f"Hello, {request.name}!")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def app(scope, receive, send):
|
|
147
|
+
"""ASGI application.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
scope (dict): The ASGI scope.
|
|
151
|
+
receive (callable): The receive function.
|
|
152
|
+
send (callable): The send function.
|
|
153
|
+
"""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# Please note that `app` is any ASGI application, such as FastAPI or Starlette.
|
|
157
|
+
|
|
158
|
+
app = ASGIApp(app)
|
|
159
|
+
app.mount(Greeter())
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 🌐 WSGI Application Example
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from pydantic_rpc import WSGIApp, Message
|
|
166
|
+
|
|
167
|
+
class HelloRequest(Message):
|
|
168
|
+
name: str
|
|
169
|
+
|
|
170
|
+
class HelloReply(Message):
|
|
171
|
+
message: str
|
|
172
|
+
|
|
173
|
+
class Greeter:
|
|
174
|
+
def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
175
|
+
return HelloReply(message=f"Hello, {request.name}!")
|
|
176
|
+
|
|
177
|
+
def app(environ, start_response):
|
|
178
|
+
"""WSGI application.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
environ (dict): The WSGI environment.
|
|
182
|
+
start_response (callable): The start_response function.
|
|
183
|
+
"""
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# Please note that `app` is any WSGI application, such as Flask or Django.
|
|
187
|
+
|
|
188
|
+
app = WSGIApp(app)
|
|
189
|
+
app.mount(Greeter())
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 🏆 Connecpy (Connect-RPC) Example
|
|
193
|
+
|
|
194
|
+
PydanticRPC also partially supports Connect-RPC via connecpy. Check out “greeting_connecpy.py” for an example:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
rye run python greeting_connecpy.py
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This will launch a Connecpy-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
|
|
201
|
+
|
|
202
|
+
> [!NOTE]
|
|
203
|
+
> Please install `protoc-gen-connecpy` to run the Connecpy example.
|
|
204
|
+
>
|
|
205
|
+
> 1. Install Go.
|
|
206
|
+
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
207
|
+
> 2. Install `protoc-gen-connecpy`:
|
|
208
|
+
> ```bash
|
|
209
|
+
> go install github.com/connecpy/protoc-gen-connecpy@latest
|
|
210
|
+
> ```
|
|
211
|
+
|
|
212
|
+
## ♻️ Skipping Protobuf Generation
|
|
213
|
+
By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
|
|
220
|
+
|
|
221
|
+
## 💎 Advanced Features
|
|
222
|
+
|
|
223
|
+
### 🔗 Multiple Services with Custom Interceptors
|
|
224
|
+
|
|
225
|
+
PydanticRPC supports defining and running multiple services in a single server:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from datetime import datetime
|
|
229
|
+
import grpc
|
|
230
|
+
from grpc import ServicerContext
|
|
231
|
+
|
|
232
|
+
from pydantic_rpc import Server, Message
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class FooRequest(Message):
|
|
236
|
+
name: str
|
|
237
|
+
age: int
|
|
238
|
+
d: dict[str, str]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class FooResponse(Message):
|
|
242
|
+
name: str
|
|
243
|
+
age: int
|
|
244
|
+
d: dict[str, str]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class BarRequest(Message):
|
|
248
|
+
names: list[str]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class BarResponse(Message):
|
|
252
|
+
names: list[str]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class FooService:
|
|
256
|
+
def foo(self, request: FooRequest) -> FooResponse:
|
|
257
|
+
return FooResponse(name=request.name, age=request.age, d=request.d)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class MyMessage(Message):
|
|
261
|
+
name: str
|
|
262
|
+
age: int
|
|
263
|
+
o: int | datetime
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Request(Message):
|
|
267
|
+
name: str
|
|
268
|
+
age: int
|
|
269
|
+
d: dict[str, str]
|
|
270
|
+
m: MyMessage
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Response(Message):
|
|
274
|
+
name: str
|
|
275
|
+
age: int
|
|
276
|
+
d: dict[str, str]
|
|
277
|
+
m: MyMessage | str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class BarService:
|
|
281
|
+
def bar(self, req: BarRequest, ctx: ServicerContext) -> BarResponse:
|
|
282
|
+
return BarResponse(names=req.names)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class CustomInterceptor(grpc.ServerInterceptor):
|
|
286
|
+
def intercept_service(self, continuation, handler_call_details):
|
|
287
|
+
# do something
|
|
288
|
+
print(handler_call_details.method)
|
|
289
|
+
return continuation(handler_call_details)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def app(scope, receive, send):
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
s = Server(10, CustomInterceptor())
|
|
298
|
+
s.run(
|
|
299
|
+
FooService(),
|
|
300
|
+
BarService(),
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 🩺 [TODO] Custom Health Check
|
|
305
|
+
|
|
306
|
+
TODO
|
|
307
|
+
|
|
308
|
+
### 🗄️ Protobuf file generation
|
|
309
|
+
|
|
310
|
+
You can generate protobuf files for a given module and a specified class using `core.py`:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
python core.py a_module.py aClass
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you could generate code for any desired language other than Python.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
## TODO
|
|
320
|
+
- [ ] Streaming Support
|
|
321
|
+
- [ ] Betterproto Support
|
|
322
|
+
- [ ] Sonora-connect Support
|
|
323
|
+
- [ ] Custom Health Check Support
|
|
324
|
+
- [ ] Add more examples
|
|
325
|
+
- [ ] Add tests
|
|
326
|
+
|
|
327
|
+
## 📜 License
|
|
328
|
+
|
|
329
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pydantic_rpc/__init__.py,sha256=AWYjSmYQcMqsqGmGK4k-pQQhX6RBBgkTvNcQtCtsctU,113
|
|
2
|
+
pydantic_rpc/core.py,sha256=IynEU8bReuJM--MvOnw3zlDpneJFROoJNk35pM4fgvI,42286
|
|
3
|
+
pydantic_rpc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pydantic_rpc-0.2.0.dist-info/METADATA,sha256=RBvdwlbQS1Q020U8Tpp0T-CPswc1xYAlsurXM414Awo,8081
|
|
5
|
+
pydantic_rpc-0.2.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
6
|
+
pydantic_rpc-0.2.0.dist-info/licenses/LICENSE,sha256=Y6jkAm2VqPqoGIGQ-mEQCecNfteQ2LwdpYhC5XiH_cA,1069
|
|
7
|
+
pydantic_rpc-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Yasushi Itoh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|