pydantic-rpc 0.6.1__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 (63) hide show
  1. pydantic_rpc-0.6.1/README.md → pydantic_rpc-0.8.0/PKG-INFO +360 -10
  2. pydantic_rpc-0.6.1/PKG-INFO → pydantic_rpc-0.8.0/README.md +342 -25
  3. {pydantic_rpc-0.6.1 → pydantic_rpc-0.8.0}/pyproject.toml +10 -7
  4. {pydantic_rpc-0.6.1 → pydantic_rpc-0.8.0}/src/pydantic_rpc/__init__.py +10 -0
  5. pydantic_rpc-0.8.0/src/pydantic_rpc/core.py +2815 -0
  6. pydantic_rpc-0.8.0/src/pydantic_rpc/mcp/__init__.py +5 -0
  7. pydantic_rpc-0.8.0/src/pydantic_rpc/mcp/converter.py +115 -0
  8. pydantic_rpc-0.8.0/src/pydantic_rpc/mcp/exporter.py +283 -0
  9. pydantic_rpc-0.6.1/.gitignore +0 -13
  10. pydantic_rpc-0.6.1/.python-version +0 -1
  11. pydantic_rpc-0.6.1/LICENSE +0 -21
  12. pydantic_rpc-0.6.1/examples/README.md +0 -152
  13. pydantic_rpc-0.6.1/examples/agent_aio_grpc.py +0 -65
  14. pydantic_rpc-0.6.1/examples/agent_connecpy.py +0 -56
  15. pydantic_rpc-0.6.1/examples/asyncio_greeting.py +0 -22
  16. pydantic_rpc-0.6.1/examples/barservice.proto +0 -17
  17. pydantic_rpc-0.6.1/examples/barservice_pb2.py +0 -37
  18. pydantic_rpc-0.6.1/examples/barservice_pb2.pyi +0 -18
  19. pydantic_rpc-0.6.1/examples/barservice_pb2_grpc.py +0 -106
  20. pydantic_rpc-0.6.1/examples/foobar.py +0 -76
  21. pydantic_rpc-0.6.1/examples/foobar_client.py +0 -20
  22. pydantic_rpc-0.6.1/examples/fooservice.proto +0 -21
  23. pydantic_rpc-0.6.1/examples/fooservice_pb2.py +0 -45
  24. pydantic_rpc-0.6.1/examples/fooservice_pb2.pyi +0 -56
  25. pydantic_rpc-0.6.1/examples/fooservice_pb2_grpc.py +0 -106
  26. pydantic_rpc-0.6.1/examples/google/protobuf/duration.proto +0 -115
  27. pydantic_rpc-0.6.1/examples/google/protobuf/timestamp.proto +0 -144
  28. pydantic_rpc-0.6.1/examples/greeter.proto +0 -35
  29. pydantic_rpc-0.6.1/examples/greeter_client.py +0 -14
  30. pydantic_rpc-0.6.1/examples/greeter_connecpy.py +0 -111
  31. pydantic_rpc-0.6.1/examples/greeter_connecpy_client.py +0 -41
  32. pydantic_rpc-0.6.1/examples/greeter_pb2.py +0 -37
  33. pydantic_rpc-0.6.1/examples/greeter_pb2.pyi +0 -17
  34. pydantic_rpc-0.6.1/examples/greeter_pb2_grpc.py +0 -119
  35. pydantic_rpc-0.6.1/examples/greeter_sonora_client.py +0 -8
  36. pydantic_rpc-0.6.1/examples/greeting.py +0 -45
  37. pydantic_rpc-0.6.1/examples/greeting_asgi.py +0 -55
  38. pydantic_rpc-0.6.1/examples/greeting_connecpy.py +0 -44
  39. pydantic_rpc-0.6.1/examples/greeting_using_exsiting_pb2_modules.py +0 -23
  40. pydantic_rpc-0.6.1/examples/greeting_wsgi.py +0 -63
  41. pydantic_rpc-0.6.1/examples/olympicsagent.proto +0 -40
  42. pydantic_rpc-0.6.1/examples/olympicsagent_pb2.py +0 -41
  43. pydantic_rpc-0.6.1/examples/olympicsagent_pb2.pyi +0 -37
  44. pydantic_rpc-0.6.1/examples/olympicsagent_pb2_grpc.py +0 -155
  45. pydantic_rpc-0.6.1/examples/olympicslocationagent.proto +0 -24
  46. pydantic_rpc-0.6.1/examples/olympicslocationagent_connecpy.py +0 -113
  47. pydantic_rpc-0.6.1/examples/olympicslocationagent_pb2.py +0 -39
  48. pydantic_rpc-0.6.1/examples/olympicslocationagent_pb2.pyi +0 -21
  49. pydantic_rpc-0.6.1/examples/olympicslocationagent_pb2_grpc.py +0 -125
  50. pydantic_rpc-0.6.1/src/pydantic_rpc/core.py +0 -1455
  51. pydantic_rpc-0.6.1/tests/asyncechoservice.proto +0 -33
  52. pydantic_rpc-0.6.1/tests/asyncechoservice_connecpy.py +0 -109
  53. pydantic_rpc-0.6.1/tests/asyncechoservice_pb2.py +0 -37
  54. pydantic_rpc-0.6.1/tests/asyncechoservice_pb2.pyi +0 -17
  55. pydantic_rpc-0.6.1/tests/asyncechoservice_pb2_grpc.py +0 -121
  56. pydantic_rpc-0.6.1/tests/echoservice.proto +0 -33
  57. pydantic_rpc-0.6.1/tests/echoservice_connecpy.py +0 -109
  58. pydantic_rpc-0.6.1/tests/echoservice_pb2.py +0 -37
  59. pydantic_rpc-0.6.1/tests/echoservice_pb2.pyi +0 -17
  60. pydantic_rpc-0.6.1/tests/echoservice_pb2_grpc.py +0 -119
  61. pydantic_rpc-0.6.1/tests/test_apps.py +0 -190
  62. pydantic_rpc-0.6.1/uv.lock +0 -1636
  63. {pydantic_rpc-0.6.1 → pydantic_rpc-0.8.0}/src/pydantic_rpc/py.typed +0 -0
