pydantic-rpc 0.6.1__py3-none-any.whl → 0.8.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.
@@ -1,16 +1,19 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.6.1
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
- Requires-Dist: connecpy>=1.4.1
9
- Requires-Dist: grpcio-health-checking>=1.56.2
10
- Requires-Dist: grpcio-reflection>=1.56.2
11
- Requires-Dist: grpcio-tools>=1.56.2
6
+ Requires-Dist: annotated-types>=0.5.0
12
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
13
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
14
17
  Description-Content-Type: text/markdown
15
18
 
16
19
  # 🚀 PydanticRPC
@@ -125,6 +128,7 @@ app.mount(OlympicsLocationAgent())
125
128
  - **For Connect-RPC:**
126
129
  - 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
127
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.
128
132
 
129
133
  ## 📦 Installation
130
134
 
@@ -178,12 +182,18 @@ class Greeter:
178
182
  return HelloReply(message=f"Hello, {request.name}!")
179
183
 
180
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
+
181
191
  if __name__ == "__main__":
182
- server = AsyncIOServer()
183
- loop = asyncio.get_event_loop()
184
- loop.run_until_complete(server.run(Greeter()))
192
+ asyncio.run(main())
185
193
  ```
186
194
 
195
+ The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
196
+
187
197
  ### 🌐 ASGI Application Example
188
198
 
189
199
  ```python
@@ -263,7 +273,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
263
273
  > - Please follow the instruction described in https://go.dev/doc/install.
264
274
  > 2. Install `protoc-gen-connecpy`:
265
275
  > ```bash
266
- > go install github.com/connecpy/protoc-gen-connecpy@latest
276
+ > go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
267
277
  > ```
268
278
 
269
279
  ## ♻️ Skipping Protobuf Generation
@@ -275,6 +285,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
275
285
 
276
286
  When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
277
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
+
278
303
  ## 💎 Advanced Features
279
304
 
280
305
  ### 🌊 Response Streaming
@@ -648,6 +673,203 @@ buf: * (#2) Call complete
648
673
  %
649
674
  ```
650
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
+
651
873
  ### 🔗 Multiple Services with Custom Interceptors
652
874
 
653
875
  PydanticRPC supports defining and running multiple services in a single server:
@@ -733,6 +955,74 @@ if __name__ == "__main__":
733
955
 
734
956
  TODO
735
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
+
736
1026
  ### 🗄️ Protobuf file and code (Python files) generation using CLI
737
1027
 
738
1028
  You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
@@ -762,16 +1052,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
762
1052
  | subclass of pydantic.BaseModel | message |
763
1053
 
764
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
+
765
1099
  ## TODO
766
- - [ ] Streaming Support
1100
+ - [x] Streaming Support
767
1101
  - [x] unary-stream
768
- - [ ] stream-unary
769
- - [ ] stream-stream
770
- - [ ] Betterproto Support
771
- - [ ] 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)
772
1106
  - [ ] Custom Health Check Support
1107
+ - [x] MCP (Model Context Protocol) Support via official MCP SDK
773
1108
  - [ ] Add more examples
774
- - [ ] Add tests
1109
+ - [x] Add tests
775
1110
 
776
1111
  ## 📜 License
777
1112
 
@@ -0,0 +1,10 @@
1
+ pydantic_rpc/__init__.py,sha256=7dbedaf58742b13e96954afb68ca91f9800a2faf0799f78da6070c9c88511065,440
2
+ pydantic_rpc/core.py,sha256=2e2bce8aaf677ae50fdcdde51507da605ed77efe5e1a522e52b406c416b18318,104265
3
+ pydantic_rpc/mcp/__init__.py,sha256=f05ad62cc38db5c5972e16870c484523c58c5ac650d8454706f1ce4539cc7a52,123
4
+ pydantic_rpc/mcp/converter.py,sha256=b60dcaf82e6bff6be4b2ab8b1e9f2d16e09cb98d8f00182a4f6f73d1a78848a4,4158
5
+ pydantic_rpc/mcp/exporter.py,sha256=20833662f9a973ed9e1a705b9b59aaa2533de6c514ebd87ef66664a430a0d04f,10239
6
+ pydantic_rpc/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
7
+ pydantic_rpc-0.8.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
8
+ pydantic_rpc-0.8.0.dist-info/entry_points.txt,sha256=72a47b1d2cae3abc045710fe0c2c2e6dfbb051fbf6960c22b62488004e9188ba,57
9
+ pydantic_rpc-0.8.0.dist-info/METADATA,sha256=1cb4d588db71dd052e24c12c63d368c89520279bc9c9a8b10658fdd0d83ee0ec,29803
10
+ pydantic_rpc-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.7.22
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  pydantic-rpc = pydantic_rpc.core:main
3
+
@@ -1,8 +0,0 @@
1
- pydantic_rpc/__init__.py,sha256=oomSVGmh_zddQQaphQt1L2xSVh9dD1LVyaAq1cN1FW4,231
2
- pydantic_rpc/core.py,sha256=SB9GDZRxsMWQFxphinYQUAqrOx10eUn40fPTc7t1xYs,52107
3
- pydantic_rpc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pydantic_rpc-0.6.1.dist-info/METADATA,sha256=Vi8bU0-HjPnhI0ETt9ReRhmMfJxBqPuf9gnAYQjTx4c,20481
5
- pydantic_rpc-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pydantic_rpc-0.6.1.dist-info/entry_points.txt,sha256=LeZJ6UN-fhjKrEGkcmsAAKuA-fIe7MpvzKMPSZfi0NE,56
7
- pydantic_rpc-0.6.1.dist-info/licenses/LICENSE,sha256=Y6jkAm2VqPqoGIGQ-mEQCecNfteQ2LwdpYhC5XiH_cA,1069
8
- pydantic_rpc-0.6.1.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any
@@ -1,21 +0,0 @@
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.