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.
Files changed (69) hide show
  1. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/PKG-INFO +208 -12
  2. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/README.md +199 -2
  3. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/pyproject.toml +3 -6
  4. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/core.py +414 -44
  5. pydantic_rpc-0.7.0/.github/workflows/release.yml +0 -35
  6. pydantic_rpc-0.7.0/.github/workflows/test.yml +0 -47
  7. pydantic_rpc-0.7.0/.gitignore +0 -24
  8. pydantic_rpc-0.7.0/.python-version +0 -1
  9. pydantic_rpc-0.7.0/LICENSE +0 -21
  10. pydantic_rpc-0.7.0/docs/mcp.md +0 -95
  11. pydantic_rpc-0.7.0/examples/README.md +0 -152
  12. pydantic_rpc-0.7.0/examples/agent_aio_grpc.py +0 -65
  13. pydantic_rpc-0.7.0/examples/agent_connecpy.py +0 -56
  14. pydantic_rpc-0.7.0/examples/asyncio_greeting.py +0 -22
  15. pydantic_rpc-0.7.0/examples/barservice.proto +0 -17
  16. pydantic_rpc-0.7.0/examples/barservice_pb2.py +0 -37
  17. pydantic_rpc-0.7.0/examples/barservice_pb2.pyi +0 -18
  18. pydantic_rpc-0.7.0/examples/barservice_pb2_grpc.py +0 -106
  19. pydantic_rpc-0.7.0/examples/foobar.py +0 -76
  20. pydantic_rpc-0.7.0/examples/foobar_client.py +0 -20
  21. pydantic_rpc-0.7.0/examples/fooservice.proto +0 -21
  22. pydantic_rpc-0.7.0/examples/fooservice_pb2.py +0 -45
  23. pydantic_rpc-0.7.0/examples/fooservice_pb2.pyi +0 -56
  24. pydantic_rpc-0.7.0/examples/fooservice_pb2_grpc.py +0 -106
  25. pydantic_rpc-0.7.0/examples/google/protobuf/duration.proto +0 -115
  26. pydantic_rpc-0.7.0/examples/google/protobuf/timestamp.proto +0 -144
  27. pydantic_rpc-0.7.0/examples/greeter.proto +0 -35
  28. pydantic_rpc-0.7.0/examples/greeter_client.py +0 -14
  29. pydantic_rpc-0.7.0/examples/greeter_connecpy.py +0 -124
  30. pydantic_rpc-0.7.0/examples/greeter_connecpy_client.py +0 -42
  31. pydantic_rpc-0.7.0/examples/greeter_pb2.py +0 -37
  32. pydantic_rpc-0.7.0/examples/greeter_pb2.pyi +0 -17
  33. pydantic_rpc-0.7.0/examples/greeter_pb2_grpc.py +0 -106
  34. pydantic_rpc-0.7.0/examples/greeter_sonora_client.py +0 -8
  35. pydantic_rpc-0.7.0/examples/greeting.py +0 -45
  36. pydantic_rpc-0.7.0/examples/greeting_asgi.py +0 -55
  37. pydantic_rpc-0.7.0/examples/greeting_connecpy.py +0 -44
  38. pydantic_rpc-0.7.0/examples/greeting_using_exsiting_pb2_modules.py +0 -23
  39. pydantic_rpc-0.7.0/examples/greeting_wsgi.py +0 -63
  40. pydantic_rpc-0.7.0/examples/mcp_debug_example.py +0 -74
  41. pydantic_rpc-0.7.0/examples/mcp_example.py +0 -129
  42. pydantic_rpc-0.7.0/examples/mcp_http_example.py +0 -125
  43. pydantic_rpc-0.7.0/examples/mcp_simple_calculator.py +0 -45
  44. pydantic_rpc-0.7.0/examples/olympicsagent.proto +0 -40
  45. pydantic_rpc-0.7.0/examples/olympicsagent_pb2.py +0 -41
  46. pydantic_rpc-0.7.0/examples/olympicsagent_pb2.pyi +0 -37
  47. pydantic_rpc-0.7.0/examples/olympicsagent_pb2_grpc.py +0 -155
  48. pydantic_rpc-0.7.0/examples/olympicslocationagent.proto +0 -24
  49. pydantic_rpc-0.7.0/examples/olympicslocationagent_connecpy.py +0 -113
  50. pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2.py +0 -39
  51. pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2.pyi +0 -21
  52. pydantic_rpc-0.7.0/examples/olympicslocationagent_pb2_grpc.py +0 -125
  53. pydantic_rpc-0.7.0/tests/asyncechoservice.proto +0 -33
  54. pydantic_rpc-0.7.0/tests/echoservice.proto +0 -33
  55. pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithduration.proto +0 -14
  56. pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithtimestamp.proto +0 -14
  57. pydantic_rpc-0.7.0/tests/google_protobuf/test_google_protobuf.py +0 -41
  58. pydantic_rpc-0.7.0/tests/greeterwithduration.proto +0 -14
  59. pydantic_rpc-0.7.0/tests/greeterwithtimestamp.proto +0 -14
  60. pydantic_rpc-0.7.0/tests/test_apps.py +0 -378
  61. pydantic_rpc-0.7.0/tests/test_conversion.py +0 -1126
  62. pydantic_rpc-0.7.0/tests/test_mcp.py +0 -181
  63. pydantic_rpc-0.7.0/tests/test_utils.py +0 -511
  64. pydantic_rpc-0.7.0/uv.lock +0 -1548
  65. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/__init__.py +0 -0
  66. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/__init__.py +0 -0
  67. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/converter.py +0 -0
  68. {pydantic_rpc-0.7.0 → pydantic_rpc-0.8.0}/src/pydantic_rpc/mcp/exporter.py +0 -0
  69. {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.4
1
+ Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.7.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
- - [ ] Betterproto Support
909
- - [ ] Sonora-connect Support
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
- - [ ] Betterproto Support
890
- - [ ] Sonora-connect Support
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.7.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 = ["hatchling"]
28
- build-backend = "hatchling.build"
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",