@@ -1,3 +1,21 @@
1
+ Metadata-Version: 2.3
2
+ Name: pydantic-rpc
3
+ Version: 0.8.0
4
+ Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
+ Author: Yasushi Itoh
6
+ Requires-Dist: annotated-types>=0.5.0
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
12
+ Requires-Dist: sonora>=0.2.3
13
+ Requires-Dist: connecpy==2.0.0
14
+ Requires-Dist: mcp>=1.9.4
15
+ Requires-Dist: starlette>=0.27.0
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # 🚀 PydanticRPC
2
20
 
3
21
  **PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[Connect RPC](https://connectrpc.com/docs/protocol/) 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.
@@ -110,6 +128,7 @@ app.mount(OlympicsLocationAgent())
110
128
  - **For Connect-RPC:**
111
129
  - 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
112
130
  - 🛠️ **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.
131
+ - 🤖 **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.
113
132
 
114
133
  ## 📦 Installation
115
134
 
@@ -163,12 +182,18 @@ class Greeter:
163
182
  return HelloReply(message=f"Hello, {request.name}!")
164
183
 
165
184
 
185
+ async def main():
186
+ # You can specify a custom port (default is 50051)
187
+ server = AsyncIOServer(port=50052)
188
+ await server.run(Greeter())
189
+
190
+
166
191
  if __name__ == "__main__":
167
- server = AsyncIOServer()
168
- loop = asyncio.get_event_loop()
169
- loop.run_until_complete(server.run(Greeter()))
192
+ asyncio.run(main())
170
193
  ```
171
194
 
195
+ The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
196
+
172
197
  ### 🌐 ASGI Application Example
173
198
 
174
199
  ```python
@@ -248,7 +273,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
248
273
  > - Please follow the instruction described in https://go.dev/doc/install.
249
274
  > 2. Install `protoc-gen-connecpy`:
250
275
  > ```bash
251
- > go install github.com/connecpy/protoc-gen-connecpy@latest
276
+ > go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
252
277
  > ```
253
278
 
254
279
  ## ♻️ Skipping Protobuf Generation
@@ -260,6 +285,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
260
285
 
261
286
  When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
262
287
 
288
+ ## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
289
+ By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
290
+
291
+ ```bash
292
+ export PYDANTIC_RPC_PROTO_PATH=/your/path
293
+ ```
294
+
295
+ ## ⚠️ Reserved Fields
296
+
297
+ You can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.
298
+
299
+ ```bash
300
+ export PYDANTIC_RPC_RESERVED_FIELDS=1
301
+ ```
302
+
263
303
  ## 💎 Advanced Features
264
304
 
265
305
  ### 🌊 Response Streaming
@@ -633,6 +673,203 @@ buf: * (#2) Call complete
633
673
  %
634
674
  ```
635
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
+
636
873
  ### 🔗 Multiple Services with Custom Interceptors
637
874
 
638
875
  PydanticRPC supports defining and running multiple services in a single server:
@@ -718,6 +955,74 @@ if __name__ == "__main__":
718
955
 
719
956
  TODO
720
957
 
958
+ ### 🤖 MCP (Model Context Protocol) Support
959
+
960
+ PydanticRPC can expose your services as MCP tools for AI assistants using FastMCP. This enables seamless integration with any MCP-compatible client.
961
+
962
+ #### Stdio Mode Example
963
+
964
+ ```python
965
+ from pydantic_rpc import Message
966
+ from pydantic_rpc.mcp import MCPExporter
967
+
968
+ class CalculateRequest(Message):
969
+ expression: str
970
+
971
+ class CalculateResponse(Message):
972
+ result: float
973
+
974
+ class MathService:
975
+ def calculate(self, req: CalculateRequest) -> CalculateResponse:
976
+ result = eval(req.expression, {"__builtins__": {}}, {})
977
+ return CalculateResponse(result=float(result))
978
+
979
+ # Run as MCP stdio server
980
+ if __name__ == "__main__":
981
+ service = MathService()
982
+ mcp = MCPExporter(service)
983
+ mcp.run_stdio()
984
+ ```
985
+
986
+ #### Configuring MCP Clients
987
+
988
+ Any MCP-compatible client can connect to your service. For example, to configure Claude Desktop:
989
+
990
+ ```json
991
+ {
992
+ "mcpServers": {
993
+ "my-math-service": {
994
+ "command": "python",
995
+ "args": ["/path/to/math_mcp_server.py"]
996
+ }
997
+ }
998
+ }
999
+ ```
1000
+
1001
+ #### HTTP/ASGI Mode Example
1002
+
1003
+ MCP can also be mounted to existing ASGI applications:
1004
+
1005
+ ```python
1006
+ from pydantic_rpc import ConnecpyASGIApp
1007
+ from pydantic_rpc.mcp import MCPExporter
1008
+
1009
+ # Create Connect-RPC ASGI app
1010
+ app = ConnecpyASGIApp()
1011
+ app.mount(MathService())
1012
+
1013
+ # Add MCP support via HTTP/SSE
1014
+ mcp = MCPExporter(MathService())
1015
+ mcp.mount_to_asgi(app, path="/mcp")
1016
+
1017
+ # Run with uvicorn
1018
+ import uvicorn
1019
+ uvicorn.run(app, host="127.0.0.1", port=8000)
1020
+ ```
1021
+
1022
+ MCP endpoints will be available at:
1023
+ - SSE: `GET http://localhost:8000/mcp/sse`
1024
+ - Messages: `POST http://localhost:8000/mcp/messages/`
1025
+
721
1026
  ### 🗄️ Protobuf file and code (Python files) generation using CLI
722
1027
 
723
1028
  You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
@@ -747,16 +1052,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
747
1052
  | subclass of pydantic.BaseModel | message |
748
1053
 
749
1054
 
1055
+ ## ⚠️ Known Limitations
1056
+
1057
+ ### Union Types with Collections
1058
+
1059
+ Due to protobuf's `oneof` restrictions, you cannot use `Union` types that contain `repeated` (list/tuple) or `map` (dict) fields directly. This is a limitation of the protobuf specification itself.
1060
+
1061
+ **❌ Not Supported:**
1062
+ ```python
1063
+ from typing import Union, List, Dict
1064
+ from pydantic_rpc import Message
1065
+
1066
+ # These will fail during proto compilation
1067
+ class MyMessage(Message):
1068
+ # Union with list - NOT SUPPORTED
1069
+ field1: Union[List[int], str]
1070
+
1071
+ # Union with dict - NOT SUPPORTED
1072
+ field2: Union[Dict[str, int], int]
1073
+
1074
+ # Union with nested collections - NOT SUPPORTED
1075
+ field3: Union[List[Dict[str, int]], str]
1076
+ ```
1077
+
1078
+ **✅ Workaround - Use Message Wrappers:**
1079
+ ```python
1080
+ from typing import Union, List, Dict
1081
+ from pydantic_rpc import Message
1082
+
1083
+ # Wrap collections in Message types
1084
+ class IntList(Message):
1085
+ values: List[int]
1086
+
1087
+ class StringIntMap(Message):
1088
+ values: Dict[str, int]
1089
+
1090
+ class MyMessage(Message):
1091
+ # Now these work!
1092
+ field1: Union[IntList, str]
1093
+ field2: Union[StringIntMap, int]
1094
+ ```
1095
+
1096
+ This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
1097
+
1098
+
750
1099
  ## TODO
751
- - [ ] Streaming Support
1100
+ - [x] Streaming Support
752
1101
  - [x] unary-stream
753
- - [ ] stream-unary
754
- - [ ] stream-stream
755
- - [ ] Betterproto Support
756
- - [ ] Sonora-connect Support
1102
+ - [x] stream-unary
1103
+ - [x] stream-stream
1104
+ - [x] Empty Message Support (automatic google.protobuf.Empty)
1105
+ - [x] Pydantic Serializer Support (@model_serializer, @field_serializer)
757
1106
  - [ ] Custom Health Check Support
1107
+ - [x] MCP (Model Context Protocol) Support via official MCP SDK
758
1108
  - [ ] Add more examples
759
- - [ ] Add tests
1109
+ - [x] Add tests
760
1110
 
761
1111
  ## 📜 License
762
1112