pydantic-rpc 0.7.0__tar.gz → 0.9.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.
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/PKG-INFO +286 -58
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/README.md +277 -47
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/pyproject.toml +5 -9
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/__init__.py +12 -4
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/core.py +686 -203
- pydantic_rpc-0.9.0/src/pydantic_rpc/decorators.py +138 -0
- pydantic_rpc-0.9.0/src/pydantic_rpc/options.py +134 -0
- pydantic_rpc-0.7.0/.github/workflows/release.yml +0 -35
- pydantic_rpc-0.7.0/.github/workflows/test.yml +0 -47
- pydantic_rpc-0.7.0/.gitignore +0 -24
- pydantic_rpc-0.7.0/.python-version +0 -1
- pydantic_rpc-0.7.0/LICENSE +0 -21
- pydantic_rpc-0.7.0/docs/mcp.md +0 -95
- pydantic_rpc-0.7.0/examples/README.md +0 -152
- pydantic_rpc-0.7.0/examples/agent_aio_grpc.py +0 -65
- pydantic_rpc-0.7.0/examples/agent_connecpy.py +0 -56
- pydantic_rpc-0.7.0/examples/asyncio_greeting.py +0 -22
- pydantic_rpc-0.7.0/examples/barservice.proto +0 -17
- pydantic_rpc-0.7.0/examples/barservice_pb2.py +0 -37
- pydantic_rpc-0.7.0/examples/barservice_pb2.pyi +0 -18
- pydantic_rpc-0.7.0/examples/barservice_pb2_grpc.py +0 -106
- pydantic_rpc-0.7.0/examples/foobar.py +0 -76
- pydantic_rpc-0.7.0/examples/foobar_client.py +0 -20
- pydantic_rpc-0.7.0/examples/fooservice.proto +0 -21
- pydantic_rpc-0.7.0/examples/fooservice_pb2.py +0 -45
- pydantic_rpc-0.7.0/examples/fooservice_pb2.pyi +0 -56
- pydantic_rpc-0.7.0/examples/fooservice_pb2_grpc.py +0 -106
- pydantic_rpc-0.7.0/examples/google/protobuf/duration.proto +0 -115
- pydantic_rpc-0.7.0/examples/google/protobuf/timestamp.proto +0 -144
- pydantic_rpc-0.7.0/examples/greeter.proto +0 -35
- pydantic_rpc-0.7.0/examples/greeter_client.py +0 -14
- pydantic_rpc-0.7.0/examples/greeter_connecpy.py +0 -124
- pydantic_rpc-0.7.0/examples/greeter_connecpy_client.py +0 -42
- pydantic_rpc-0.7.0/examples/greeter_pb2.py +0 -37
- pydantic_rpc-0.7.0/examples/greeter_pb2.pyi +0 -17
- pydantic_rpc-0.7.0/examples/greeter_pb2_grpc.py +0 -106
- pydantic_rpc-0.7.0/examples/greeter_sonora_client.py +0 -8
- pydantic_rpc-0.7.0/examples/greeting.py +0 -45
- pydantic_rpc-0.7.0/examples/greeting_asgi.py +0 -55
- pydantic_rpc-0.7.0/examples/greeting_connecpy.py +0 -44
- pydantic_rpc-0.7.0/examples/greeting_using_exsiting_pb2_modules.py +0 -23
- pydantic_rpc-0.7.0/examples/greeting_wsgi.py +0 -63
- pydantic_rpc-0.7.0/examples/mcp_debug_example.py +0 -74
- pydantic_rpc-0.7.0/examples/mcp_example.py +0 -129
- pydantic_rpc-0.7.0/examples/mcp_http_example.py +0 -125
- pydantic_rpc-0.7.0/examples/mcp_simple_calculator.py +0 -45
- pydantic_rpc-0.7.0/examples/olympicsagent.proto +0 -40
- pydantic_rpc-0.7.0/examples/olympicsagent_pb2.py +0 -41
- pydantic_rpc-0.7.0/examples/olympicsagent_pb2.pyi +0 -37
- pydantic_rpc-0.7.0/examples/olympicsagent_pb2_grpc.py +0 -155
- pydantic_rpc-0.7.0/examples/olympicslocationagent.proto +0 -24
- pydantic_rpc-0.7.0/examples/olympicslocationagent_connecpy.py +0 -113
- pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2.py +0 -39
- pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2.pyi +0 -21
- pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2_grpc.py +0 -125
- pydantic_rpc-0.7.0/tests/asyncechoservice.proto +0 -33
- pydantic_rpc-0.7.0/tests/echoservice.proto +0 -33
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithduration.proto +0 -14
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithtimestamp.proto +0 -14
- pydantic_rpc-0.7.0/tests/google_protobuf/test_google_protobuf.py +0 -41
- pydantic_rpc-0.7.0/tests/greeterwithduration.proto +0 -14
- pydantic_rpc-0.7.0/tests/greeterwithtimestamp.proto +0 -14
- pydantic_rpc-0.7.0/tests/test_apps.py +0 -378
- pydantic_rpc-0.7.0/tests/test_conversion.py +0 -1126
- pydantic_rpc-0.7.0/tests/test_mcp.py +0 -181
- pydantic_rpc-0.7.0/tests/test_utils.py +0 -511
- pydantic_rpc-0.7.0/uv.lock +0 -1548
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/mcp/__init__.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/mcp/converter.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/mcp/exporter.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.9.0}/src/pydantic_rpc/py.typed +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pydantic-rpc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
5
|
Author: Yasushi Itoh
|
|
6
|
-
|
|
7
|
-
Requires-
|
|
8
|
-
Requires-Dist: annotated-types>=0.5.0
|
|
9
|
-
Requires-Dist: connecpy==2.0.0
|
|
10
|
-
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
11
|
-
Requires-Dist: grpcio-reflection>=1.56.2
|
|
12
|
-
Requires-Dist: grpcio-tools>=1.56.2
|
|
6
|
+
Requires-Dist: annotated-types==0.7.0
|
|
7
|
+
Requires-Dist: pydantic>=2.1.1
|
|
13
8
|
Requires-Dist: grpcio>=1.56.2
|
|
9
|
+
Requires-Dist: grpcio-tools>=1.56.2
|
|
10
|
+
Requires-Dist: grpcio-reflection>=1.56.2
|
|
11
|
+
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
12
|
+
Requires-Dist: connecpy>=2.2.0
|
|
14
13
|
Requires-Dist: mcp>=1.9.4
|
|
15
|
-
Requires-Dist: pydantic>=2.1.1
|
|
16
|
-
Requires-Dist: sonora>=0.2.3
|
|
17
14
|
Requires-Dist: starlette>=0.27.0
|
|
15
|
+
Requires-Python: >=3.11
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
19
17
|
|
|
20
18
|
# 🚀 PydanticRPC
|
|
@@ -77,7 +75,7 @@ import asyncio
|
|
|
77
75
|
from openai import AsyncOpenAI
|
|
78
76
|
from pydantic_ai import Agent
|
|
79
77
|
from pydantic_ai.models.openai import OpenAIModel
|
|
80
|
-
from pydantic_rpc import
|
|
78
|
+
from pydantic_rpc import ASGIApp, Message
|
|
81
79
|
|
|
82
80
|
|
|
83
81
|
class CityLocation(Message):
|
|
@@ -108,7 +106,7 @@ class OlympicsLocationAgent:
|
|
|
108
106
|
result = await self._agent.run(req.prompt())
|
|
109
107
|
return result.data
|
|
110
108
|
|
|
111
|
-
app =
|
|
109
|
+
app = ASGIApp()
|
|
112
110
|
app.mount(OlympicsLocationAgent())
|
|
113
111
|
|
|
114
112
|
```
|
|
@@ -124,10 +122,10 @@ app.mount(OlympicsLocationAgent())
|
|
|
124
122
|
- 💚 **Health Checking:** Built-in support for gRPC health checks using `grpc_health.v1`.
|
|
125
123
|
- 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
|
|
126
124
|
- ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
|
|
127
|
-
- **For gRPC-Web:**
|
|
128
|
-
- 🌐 **WSGI/ASGI Support:** Create gRPC-Web services that can run as WSGI or ASGI applications powered by `Sonora`.
|
|
129
125
|
- **For Connect-RPC:**
|
|
130
|
-
- 🌐 **
|
|
126
|
+
- 🌐 **Full Protocol Support:** Native Connect-RPC support via `Connecpy` v2.2.0+
|
|
127
|
+
- 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
|
|
128
|
+
- 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
|
|
131
129
|
- 🛠️ **Pre-generated Protobuf Files and Code:** Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTIC_RPC_SKIP_GENERATION), you can skip runtime generation.
|
|
132
130
|
- 🤖 **MCP (Model Context Protocol) Support:** Expose your services as tools for AI assistants using the official MCP SDK, supporting both stdio and HTTP/SSE transports.
|
|
133
131
|
|
|
@@ -141,7 +139,11 @@ pip install pydantic-rpc
|
|
|
141
139
|
|
|
142
140
|
## 🚀 Getting Started
|
|
143
141
|
|
|
144
|
-
|
|
142
|
+
PydanticRPC supports two main protocols:
|
|
143
|
+
- **gRPC**: Traditional gRPC services with `Server` and `AsyncIOServer`
|
|
144
|
+
- **Connect-RPC**: Modern HTTP-based RPC with `ASGIApp` and `WSGIApp`
|
|
145
|
+
|
|
146
|
+
### 🔧 Synchronous gRPC Service Example
|
|
145
147
|
|
|
146
148
|
```python
|
|
147
149
|
from pydantic_rpc import Server, Message
|
|
@@ -162,7 +164,7 @@ if __name__ == "__main__":
|
|
|
162
164
|
server.run(Greeter())
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
### ⚙️ Asynchronous Service Example
|
|
167
|
+
### ⚙️ Asynchronous gRPC Service Example
|
|
166
168
|
|
|
167
169
|
```python
|
|
168
170
|
import asyncio
|
|
@@ -195,7 +197,7 @@ if __name__ == "__main__":
|
|
|
195
197
|
|
|
196
198
|
The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
|
|
197
199
|
|
|
198
|
-
### 🌐 ASGI Application Example
|
|
200
|
+
### 🌐 Connect-RPC ASGI Application Example
|
|
199
201
|
|
|
200
202
|
```python
|
|
201
203
|
from pydantic_rpc import ASGIApp, Message
|
|
@@ -207,27 +209,17 @@ class HelloReply(Message):
|
|
|
207
209
|
message: str
|
|
208
210
|
|
|
209
211
|
class Greeter:
|
|
210
|
-
def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
212
|
+
async def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
211
213
|
return HelloReply(message=f"Hello, {request.name}!")
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
async def app(scope, receive, send):
|
|
215
|
-
"""ASGI application.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
scope (dict): The ASGI scope.
|
|
219
|
-
receive (callable): The receive function.
|
|
220
|
-
send (callable): The send function.
|
|
221
|
-
"""
|
|
222
|
-
pass
|
|
223
|
-
|
|
224
|
-
# Please note that `app` is any ASGI application, such as FastAPI or Starlette.
|
|
225
|
-
|
|
226
|
-
app = ASGIApp(app)
|
|
215
|
+
app = ASGIApp()
|
|
227
216
|
app.mount(Greeter())
|
|
217
|
+
|
|
218
|
+
# Run with uvicorn:
|
|
219
|
+
# uvicorn script:app --host 0.0.0.0 --port 8000
|
|
228
220
|
```
|
|
229
221
|
|
|
230
|
-
### 🌐 WSGI Application Example
|
|
222
|
+
### 🌐 Connect-RPC WSGI Application Example
|
|
231
223
|
|
|
232
224
|
```python
|
|
233
225
|
from pydantic_rpc import WSGIApp, Message
|
|
@@ -242,31 +234,69 @@ class Greeter:
|
|
|
242
234
|
def say_hello(self, request: HelloRequest) -> HelloReply:
|
|
243
235
|
return HelloReply(message=f"Hello, {request.name}!")
|
|
244
236
|
|
|
245
|
-
|
|
246
|
-
"""WSGI application.
|
|
247
|
-
|
|
248
|
-
Args:
|
|
249
|
-
environ (dict): The WSGI environment.
|
|
250
|
-
start_response (callable): The start_response function.
|
|
251
|
-
"""
|
|
252
|
-
pass
|
|
253
|
-
|
|
254
|
-
# Please note that `app` is any WSGI application, such as Flask or Django.
|
|
255
|
-
|
|
256
|
-
app = WSGIApp(app)
|
|
237
|
+
app = WSGIApp()
|
|
257
238
|
app.mount(Greeter())
|
|
239
|
+
|
|
240
|
+
# Run with gunicorn:
|
|
241
|
+
# gunicorn script:app
|
|
258
242
|
```
|
|
259
243
|
|
|
260
|
-
### 🏆
|
|
244
|
+
### 🏆 Connect-RPC with Streaming Example
|
|
261
245
|
|
|
262
|
-
PydanticRPC
|
|
246
|
+
PydanticRPC provides native Connect-RPC support via Connecpy v2.2.0+, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
|
|
263
247
|
|
|
264
248
|
```bash
|
|
265
|
-
|
|
249
|
+
# Run with uvicorn
|
|
250
|
+
uv run uvicorn greeting_asgi:app --port 3000
|
|
251
|
+
|
|
252
|
+
# Or run streaming example
|
|
253
|
+
uv run python examples/streaming_connecpy.py
|
|
266
254
|
```
|
|
267
255
|
|
|
268
256
|
This will launch a Connecpy-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
|
|
269
257
|
|
|
258
|
+
#### Streaming Support with Connecpy
|
|
259
|
+
|
|
260
|
+
Connecpy v2.2.0 provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from typing import AsyncIterator
|
|
264
|
+
from pydantic_rpc import ASGIApp, Message
|
|
265
|
+
|
|
266
|
+
class StreamRequest(Message):
|
|
267
|
+
text: str
|
|
268
|
+
count: int
|
|
269
|
+
|
|
270
|
+
class StreamResponse(Message):
|
|
271
|
+
text: str
|
|
272
|
+
index: int
|
|
273
|
+
|
|
274
|
+
class StreamingService:
|
|
275
|
+
# Server streaming
|
|
276
|
+
async def server_stream(self, request: StreamRequest) -> AsyncIterator[StreamResponse]:
|
|
277
|
+
for i in range(request.count):
|
|
278
|
+
yield StreamResponse(text=f"{request.text}_{i}", index=i)
|
|
279
|
+
|
|
280
|
+
# Client streaming
|
|
281
|
+
async def client_stream(self, requests: AsyncIterator[StreamRequest]) -> StreamResponse:
|
|
282
|
+
texts = []
|
|
283
|
+
async for req in requests:
|
|
284
|
+
texts.append(req.text)
|
|
285
|
+
return StreamResponse(text=" ".join(texts), index=len(texts))
|
|
286
|
+
|
|
287
|
+
# Bidirectional streaming
|
|
288
|
+
async def bidi_stream(
|
|
289
|
+
self, requests: AsyncIterator[StreamRequest]
|
|
290
|
+
) -> AsyncIterator[StreamResponse]:
|
|
291
|
+
idx = 0
|
|
292
|
+
async for req in requests:
|
|
293
|
+
yield StreamResponse(text=f"Echo: {req.text}", index=idx)
|
|
294
|
+
idx += 1
|
|
295
|
+
|
|
296
|
+
app = ASGIApp()
|
|
297
|
+
app.mount(StreamingService())
|
|
298
|
+
```
|
|
299
|
+
|
|
270
300
|
> [!NOTE]
|
|
271
301
|
> Please install `protoc-gen-connecpy` to run the Connecpy example.
|
|
272
302
|
>
|
|
@@ -303,9 +333,9 @@ export PYDANTIC_RPC_RESERVED_FIELDS=1
|
|
|
303
333
|
|
|
304
334
|
## 💎 Advanced Features
|
|
305
335
|
|
|
306
|
-
### 🌊 Response Streaming
|
|
307
|
-
PydanticRPC supports streaming responses
|
|
308
|
-
If a service class method
|
|
336
|
+
### 🌊 Response Streaming (gRPC)
|
|
337
|
+
PydanticRPC supports streaming responses for both gRPC and Connect-RPC services.
|
|
338
|
+
If a service class method's return type is `typing.AsyncIterator[T]`, the method is considered a streaming method.
|
|
309
339
|
|
|
310
340
|
|
|
311
341
|
Please see the sample code below:
|
|
@@ -674,9 +704,207 @@ buf: * (#2) Call complete
|
|
|
674
704
|
%
|
|
675
705
|
```
|
|
676
706
|
|
|
707
|
+
### 🪶 Empty Messages
|
|
708
|
+
|
|
709
|
+
Empty request/response messages are automatically mapped to `google.protobuf.Empty`:
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class EmptyRequest(Message):
|
|
716
|
+
pass # Automatically uses google.protobuf.Empty
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class GreetingResponse(Message):
|
|
720
|
+
message: str
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class GreetingService:
|
|
724
|
+
async def say_hello(self, request: EmptyRequest) -> GreetingResponse:
|
|
725
|
+
return GreetingResponse(message="Hello!")
|
|
726
|
+
|
|
727
|
+
async def get_default_greeting(self) -> GreetingResponse:
|
|
728
|
+
# Method with no request parameter (implicitly empty)
|
|
729
|
+
return GreetingResponse(message="Hello, World!")
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### 🎨 Custom Serialization
|
|
733
|
+
|
|
734
|
+
Pydantic's serialization decorators are fully supported:
|
|
735
|
+
|
|
736
|
+
```python
|
|
737
|
+
from typing import Any
|
|
738
|
+
from pydantic import field_serializer, model_serializer
|
|
739
|
+
from pydantic_rpc import Message
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class UserMessage(Message):
|
|
743
|
+
name: str
|
|
744
|
+
age: int
|
|
745
|
+
|
|
746
|
+
@field_serializer('name')
|
|
747
|
+
def serialize_name(self, name: str) -> str:
|
|
748
|
+
"""Always uppercase the name when serializing."""
|
|
749
|
+
return name.upper()
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class ComplexMessage(Message):
|
|
753
|
+
value: int
|
|
754
|
+
multiplier: int
|
|
755
|
+
|
|
756
|
+
@model_serializer
|
|
757
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
758
|
+
"""Custom serialization with computed fields."""
|
|
759
|
+
return {
|
|
760
|
+
'value': self.value,
|
|
761
|
+
'multiplier': self.multiplier,
|
|
762
|
+
'result': self.value * self.multiplier # Computed field
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
The serializers are automatically applied when converting between Pydantic models and protobuf messages.
|
|
767
|
+
|
|
768
|
+
#### ⚠️ Limitations and Considerations
|
|
769
|
+
|
|
770
|
+
**1. Nested Message serializers are now supported (v0.8.0+)**
|
|
771
|
+
```python
|
|
772
|
+
class Address(Message):
|
|
773
|
+
city: str
|
|
774
|
+
|
|
775
|
+
@field_serializer("city")
|
|
776
|
+
def serialize_city(self, city: str) -> str:
|
|
777
|
+
return city.upper()
|
|
778
|
+
|
|
779
|
+
class User(Message):
|
|
780
|
+
name: str
|
|
781
|
+
address: Address # ← Address's serializers ARE applied with DEEP strategy
|
|
782
|
+
|
|
783
|
+
@field_serializer("name")
|
|
784
|
+
def serialize_name(self, name: str) -> str:
|
|
785
|
+
return name.upper() # ← This IS applied
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Serializer Strategy Control:**
|
|
789
|
+
You can control how nested serializers are applied via environment variable:
|
|
790
|
+
```bash
|
|
791
|
+
# Apply serializers at all nesting levels (default)
|
|
792
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=deep
|
|
793
|
+
|
|
794
|
+
# Apply only top-level serializers
|
|
795
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=shallow
|
|
796
|
+
|
|
797
|
+
# Disable all serializers
|
|
798
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=none
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Performance Impact:**
|
|
802
|
+
- DEEP strategy: ~4% overhead for simple nested structures
|
|
803
|
+
- SHALLOW strategy: ~2% overhead (only top-level)
|
|
804
|
+
- NONE strategy: No overhead (serializers disabled)
|
|
805
|
+
|
|
806
|
+
**2. New fields added by serializers are ignored**
|
|
807
|
+
```python
|
|
808
|
+
class ComplexMessage(Message):
|
|
809
|
+
value: int
|
|
810
|
+
multiplier: int
|
|
811
|
+
|
|
812
|
+
@model_serializer
|
|
813
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
814
|
+
return {
|
|
815
|
+
"value": self.value,
|
|
816
|
+
"multiplier": self.multiplier,
|
|
817
|
+
"result": self.value * self.multiplier # ← Won't appear in protobuf
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
**Problem**: The `result` field doesn't exist in the Message definition, so it's not in the protobuf schema.
|
|
821
|
+
|
|
822
|
+
**3. Type must remain consistent**
|
|
823
|
+
```python
|
|
824
|
+
class BadExample(Message):
|
|
825
|
+
number: int
|
|
826
|
+
|
|
827
|
+
@field_serializer("number")
|
|
828
|
+
def serialize_number(self, number: int) -> str: # ❌ int → str
|
|
829
|
+
return str(number) # This will cause issues
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**4. Union/Optional fields have limited support**
|
|
833
|
+
```python
|
|
834
|
+
class UnionExample(Message):
|
|
835
|
+
data: str | int | None # Union type
|
|
836
|
+
|
|
837
|
+
@field_serializer("data")
|
|
838
|
+
def serialize_data(self, data: str | int | None) -> str | int | None:
|
|
839
|
+
# Serializer may not be applied to Union types
|
|
840
|
+
return data
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**5. Errors fail silently with fallback**
|
|
844
|
+
```python
|
|
845
|
+
class RiskyMessage(Message):
|
|
846
|
+
value: int
|
|
847
|
+
|
|
848
|
+
@field_serializer("value")
|
|
849
|
+
def serialize_value(self, value: int) -> int:
|
|
850
|
+
if value == 0:
|
|
851
|
+
raise ValueError("Cannot serialize zero")
|
|
852
|
+
return value * 2
|
|
853
|
+
|
|
854
|
+
# If error occurs, original value is used (silent fallback)
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**6. Circular references are handled gracefully**
|
|
858
|
+
```python
|
|
859
|
+
class Node(Message):
|
|
860
|
+
value: str
|
|
861
|
+
child: "Node | None" = None
|
|
862
|
+
|
|
863
|
+
@field_serializer("value")
|
|
864
|
+
def serialize_value(self, v: str) -> str:
|
|
865
|
+
return v.upper()
|
|
866
|
+
|
|
867
|
+
# Circular references are detected and prevented
|
|
868
|
+
node1 = Node(value="first")
|
|
869
|
+
node2 = Node(value="second")
|
|
870
|
+
node1.child = node2
|
|
871
|
+
node2.child = node1 # Circular reference
|
|
872
|
+
|
|
873
|
+
# When converting to protobuf:
|
|
874
|
+
# - Circular references are detected
|
|
875
|
+
# - Empty proto is returned for repeated objects
|
|
876
|
+
# - No infinite recursion occurs
|
|
877
|
+
# Note: Pydantic's model_dump() will fail on circular refs,
|
|
878
|
+
# so serializers won't be applied in this case
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**✅ Recommended Usage:**
|
|
882
|
+
```python
|
|
883
|
+
class GoodMessage(Message):
|
|
884
|
+
# Use with primitive types
|
|
885
|
+
name: str
|
|
886
|
+
age: int
|
|
887
|
+
|
|
888
|
+
@field_serializer("name")
|
|
889
|
+
def normalize_name(self, name: str) -> str:
|
|
890
|
+
return name.strip().title() # Normalization
|
|
891
|
+
|
|
892
|
+
@field_serializer("age")
|
|
893
|
+
def clamp_age(self, age: int) -> int:
|
|
894
|
+
return max(0, min(age, 150)) # Range limiting
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**Best Practices:**
|
|
898
|
+
- Use serializers primarily for primitive types (str, int, float, bool)
|
|
899
|
+
- Keep type consistency (int → int, str → str)
|
|
900
|
+
- Avoid complex transformations or side effects
|
|
901
|
+
- Test error cases thoroughly
|
|
902
|
+
- Be aware that errors fail silently
|
|
903
|
+
|
|
677
904
|
### 🔗 Multiple Services with Custom Interceptors
|
|
905
|
+
>>>>>>> origin/main
|
|
678
906
|
|
|
679
|
-
PydanticRPC supports defining and running multiple services in a single server:
|
|
907
|
+
PydanticRPC supports defining and running multiple gRPC services in a single server:
|
|
680
908
|
|
|
681
909
|
```python
|
|
682
910
|
from datetime import datetime
|
|
@@ -807,11 +1035,11 @@ Any MCP-compatible client can connect to your service. For example, to configure
|
|
|
807
1035
|
MCP can also be mounted to existing ASGI applications:
|
|
808
1036
|
|
|
809
1037
|
```python
|
|
810
|
-
from pydantic_rpc import
|
|
1038
|
+
from pydantic_rpc import ASGIApp
|
|
811
1039
|
from pydantic_rpc.mcp import MCPExporter
|
|
812
1040
|
|
|
813
1041
|
# Create Connect-RPC ASGI app
|
|
814
|
-
app =
|
|
1042
|
+
app = ASGIApp()
|
|
815
1043
|
app.mount(MathService())
|
|
816
1044
|
|
|
817
1045
|
# Add MCP support via HTTP/SSE
|
|
@@ -905,8 +1133,8 @@ This approach works because protobuf allows message types within `oneof` fields,
|
|
|
905
1133
|
- [x] unary-stream
|
|
906
1134
|
- [x] stream-unary
|
|
907
1135
|
- [x] stream-stream
|
|
908
|
-
- [
|
|
909
|
-
- [
|
|
1136
|
+
- [x] Empty Message Support (automatic google.protobuf.Empty)
|
|
1137
|
+
- [x] Pydantic Serializer Support (@model_serializer, @field_serializer)
|
|
910
1138
|
- [ ] Custom Health Check Support
|
|
911
1139
|
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
912
1140
|
- [ ] Add more examples
|