pydantic-rpc 0.7.0__tar.gz → 0.8.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.8.0}/PKG-INFO +208 -12
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/README.md +199 -2
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/pyproject.toml +3 -6
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/core.py +414 -44
- 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.8.0}/src/pydantic_rpc/__init__.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/__init__.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/converter.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/exporter.py +0 -0
- {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/py.typed +0 -0
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pydantic-rpc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
5
|
Author: Yasushi Itoh
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
6
|
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
|
|
13
|
-
Requires-Dist: grpcio>=1.56.2
|
|
14
|
-
Requires-Dist: mcp>=1.9.4
|
|
15
7
|
Requires-Dist: pydantic>=2.1.1
|
|
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
|
|
16
12
|
Requires-Dist: sonora>=0.2.3
|
|
13
|
+
Requires-Dist: connecpy==2.0.0
|
|
14
|
+
Requires-Dist: mcp>=1.9.4
|
|
17
15
|
Requires-Dist: starlette>=0.27.0
|
|
16
|
+
Requires-Python: >=3.11
|
|
18
17
|
Description-Content-Type: text/markdown
|
|
19
18
|
|
|
20
19
|
# 🚀 PydanticRPC
|
|
@@ -674,6 +673,203 @@ buf: * (#2) Call complete
|
|
|
674
673
|
%
|
|
675
674
|
```
|
|
676
675
|
|
|
676
|
+
### 🪶 Empty Messages
|
|
677
|
+
|
|
678
|
+
Empty request/response messages are automatically mapped to `google.protobuf.Empty`:
|
|
679
|
+
|
|
680
|
+
```python
|
|
681
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class EmptyRequest(Message):
|
|
685
|
+
pass # Automatically uses google.protobuf.Empty
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class GreetingResponse(Message):
|
|
689
|
+
message: str
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class GreetingService:
|
|
693
|
+
async def say_hello(self, request: EmptyRequest) -> GreetingResponse:
|
|
694
|
+
return GreetingResponse(message="Hello!")
|
|
695
|
+
|
|
696
|
+
async def get_default_greeting(self) -> GreetingResponse:
|
|
697
|
+
# Method with no request parameter (implicitly empty)
|
|
698
|
+
return GreetingResponse(message="Hello, World!")
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 🎨 Custom Serialization
|
|
702
|
+
|
|
703
|
+
Pydantic's serialization decorators are fully supported:
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
from typing import Any
|
|
707
|
+
from pydantic import field_serializer, model_serializer
|
|
708
|
+
from pydantic_rpc import Message
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class UserMessage(Message):
|
|
712
|
+
name: str
|
|
713
|
+
age: int
|
|
714
|
+
|
|
715
|
+
@field_serializer('name')
|
|
716
|
+
def serialize_name(self, name: str) -> str:
|
|
717
|
+
"""Always uppercase the name when serializing."""
|
|
718
|
+
return name.upper()
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class ComplexMessage(Message):
|
|
722
|
+
value: int
|
|
723
|
+
multiplier: int
|
|
724
|
+
|
|
725
|
+
@model_serializer
|
|
726
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
727
|
+
"""Custom serialization with computed fields."""
|
|
728
|
+
return {
|
|
729
|
+
'value': self.value,
|
|
730
|
+
'multiplier': self.multiplier,
|
|
731
|
+
'result': self.value * self.multiplier # Computed field
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
The serializers are automatically applied when converting between Pydantic models and protobuf messages.
|
|
736
|
+
|
|
737
|
+
#### ⚠️ Limitations and Considerations
|
|
738
|
+
|
|
739
|
+
**1. Nested Message serializers are now supported (v0.8.0+)**
|
|
740
|
+
```python
|
|
741
|
+
class Address(Message):
|
|
742
|
+
city: str
|
|
743
|
+
|
|
744
|
+
@field_serializer("city")
|
|
745
|
+
def serialize_city(self, city: str) -> str:
|
|
746
|
+
return city.upper()
|
|
747
|
+
|
|
748
|
+
class User(Message):
|
|
749
|
+
name: str
|
|
750
|
+
address: Address # ← Address's serializers ARE applied with DEEP strategy
|
|
751
|
+
|
|
752
|
+
@field_serializer("name")
|
|
753
|
+
def serialize_name(self, name: str) -> str:
|
|
754
|
+
return name.upper() # ← This IS applied
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Serializer Strategy Control:**
|
|
758
|
+
You can control how nested serializers are applied via environment variable:
|
|
759
|
+
```bash
|
|
760
|
+
# Apply serializers at all nesting levels (default)
|
|
761
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=deep
|
|
762
|
+
|
|
763
|
+
# Apply only top-level serializers
|
|
764
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=shallow
|
|
765
|
+
|
|
766
|
+
# Disable all serializers
|
|
767
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=none
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
**Performance Impact:**
|
|
771
|
+
- DEEP strategy: ~4% overhead for simple nested structures
|
|
772
|
+
- SHALLOW strategy: ~2% overhead (only top-level)
|
|
773
|
+
- NONE strategy: No overhead (serializers disabled)
|
|
774
|
+
|
|
775
|
+
**2. New fields added by serializers are ignored**
|
|
776
|
+
```python
|
|
777
|
+
class ComplexMessage(Message):
|
|
778
|
+
value: int
|
|
779
|
+
multiplier: int
|
|
780
|
+
|
|
781
|
+
@model_serializer
|
|
782
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
783
|
+
return {
|
|
784
|
+
"value": self.value,
|
|
785
|
+
"multiplier": self.multiplier,
|
|
786
|
+
"result": self.value * self.multiplier # ← Won't appear in protobuf
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
**Problem**: The `result` field doesn't exist in the Message definition, so it's not in the protobuf schema.
|
|
790
|
+
|
|
791
|
+
**3. Type must remain consistent**
|
|
792
|
+
```python
|
|
793
|
+
class BadExample(Message):
|
|
794
|
+
number: int
|
|
795
|
+
|
|
796
|
+
@field_serializer("number")
|
|
797
|
+
def serialize_number(self, number: int) -> str: # ❌ int → str
|
|
798
|
+
return str(number) # This will cause issues
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**4. Union/Optional fields have limited support**
|
|
802
|
+
```python
|
|
803
|
+
class UnionExample(Message):
|
|
804
|
+
data: str | int | None # Union type
|
|
805
|
+
|
|
806
|
+
@field_serializer("data")
|
|
807
|
+
def serialize_data(self, data: str | int | None) -> str | int | None:
|
|
808
|
+
# Serializer may not be applied to Union types
|
|
809
|
+
return data
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**5. Errors fail silently with fallback**
|
|
813
|
+
```python
|
|
814
|
+
class RiskyMessage(Message):
|
|
815
|
+
value: int
|
|
816
|
+
|
|
817
|
+
@field_serializer("value")
|
|
818
|
+
def serialize_value(self, value: int) -> int:
|
|
819
|
+
if value == 0:
|
|
820
|
+
raise ValueError("Cannot serialize zero")
|
|
821
|
+
return value * 2
|
|
822
|
+
|
|
823
|
+
# If error occurs, original value is used (silent fallback)
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
**6. Circular references are handled gracefully**
|
|
827
|
+
```python
|
|
828
|
+
class Node(Message):
|
|
829
|
+
value: str
|
|
830
|
+
child: "Node | None" = None
|
|
831
|
+
|
|
832
|
+
@field_serializer("value")
|
|
833
|
+
def serialize_value(self, v: str) -> str:
|
|
834
|
+
return v.upper()
|
|
835
|
+
|
|
836
|
+
# Circular references are detected and prevented
|
|
837
|
+
node1 = Node(value="first")
|
|
838
|
+
node2 = Node(value="second")
|
|
839
|
+
node1.child = node2
|
|
840
|
+
node2.child = node1 # Circular reference
|
|
841
|
+
|
|
842
|
+
# When converting to protobuf:
|
|
843
|
+
# - Circular references are detected
|
|
844
|
+
# - Empty proto is returned for repeated objects
|
|
845
|
+
# - No infinite recursion occurs
|
|
846
|
+
# Note: Pydantic's model_dump() will fail on circular refs,
|
|
847
|
+
# so serializers won't be applied in this case
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**✅ Recommended Usage:**
|
|
851
|
+
```python
|
|
852
|
+
class GoodMessage(Message):
|
|
853
|
+
# Use with primitive types
|
|
854
|
+
name: str
|
|
855
|
+
age: int
|
|
856
|
+
|
|
857
|
+
@field_serializer("name")
|
|
858
|
+
def normalize_name(self, name: str) -> str:
|
|
859
|
+
return name.strip().title() # Normalization
|
|
860
|
+
|
|
861
|
+
@field_serializer("age")
|
|
862
|
+
def clamp_age(self, age: int) -> int:
|
|
863
|
+
return max(0, min(age, 150)) # Range limiting
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Best Practices:**
|
|
867
|
+
- Use serializers primarily for primitive types (str, int, float, bool)
|
|
868
|
+
- Keep type consistency (int → int, str → str)
|
|
869
|
+
- Avoid complex transformations or side effects
|
|
870
|
+
- Test error cases thoroughly
|
|
871
|
+
- Be aware that errors fail silently
|
|
872
|
+
|
|
677
873
|
### 🔗 Multiple Services with Custom Interceptors
|
|
678
874
|
|
|
679
875
|
PydanticRPC supports defining and running multiple services in a single server:
|
|
@@ -905,8 +1101,8 @@ This approach works because protobuf allows message types within `oneof` fields,
|
|
|
905
1101
|
- [x] unary-stream
|
|
906
1102
|
- [x] stream-unary
|
|
907
1103
|
- [x] stream-stream
|
|
908
|
-
- [
|
|
909
|
-
- [
|
|
1104
|
+
- [x] Empty Message Support (automatic google.protobuf.Empty)
|
|
1105
|
+
- [x] Pydantic Serializer Support (@model_serializer, @field_serializer)
|
|
910
1106
|
- [ ] Custom Health Check Support
|
|
911
1107
|
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
912
1108
|
- [ ] Add more examples
|
|
@@ -655,6 +655,203 @@ buf: * (#2) Call complete
|
|
|
655
655
|
%
|
|
656
656
|
```
|
|
657
657
|
|
|
658
|
+
### 🪶 Empty Messages
|
|
659
|
+
|
|
660
|
+
Empty request/response messages are automatically mapped to `google.protobuf.Empty`:
|
|
661
|
+
|
|
662
|
+
```python
|
|
663
|
+
from pydantic_rpc import AsyncIOServer, Message
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class EmptyRequest(Message):
|
|
667
|
+
pass # Automatically uses google.protobuf.Empty
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class GreetingResponse(Message):
|
|
671
|
+
message: str
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class GreetingService:
|
|
675
|
+
async def say_hello(self, request: EmptyRequest) -> GreetingResponse:
|
|
676
|
+
return GreetingResponse(message="Hello!")
|
|
677
|
+
|
|
678
|
+
async def get_default_greeting(self) -> GreetingResponse:
|
|
679
|
+
# Method with no request parameter (implicitly empty)
|
|
680
|
+
return GreetingResponse(message="Hello, World!")
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 🎨 Custom Serialization
|
|
684
|
+
|
|
685
|
+
Pydantic's serialization decorators are fully supported:
|
|
686
|
+
|
|
687
|
+
```python
|
|
688
|
+
from typing import Any
|
|
689
|
+
from pydantic import field_serializer, model_serializer
|
|
690
|
+
from pydantic_rpc import Message
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class UserMessage(Message):
|
|
694
|
+
name: str
|
|
695
|
+
age: int
|
|
696
|
+
|
|
697
|
+
@field_serializer('name')
|
|
698
|
+
def serialize_name(self, name: str) -> str:
|
|
699
|
+
"""Always uppercase the name when serializing."""
|
|
700
|
+
return name.upper()
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class ComplexMessage(Message):
|
|
704
|
+
value: int
|
|
705
|
+
multiplier: int
|
|
706
|
+
|
|
707
|
+
@model_serializer
|
|
708
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
709
|
+
"""Custom serialization with computed fields."""
|
|
710
|
+
return {
|
|
711
|
+
'value': self.value,
|
|
712
|
+
'multiplier': self.multiplier,
|
|
713
|
+
'result': self.value * self.multiplier # Computed field
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
The serializers are automatically applied when converting between Pydantic models and protobuf messages.
|
|
718
|
+
|
|
719
|
+
#### ⚠️ Limitations and Considerations
|
|
720
|
+
|
|
721
|
+
**1. Nested Message serializers are now supported (v0.8.0+)**
|
|
722
|
+
```python
|
|
723
|
+
class Address(Message):
|
|
724
|
+
city: str
|
|
725
|
+
|
|
726
|
+
@field_serializer("city")
|
|
727
|
+
def serialize_city(self, city: str) -> str:
|
|
728
|
+
return city.upper()
|
|
729
|
+
|
|
730
|
+
class User(Message):
|
|
731
|
+
name: str
|
|
732
|
+
address: Address # ← Address's serializers ARE applied with DEEP strategy
|
|
733
|
+
|
|
734
|
+
@field_serializer("name")
|
|
735
|
+
def serialize_name(self, name: str) -> str:
|
|
736
|
+
return name.upper() # ← This IS applied
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Serializer Strategy Control:**
|
|
740
|
+
You can control how nested serializers are applied via environment variable:
|
|
741
|
+
```bash
|
|
742
|
+
# Apply serializers at all nesting levels (default)
|
|
743
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=deep
|
|
744
|
+
|
|
745
|
+
# Apply only top-level serializers
|
|
746
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=shallow
|
|
747
|
+
|
|
748
|
+
# Disable all serializers
|
|
749
|
+
export PYDANTIC_RPC_SERIALIZER_STRATEGY=none
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Performance Impact:**
|
|
753
|
+
- DEEP strategy: ~4% overhead for simple nested structures
|
|
754
|
+
- SHALLOW strategy: ~2% overhead (only top-level)
|
|
755
|
+
- NONE strategy: No overhead (serializers disabled)
|
|
756
|
+
|
|
757
|
+
**2. New fields added by serializers are ignored**
|
|
758
|
+
```python
|
|
759
|
+
class ComplexMessage(Message):
|
|
760
|
+
value: int
|
|
761
|
+
multiplier: int
|
|
762
|
+
|
|
763
|
+
@model_serializer
|
|
764
|
+
def serialize_model(self) -> dict[str, Any]:
|
|
765
|
+
return {
|
|
766
|
+
"value": self.value,
|
|
767
|
+
"multiplier": self.multiplier,
|
|
768
|
+
"result": self.value * self.multiplier # ← Won't appear in protobuf
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
**Problem**: The `result` field doesn't exist in the Message definition, so it's not in the protobuf schema.
|
|
772
|
+
|
|
773
|
+
**3. Type must remain consistent**
|
|
774
|
+
```python
|
|
775
|
+
class BadExample(Message):
|
|
776
|
+
number: int
|
|
777
|
+
|
|
778
|
+
@field_serializer("number")
|
|
779
|
+
def serialize_number(self, number: int) -> str: # ❌ int → str
|
|
780
|
+
return str(number) # This will cause issues
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**4. Union/Optional fields have limited support**
|
|
784
|
+
```python
|
|
785
|
+
class UnionExample(Message):
|
|
786
|
+
data: str | int | None # Union type
|
|
787
|
+
|
|
788
|
+
@field_serializer("data")
|
|
789
|
+
def serialize_data(self, data: str | int | None) -> str | int | None:
|
|
790
|
+
# Serializer may not be applied to Union types
|
|
791
|
+
return data
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**5. Errors fail silently with fallback**
|
|
795
|
+
```python
|
|
796
|
+
class RiskyMessage(Message):
|
|
797
|
+
value: int
|
|
798
|
+
|
|
799
|
+
@field_serializer("value")
|
|
800
|
+
def serialize_value(self, value: int) -> int:
|
|
801
|
+
if value == 0:
|
|
802
|
+
raise ValueError("Cannot serialize zero")
|
|
803
|
+
return value * 2
|
|
804
|
+
|
|
805
|
+
# If error occurs, original value is used (silent fallback)
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**6. Circular references are handled gracefully**
|
|
809
|
+
```python
|
|
810
|
+
class Node(Message):
|
|
811
|
+
value: str
|
|
812
|
+
child: "Node | None" = None
|
|
813
|
+
|
|
814
|
+
@field_serializer("value")
|
|
815
|
+
def serialize_value(self, v: str) -> str:
|
|
816
|
+
return v.upper()
|
|
817
|
+
|
|
818
|
+
# Circular references are detected and prevented
|
|
819
|
+
node1 = Node(value="first")
|
|
820
|
+
node2 = Node(value="second")
|
|
821
|
+
node1.child = node2
|
|
822
|
+
node2.child = node1 # Circular reference
|
|
823
|
+
|
|
824
|
+
# When converting to protobuf:
|
|
825
|
+
# - Circular references are detected
|
|
826
|
+
# - Empty proto is returned for repeated objects
|
|
827
|
+
# - No infinite recursion occurs
|
|
828
|
+
# Note: Pydantic's model_dump() will fail on circular refs,
|
|
829
|
+
# so serializers won't be applied in this case
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**✅ Recommended Usage:**
|
|
833
|
+
```python
|
|
834
|
+
class GoodMessage(Message):
|
|
835
|
+
# Use with primitive types
|
|
836
|
+
name: str
|
|
837
|
+
age: int
|
|
838
|
+
|
|
839
|
+
@field_serializer("name")
|
|
840
|
+
def normalize_name(self, name: str) -> str:
|
|
841
|
+
return name.strip().title() # Normalization
|
|
842
|
+
|
|
843
|
+
@field_serializer("age")
|
|
844
|
+
def clamp_age(self, age: int) -> int:
|
|
845
|
+
return max(0, min(age, 150)) # Range limiting
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**Best Practices:**
|
|
849
|
+
- Use serializers primarily for primitive types (str, int, float, bool)
|
|
850
|
+
- Keep type consistency (int → int, str → str)
|
|
851
|
+
- Avoid complex transformations or side effects
|
|
852
|
+
- Test error cases thoroughly
|
|
853
|
+
- Be aware that errors fail silently
|
|
854
|
+
|
|
658
855
|
### 🔗 Multiple Services with Custom Interceptors
|
|
659
856
|
|
|
660
857
|
PydanticRPC supports defining and running multiple services in a single server:
|
|
@@ -886,8 +1083,8 @@ This approach works because protobuf allows message types within `oneof` fields,
|
|
|
886
1083
|
- [x] unary-stream
|
|
887
1084
|
- [x] stream-unary
|
|
888
1085
|
- [x] stream-stream
|
|
889
|
-
- [
|
|
890
|
-
- [
|
|
1086
|
+
- [x] Empty Message Support (automatic google.protobuf.Empty)
|
|
1087
|
+
- [x] Pydantic Serializer Support (@model_serializer, @field_serializer)
|
|
891
1088
|
- [ ] Custom Health Check Support
|
|
892
1089
|
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
893
1090
|
- [ ] Add more examples
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pydantic-rpc"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "A Python library for building gRPC/ConnectRPC services with Pydantic models."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Yasushi Itoh" }
|
|
@@ -24,8 +24,8 @@ requires-python = ">= 3.11"
|
|
|
24
24
|
pydantic-rpc = "pydantic_rpc.core:main"
|
|
25
25
|
|
|
26
26
|
[build-system]
|
|
27
|
-
requires = ["
|
|
28
|
-
build-backend = "
|
|
27
|
+
requires = ["uv_build>=0.7.21,<0.8.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
29
|
|
|
30
30
|
[tool.uv]
|
|
31
31
|
managed = true
|
|
@@ -37,9 +37,6 @@ dev-dependencies = [
|
|
|
37
37
|
"ruff>=0.9.4",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
[tool.hatch.metadata]
|
|
41
|
-
allow-direct-references = true
|
|
42
|
-
|
|
43
40
|
[tool.pytest.ini_options]
|
|
44
41
|
markers = [
|
|
45
42
|
"asyncio: mark test as asyncio",
|