pydantic-rpc 0.6.0__tar.gz → 0.7.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/.github/workflows/release.yml +35 -0
- pydantic_rpc-0.7.0/.github/workflows/test.yml +47 -0
- pydantic_rpc-0.7.0/.gitignore +24 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/PKG-INFO +475 -13
- pydantic_rpc-0.7.0/README.md +898 -0
- pydantic_rpc-0.7.0/docs/mcp.md +95 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/agent_aio_grpc.py +11 -8
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/agent_connecpy.py +11 -8
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/asyncio_greeting.py +22 -22
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice.proto +17 -17
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/foobar.py +76 -76
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/foobar_client.py +20 -20
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice.proto +21 -21
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/google/protobuf/duration.proto +115 -115
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/google/protobuf/timestamp.proto +144 -144
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter.proto +35 -35
- pydantic_rpc-0.7.0/examples/greeter_connecpy.py +124 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_connecpy_client.py +10 -9
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2_grpc.py +4 -17
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting.py +45 -45
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_asgi.py +55 -55
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_connecpy.py +44 -46
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_wsgi.py +63 -63
- pydantic_rpc-0.7.0/examples/mcp_debug_example.py +74 -0
- pydantic_rpc-0.7.0/examples/mcp_example.py +129 -0
- pydantic_rpc-0.7.0/examples/mcp_http_example.py +125 -0
- pydantic_rpc-0.7.0/examples/mcp_simple_calculator.py +45 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent.proto +40 -40
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent.proto +24 -24
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_connecpy.py +30 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/pyproject.toml +9 -3
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/src/pydantic_rpc/__init__.py +10 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/core.py +2445 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/__init__.py +5 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/converter.py +115 -0
- pydantic_rpc-0.7.0/src/pydantic_rpc/mcp/exporter.py +283 -0
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithduration.proto +14 -0
- pydantic_rpc-0.7.0/tests/google_protobuf/greeterwithtimestamp.proto +14 -0
- pydantic_rpc-0.7.0/tests/google_protobuf/test_google_protobuf.py +41 -0
- pydantic_rpc-0.7.0/tests/greeterwithduration.proto +14 -0
- pydantic_rpc-0.7.0/tests/greeterwithtimestamp.proto +14 -0
- pydantic_rpc-0.7.0/tests/test_apps.py +378 -0
- pydantic_rpc-0.7.0/tests/test_conversion.py +1126 -0
- pydantic_rpc-0.7.0/tests/test_mcp.py +181 -0
- pydantic_rpc-0.7.0/tests/test_utils.py +511 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/uv.lock +764 -852
- pydantic_rpc-0.6.0/.gitignore +0 -13
- pydantic_rpc-0.6.0/README.md +0 -440
- pydantic_rpc-0.6.0/examples/greeter_connecpy.py +0 -105
- pydantic_rpc-0.6.0/src/pydantic_rpc/core.py +0 -1455
- pydantic_rpc-0.6.0/tests/asyncechoservice_connecpy.py +0 -101
- pydantic_rpc-0.6.0/tests/asyncechoservice_pb2.py +0 -40
- pydantic_rpc-0.6.0/tests/asyncechoservice_pb2.pyi +0 -17
- pydantic_rpc-0.6.0/tests/asyncechoservice_pb2_grpc.py +0 -110
- pydantic_rpc-0.6.0/tests/echoservice_connecpy.py +0 -101
- pydantic_rpc-0.6.0/tests/echoservice_pb2.py +0 -40
- pydantic_rpc-0.6.0/tests/echoservice_pb2.pyi +0 -17
- pydantic_rpc-0.6.0/tests/echoservice_pb2_grpc.py +0 -110
- pydantic_rpc-0.6.0/tests/test_apps.py +0 -190
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/.python-version +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/LICENSE +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/README.md +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2.pyi +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/barservice_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2.pyi +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/fooservice_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_client.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_pb2.pyi +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeter_sonora_client.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/greeting_using_exsiting_pb2_modules.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2.pyi +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicsagent_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2.pyi +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/examples/olympicslocationagent_pb2_grpc.py +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/src/pydantic_rpc/py.typed +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/tests/asyncechoservice.proto +0 -0
- {pydantic_rpc-0.6.0 → pydantic_rpc-0.7.0}/tests/echoservice.proto +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
attestations: write
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
22
|
+
with:
|
|
23
|
+
python-version-file: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- run: uv sync --frozen
|
|
26
|
+
|
|
27
|
+
- run: uv build
|
|
28
|
+
|
|
29
|
+
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
|
30
|
+
with:
|
|
31
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
32
|
+
|
|
33
|
+
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
|
34
|
+
with:
|
|
35
|
+
subject-path: dist/*.tar.gz
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
fail-fast: false
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Go
|
|
22
|
+
uses: actions/setup-go@v5
|
|
23
|
+
with:
|
|
24
|
+
go-version: '1.24'
|
|
25
|
+
cache: true
|
|
26
|
+
|
|
27
|
+
- name: Install protoc-gen-connecpy plugin
|
|
28
|
+
run: go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
29
|
+
|
|
30
|
+
- name: Setup uv with cache
|
|
31
|
+
id: setup-uv
|
|
32
|
+
uses: astral-sh/setup-uv@v6
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
|
|
36
|
+
- name: Install Python ${{ matrix.python-version }}
|
|
37
|
+
run: uv python install ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
run: uv sync
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: |
|
|
44
|
+
export PATH="$PATH:$(go env GOPATH)/bin"
|
|
45
|
+
uv run pytest
|
|
46
|
+
env:
|
|
47
|
+
PYTHONPATH: ${{ github.workspace }}/src
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# python generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# venv
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# vscode
|
|
13
|
+
.vscode
|
|
14
|
+
|
|
15
|
+
# development
|
|
16
|
+
.devcontainer/
|
|
17
|
+
.env
|
|
18
|
+
rpc_files/
|
|
19
|
+
|
|
20
|
+
# tests
|
|
21
|
+
tests/**/*.py
|
|
22
|
+
tests/**/*.pyi
|
|
23
|
+
!tests/**/test_*.py
|
|
24
|
+
.coverage
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-rpc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
5
|
Author: Yasushi Itoh
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
|
-
Requires-Dist:
|
|
8
|
+
Requires-Dist: annotated-types>=0.5.0
|
|
9
|
+
Requires-Dist: connecpy==2.0.0
|
|
9
10
|
Requires-Dist: grpcio-health-checking>=1.56.2
|
|
10
11
|
Requires-Dist: grpcio-reflection>=1.56.2
|
|
11
12
|
Requires-Dist: grpcio-tools>=1.56.2
|
|
13
|
+
Requires-Dist: grpcio>=1.56.2
|
|
14
|
+
Requires-Dist: mcp>=1.9.4
|
|
12
15
|
Requires-Dist: pydantic>=2.1.1
|
|
13
16
|
Requires-Dist: sonora>=0.2.3
|
|
17
|
+
Requires-Dist: starlette>=0.27.0
|
|
14
18
|
Description-Content-Type: text/markdown
|
|
15
19
|
|
|
16
20
|
# 🚀 PydanticRPC
|
|
@@ -23,7 +27,9 @@ Below is an example of a simple gRPC service that exposes a [PydanticAI](https:/
|
|
|
23
27
|
```python
|
|
24
28
|
import asyncio
|
|
25
29
|
|
|
30
|
+
from openai import AsyncOpenAI
|
|
26
31
|
from pydantic_ai import Agent
|
|
32
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
27
33
|
from pydantic_rpc import AsyncIOServer, Message
|
|
28
34
|
|
|
29
35
|
|
|
@@ -42,7 +48,15 @@ class Olympics(Message):
|
|
|
42
48
|
|
|
43
49
|
class OlympicsLocationAgent:
|
|
44
50
|
def __init__(self):
|
|
45
|
-
|
|
51
|
+
client = AsyncOpenAI(
|
|
52
|
+
base_url="http://localhost:11434/v1",
|
|
53
|
+
api_key="ollama_api_key",
|
|
54
|
+
)
|
|
55
|
+
ollama_model = OpenAIModel(
|
|
56
|
+
model_name="llama3.2",
|
|
57
|
+
openai_client=client,
|
|
58
|
+
)
|
|
59
|
+
self._agent = Agent(ollama_model)
|
|
46
60
|
|
|
47
61
|
async def ask(self, req: Olympics) -> CityLocation:
|
|
48
62
|
result = await self._agent.run(req.prompt())
|
|
@@ -60,7 +74,9 @@ And here is an example of a simple Connect RPC service that exposes the same age
|
|
|
60
74
|
```python
|
|
61
75
|
import asyncio
|
|
62
76
|
|
|
77
|
+
from openai import AsyncOpenAI
|
|
63
78
|
from pydantic_ai import Agent
|
|
79
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
64
80
|
from pydantic_rpc import ConnecpyASGIApp, Message
|
|
65
81
|
|
|
66
82
|
|
|
@@ -78,7 +94,15 @@ class Olympics(Message):
|
|
|
78
94
|
|
|
79
95
|
class OlympicsLocationAgent:
|
|
80
96
|
def __init__(self):
|
|
81
|
-
|
|
97
|
+
client = AsyncOpenAI(
|
|
98
|
+
base_url="http://localhost:11434/v1",
|
|
99
|
+
api_key="ollama_api_key",
|
|
100
|
+
)
|
|
101
|
+
ollama_model = OpenAIModel(
|
|
102
|
+
model_name="llama3.2",
|
|
103
|
+
openai_client=client,
|
|
104
|
+
)
|
|
105
|
+
self._agent = Agent(ollama_model, result_type=CityLocation)
|
|
82
106
|
|
|
83
107
|
async def ask(self, req: Olympics) -> CityLocation:
|
|
84
108
|
result = await self._agent.run(req.prompt())
|
|
@@ -105,6 +129,7 @@ app.mount(OlympicsLocationAgent())
|
|
|
105
129
|
- **For Connect-RPC:**
|
|
106
130
|
- 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
|
|
107
131
|
- 🛠️ **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
|
+
- 🤖 **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.
|
|
108
133
|
|
|
109
134
|
## 📦 Installation
|
|
110
135
|
|
|
@@ -158,12 +183,18 @@ class Greeter:
|
|
|
158
183
|
return HelloReply(message=f"Hello, {request.name}!")
|
|
159
184
|
|
|
160
185
|
|
|
186
|
+
async def main():
|
|
187
|
+
# You can specify a custom port (default is 50051)
|
|
188
|
+
server = AsyncIOServer(port=50052)
|
|
189
|
+
await server.run(Greeter())
|
|
190
|
+
|
|
191
|
+
|
|
161
192
|
if __name__ == "__main__":
|
|
162
|
-
|
|
163
|
-
loop = asyncio.get_event_loop()
|
|
164
|
-
loop.run_until_complete(server.run(Greeter()))
|
|
193
|
+
asyncio.run(main())
|
|
165
194
|
```
|
|
166
195
|
|
|
196
|
+
The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
|
|
197
|
+
|
|
167
198
|
### 🌐 ASGI Application Example
|
|
168
199
|
|
|
169
200
|
```python
|
|
@@ -243,7 +274,7 @@ This will launch a Connecpy-based ASGI application that uses the same Pydantic m
|
|
|
243
274
|
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
244
275
|
> 2. Install `protoc-gen-connecpy`:
|
|
245
276
|
> ```bash
|
|
246
|
-
> go install github.com/connecpy/protoc-gen-connecpy@latest
|
|
277
|
+
> go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
247
278
|
> ```
|
|
248
279
|
|
|
249
280
|
## ♻️ Skipping Protobuf Generation
|
|
@@ -255,6 +286,21 @@ export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
|
255
286
|
|
|
256
287
|
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
|
|
257
288
|
|
|
289
|
+
## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
|
|
290
|
+
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:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
export PYDANTIC_RPC_PROTO_PATH=/your/path
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## ⚠️ Reserved Fields
|
|
297
|
+
|
|
298
|
+
You can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
export PYDANTIC_RPC_RESERVED_FIELDS=1
|
|
302
|
+
```
|
|
303
|
+
|
|
258
304
|
## 💎 Advanced Features
|
|
259
305
|
|
|
260
306
|
### 🌊 Response Streaming
|
|
@@ -268,8 +314,10 @@ Please see the sample code below:
|
|
|
268
314
|
import asyncio
|
|
269
315
|
from typing import Annotated, AsyncIterator
|
|
270
316
|
|
|
317
|
+
from openai import AsyncOpenAI
|
|
271
318
|
from pydantic import Field
|
|
272
319
|
from pydantic_ai import Agent
|
|
320
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
273
321
|
from pydantic_rpc import AsyncIOServer, Message
|
|
274
322
|
|
|
275
323
|
|
|
@@ -302,7 +350,15 @@ class StreamingResult(Message):
|
|
|
302
350
|
|
|
303
351
|
class OlympicsAgent:
|
|
304
352
|
def __init__(self):
|
|
305
|
-
|
|
353
|
+
client = AsyncOpenAI(
|
|
354
|
+
base_url='http://localhost:11434/v1',
|
|
355
|
+
api_key='ollama_api_key',
|
|
356
|
+
)
|
|
357
|
+
ollama_model = OpenAIModel(
|
|
358
|
+
model_name='llama3.2',
|
|
359
|
+
openai_client=client,
|
|
360
|
+
)
|
|
361
|
+
self._agent = Agent(ollama_model)
|
|
306
362
|
|
|
307
363
|
async def ask(self, req: OlympicsQuery) -> CityLocation:
|
|
308
364
|
result = await self._agent.run(req.prompt(), result_type=CityLocation)
|
|
@@ -324,6 +380,299 @@ if __name__ == "__main__":
|
|
|
324
380
|
|
|
325
381
|
In the example above, the `ask_stream` method returns an `AsyncIterator[StreamingResult]` object, which is considered a streaming method. The `StreamingResult` class is a Pydantic model that defines the response type of the streaming method. You can use any Pydantic model as the response type.
|
|
326
382
|
|
|
383
|
+
Now, you can call the `ask_stream` method of the server described above using your preferred gRPC client tool. The example below uses `buf curl`.
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
```console
|
|
387
|
+
% buf curl --data '{"start": 1980, "end": 2024}' -v http://localhost:50051/olympicsagent.v1.OlympicsAgent/AskStream --protocol grpc --http2-prior-knowledge
|
|
388
|
+
|
|
389
|
+
buf: * Using server reflection to resolve "olympicsagent.v1.OlympicsAgent"
|
|
390
|
+
buf: * Dialing (tcp) localhost:50051...
|
|
391
|
+
buf: * Connected to [::1]:50051
|
|
392
|
+
buf: > (#1) POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo
|
|
393
|
+
buf: > (#1) Accept-Encoding: identity
|
|
394
|
+
buf: > (#1) Content-Type: application/grpc+proto
|
|
395
|
+
buf: > (#1) Grpc-Accept-Encoding: gzip
|
|
396
|
+
buf: > (#1) Grpc-Timeout: 119997m
|
|
397
|
+
buf: > (#1) Te: trailers
|
|
398
|
+
buf: > (#1) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
|
|
399
|
+
buf: > (#1)
|
|
400
|
+
buf: } (#1) [5 bytes data]
|
|
401
|
+
buf: } (#1) [32 bytes data]
|
|
402
|
+
buf: < (#1) HTTP/2.0 200 OK
|
|
403
|
+
buf: < (#1) Content-Type: application/grpc
|
|
404
|
+
buf: < (#1) Grpc-Message: Method not found!
|
|
405
|
+
buf: < (#1) Grpc-Status: 12
|
|
406
|
+
buf: < (#1)
|
|
407
|
+
buf: * (#1) Call complete
|
|
408
|
+
buf: > (#2) POST /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo
|
|
409
|
+
buf: > (#2) Accept-Encoding: identity
|
|
410
|
+
buf: > (#2) Content-Type: application/grpc+proto
|
|
411
|
+
buf: > (#2) Grpc-Accept-Encoding: gzip
|
|
412
|
+
buf: > (#2) Grpc-Timeout: 119967m
|
|
413
|
+
buf: > (#2) Te: trailers
|
|
414
|
+
buf: > (#2) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
|
|
415
|
+
buf: > (#2)
|
|
416
|
+
buf: } (#2) [5 bytes data]
|
|
417
|
+
buf: } (#2) [32 bytes data]
|
|
418
|
+
buf: < (#2) HTTP/2.0 200 OK
|
|
419
|
+
buf: < (#2) Content-Type: application/grpc
|
|
420
|
+
buf: < (#2) Grpc-Accept-Encoding: identity, deflate, gzip
|
|
421
|
+
buf: < (#2)
|
|
422
|
+
buf: { (#2) [5 bytes data]
|
|
423
|
+
buf: { (#2) [434 bytes data]
|
|
424
|
+
buf: * Server reflection has resolved file "olympicsagent.proto"
|
|
425
|
+
buf: * Invoking RPC olympicsagent.v1.OlympicsAgent.AskStream
|
|
426
|
+
buf: > (#3) POST /olympicsagent.v1.OlympicsAgent/AskStream
|
|
427
|
+
buf: > (#3) Accept-Encoding: identity
|
|
428
|
+
buf: > (#3) Content-Type: application/grpc+proto
|
|
429
|
+
buf: > (#3) Grpc-Accept-Encoding: gzip
|
|
430
|
+
buf: > (#3) Grpc-Timeout: 119947m
|
|
431
|
+
buf: > (#3) Te: trailers
|
|
432
|
+
buf: > (#3) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1
|
|
433
|
+
buf: > (#3)
|
|
434
|
+
buf: } (#3) [5 bytes data]
|
|
435
|
+
buf: } (#3) [6 bytes data]
|
|
436
|
+
buf: * (#3) Finished upload
|
|
437
|
+
buf: < (#3) HTTP/2.0 200 OK
|
|
438
|
+
buf: < (#3) Content-Type: application/grpc
|
|
439
|
+
buf: < (#3) Grpc-Accept-Encoding: identity, deflate, gzip
|
|
440
|
+
buf: < (#3)
|
|
441
|
+
buf: { (#3) [5 bytes data]
|
|
442
|
+
buf: { (#3) [25 bytes data]
|
|
443
|
+
{
|
|
444
|
+
"answer": "Here's a list of Summer"
|
|
445
|
+
}
|
|
446
|
+
buf: { (#3) [5 bytes data]
|
|
447
|
+
buf: { (#3) [31 bytes data]
|
|
448
|
+
{
|
|
449
|
+
"answer": " and Winter Olympics from 198"
|
|
450
|
+
}
|
|
451
|
+
buf: { (#3) [5 bytes data]
|
|
452
|
+
buf: { (#3) [29 bytes data]
|
|
453
|
+
{
|
|
454
|
+
"answer": "0 to 2024:\n\nSummer Olympics"
|
|
455
|
+
}
|
|
456
|
+
buf: { (#3) [5 bytes data]
|
|
457
|
+
buf: { (#3) [20 bytes data]
|
|
458
|
+
{
|
|
459
|
+
"answer": ":\n1. 1980 - Moscow"
|
|
460
|
+
}
|
|
461
|
+
buf: { (#3) [5 bytes data]
|
|
462
|
+
buf: { (#3) [20 bytes data]
|
|
463
|
+
{
|
|
464
|
+
"answer": ", Soviet Union\n2. "
|
|
465
|
+
}
|
|
466
|
+
buf: { (#3) [5 bytes data]
|
|
467
|
+
buf: { (#3) [32 bytes data]
|
|
468
|
+
{
|
|
469
|
+
"answer": "1984 - Los Angeles, California"
|
|
470
|
+
}
|
|
471
|
+
buf: { (#3) [5 bytes data]
|
|
472
|
+
buf: { (#3) [15 bytes data]
|
|
473
|
+
{
|
|
474
|
+
"answer": ", USA\n3. 1988"
|
|
475
|
+
}
|
|
476
|
+
buf: { (#3) [5 bytes data]
|
|
477
|
+
buf: { (#3) [26 bytes data]
|
|
478
|
+
{
|
|
479
|
+
"answer": " - Seoul, South Korea\n4."
|
|
480
|
+
}
|
|
481
|
+
buf: { (#3) [5 bytes data]
|
|
482
|
+
buf: { (#3) [27 bytes data]
|
|
483
|
+
{
|
|
484
|
+
"answer": " 1992 - Barcelona, Spain\n"
|
|
485
|
+
}
|
|
486
|
+
buf: { (#3) [5 bytes data]
|
|
487
|
+
buf: { (#3) [20 bytes data]
|
|
488
|
+
{
|
|
489
|
+
"answer": "5. 1996 - Atlanta,"
|
|
490
|
+
}
|
|
491
|
+
buf: { (#3) [5 bytes data]
|
|
492
|
+
buf: { (#3) [22 bytes data]
|
|
493
|
+
{
|
|
494
|
+
"answer": " Georgia, USA\n6. 200"
|
|
495
|
+
}
|
|
496
|
+
buf: { (#3) [5 bytes data]
|
|
497
|
+
buf: { (#3) [26 bytes data]
|
|
498
|
+
{
|
|
499
|
+
"answer": "0 - Sydney, Australia\n7."
|
|
500
|
+
}
|
|
501
|
+
buf: { (#3) [5 bytes data]
|
|
502
|
+
buf: { (#3) [25 bytes data]
|
|
503
|
+
{
|
|
504
|
+
"answer": " 2004 - Athens, Greece\n"
|
|
505
|
+
}
|
|
506
|
+
buf: { (#3) [5 bytes data]
|
|
507
|
+
buf: { (#3) [20 bytes data]
|
|
508
|
+
{
|
|
509
|
+
"answer": "8. 2008 - Beijing,"
|
|
510
|
+
}
|
|
511
|
+
buf: { (#3) [5 bytes data]
|
|
512
|
+
buf: { (#3) [18 bytes data]
|
|
513
|
+
{
|
|
514
|
+
"answer": " China\n9. 2012 -"
|
|
515
|
+
}
|
|
516
|
+
buf: { (#3) [5 bytes data]
|
|
517
|
+
buf: { (#3) [29 bytes data]
|
|
518
|
+
{
|
|
519
|
+
"answer": " London, United Kingdom\n10."
|
|
520
|
+
}
|
|
521
|
+
buf: { (#3) [5 bytes data]
|
|
522
|
+
buf: { (#3) [24 bytes data]
|
|
523
|
+
{
|
|
524
|
+
"answer": " 2016 - Rio de Janeiro"
|
|
525
|
+
}
|
|
526
|
+
buf: { (#3) [5 bytes data]
|
|
527
|
+
buf: { (#3) [18 bytes data]
|
|
528
|
+
{
|
|
529
|
+
"answer": ", Brazil\n11. 202"
|
|
530
|
+
}
|
|
531
|
+
buf: { (#3) [5 bytes data]
|
|
532
|
+
buf: { (#3) [24 bytes data]
|
|
533
|
+
{
|
|
534
|
+
"answer": "0 - Tokyo, Japan (held"
|
|
535
|
+
}
|
|
536
|
+
buf: { (#3) [5 bytes data]
|
|
537
|
+
buf: { (#3) [21 bytes data]
|
|
538
|
+
{
|
|
539
|
+
"answer": " in 2021 due to the"
|
|
540
|
+
}
|
|
541
|
+
buf: { (#3) [5 bytes data]
|
|
542
|
+
buf: { (#3) [26 bytes data]
|
|
543
|
+
{
|
|
544
|
+
"answer": " COVID-19 pandemic)\n12. "
|
|
545
|
+
}
|
|
546
|
+
buf: { (#3) [5 bytes data]
|
|
547
|
+
buf: { (#3) [28 bytes data]
|
|
548
|
+
{
|
|
549
|
+
"answer": "2024 - Paris, France\n\nNote"
|
|
550
|
+
}
|
|
551
|
+
buf: { (#3) [5 bytes data]
|
|
552
|
+
buf: { (#3) [41 bytes data]
|
|
553
|
+
{
|
|
554
|
+
"answer": ": The Olympics were held without a host"
|
|
555
|
+
}
|
|
556
|
+
buf: { (#3) [5 bytes data]
|
|
557
|
+
buf: { (#3) [26 bytes data]
|
|
558
|
+
{
|
|
559
|
+
"answer": " city for one year (2022"
|
|
560
|
+
}
|
|
561
|
+
buf: { (#3) [5 bytes data]
|
|
562
|
+
buf: { (#3) [42 bytes data]
|
|
563
|
+
{
|
|
564
|
+
"answer": ", due to the Russian invasion of Ukraine"
|
|
565
|
+
}
|
|
566
|
+
buf: { (#3) [5 bytes data]
|
|
567
|
+
buf: { (#3) [29 bytes data]
|
|
568
|
+
{
|
|
569
|
+
"answer": ").\n\nWinter Olympics:\n1. 198"
|
|
570
|
+
}
|
|
571
|
+
buf: { (#3) [5 bytes data]
|
|
572
|
+
buf: { (#3) [27 bytes data]
|
|
573
|
+
{
|
|
574
|
+
"answer": "0 - Lake Placid, New York"
|
|
575
|
+
}
|
|
576
|
+
buf: { (#3) [5 bytes data]
|
|
577
|
+
buf: { (#3) [15 bytes data]
|
|
578
|
+
{
|
|
579
|
+
"answer": ", USA\n2. 1984"
|
|
580
|
+
}
|
|
581
|
+
buf: { (#3) [5 bytes data]
|
|
582
|
+
buf: { (#3) [27 bytes data]
|
|
583
|
+
{
|
|
584
|
+
"answer": " - Sarajevo, Yugoslavia ("
|
|
585
|
+
}
|
|
586
|
+
buf: { (#3) [5 bytes data]
|
|
587
|
+
buf: { (#3) [30 bytes data]
|
|
588
|
+
{
|
|
589
|
+
"answer": "now Bosnia and Herzegovina)\n"
|
|
590
|
+
}
|
|
591
|
+
buf: { (#3) [5 bytes data]
|
|
592
|
+
buf: { (#3) [20 bytes data]
|
|
593
|
+
{
|
|
594
|
+
"answer": "3. 1988 - Calgary,"
|
|
595
|
+
}
|
|
596
|
+
buf: { (#3) [5 bytes data]
|
|
597
|
+
buf: { (#3) [25 bytes data]
|
|
598
|
+
{
|
|
599
|
+
"answer": " Alberta, Canada\n4. 199"
|
|
600
|
+
}
|
|
601
|
+
buf: { (#3) [5 bytes data]
|
|
602
|
+
buf: { (#3) [26 bytes data]
|
|
603
|
+
{
|
|
604
|
+
"answer": "2 - Albertville, France\n"
|
|
605
|
+
}
|
|
606
|
+
buf: { (#3) [5 bytes data]
|
|
607
|
+
buf: { (#3) [13 bytes data]
|
|
608
|
+
{
|
|
609
|
+
"answer": "5. 1994 - L"
|
|
610
|
+
}
|
|
611
|
+
buf: { (#3) [5 bytes data]
|
|
612
|
+
buf: { (#3) [24 bytes data]
|
|
613
|
+
{
|
|
614
|
+
"answer": "illehammer, Norway\n6. "
|
|
615
|
+
}
|
|
616
|
+
buf: { (#3) [5 bytes data]
|
|
617
|
+
buf: { (#3) [23 bytes data]
|
|
618
|
+
{
|
|
619
|
+
"answer": "1998 - Nagano, Japan\n"
|
|
620
|
+
}
|
|
621
|
+
buf: { (#3) [5 bytes data]
|
|
622
|
+
buf: { (#3) [16 bytes data]
|
|
623
|
+
{
|
|
624
|
+
"answer": "7. 2002 - Salt"
|
|
625
|
+
}
|
|
626
|
+
buf: { (#3) [5 bytes data]
|
|
627
|
+
buf: { (#3) [24 bytes data]
|
|
628
|
+
{
|
|
629
|
+
"answer": " Lake City, Utah, USA\n"
|
|
630
|
+
}
|
|
631
|
+
buf: { (#3) [5 bytes data]
|
|
632
|
+
buf: { (#3) [18 bytes data]
|
|
633
|
+
{
|
|
634
|
+
"answer": "8. 2006 - Torino"
|
|
635
|
+
}
|
|
636
|
+
buf: { (#3) [5 bytes data]
|
|
637
|
+
buf: { (#3) [17 bytes data]
|
|
638
|
+
{
|
|
639
|
+
"answer": ", Italy\n9. 2010"
|
|
640
|
+
}
|
|
641
|
+
buf: { (#3) [5 bytes data]
|
|
642
|
+
buf: { (#3) [40 bytes data]
|
|
643
|
+
{
|
|
644
|
+
"answer": " - Vancouver, British Columbia, Canada"
|
|
645
|
+
}
|
|
646
|
+
buf: { (#3) [5 bytes data]
|
|
647
|
+
buf: { (#3) [13 bytes data]
|
|
648
|
+
{
|
|
649
|
+
"answer": "\n10. 2014 -"
|
|
650
|
+
}
|
|
651
|
+
buf: { (#3) [5 bytes data]
|
|
652
|
+
buf: { (#3) [20 bytes data]
|
|
653
|
+
{
|
|
654
|
+
"answer": " Sochi, Russia\n11."
|
|
655
|
+
}
|
|
656
|
+
buf: { (#3) [5 bytes data]
|
|
657
|
+
buf: { (#3) [16 bytes data]
|
|
658
|
+
{
|
|
659
|
+
"answer": " 2018 - Pyeong"
|
|
660
|
+
}
|
|
661
|
+
buf: { (#3) [5 bytes data]
|
|
662
|
+
buf: { (#3) [24 bytes data]
|
|
663
|
+
{
|
|
664
|
+
"answer": "chang, South Korea\n12."
|
|
665
|
+
}
|
|
666
|
+
buf: < (#3)
|
|
667
|
+
buf: < (#3) Grpc-Message:
|
|
668
|
+
buf: < (#3) Grpc-Status: 0
|
|
669
|
+
buf: * (#3) Call complete
|
|
670
|
+
buf: < (#2)
|
|
671
|
+
buf: < (#2) Grpc-Message:
|
|
672
|
+
buf: < (#2) Grpc-Status: 0
|
|
673
|
+
buf: * (#2) Call complete
|
|
674
|
+
%
|
|
675
|
+
```
|
|
327
676
|
|
|
328
677
|
### 🔗 Multiple Services with Custom Interceptors
|
|
329
678
|
|
|
@@ -410,6 +759,74 @@ if __name__ == "__main__":
|
|
|
410
759
|
|
|
411
760
|
TODO
|
|
412
761
|
|
|
762
|
+
### 🤖 MCP (Model Context Protocol) Support
|
|
763
|
+
|
|
764
|
+
PydanticRPC can expose your services as MCP tools for AI assistants using FastMCP. This enables seamless integration with any MCP-compatible client.
|
|
765
|
+
|
|
766
|
+
#### Stdio Mode Example
|
|
767
|
+
|
|
768
|
+
```python
|
|
769
|
+
from pydantic_rpc import Message
|
|
770
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
771
|
+
|
|
772
|
+
class CalculateRequest(Message):
|
|
773
|
+
expression: str
|
|
774
|
+
|
|
775
|
+
class CalculateResponse(Message):
|
|
776
|
+
result: float
|
|
777
|
+
|
|
778
|
+
class MathService:
|
|
779
|
+
def calculate(self, req: CalculateRequest) -> CalculateResponse:
|
|
780
|
+
result = eval(req.expression, {"__builtins__": {}}, {})
|
|
781
|
+
return CalculateResponse(result=float(result))
|
|
782
|
+
|
|
783
|
+
# Run as MCP stdio server
|
|
784
|
+
if __name__ == "__main__":
|
|
785
|
+
service = MathService()
|
|
786
|
+
mcp = MCPExporter(service)
|
|
787
|
+
mcp.run_stdio()
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
#### Configuring MCP Clients
|
|
791
|
+
|
|
792
|
+
Any MCP-compatible client can connect to your service. For example, to configure Claude Desktop:
|
|
793
|
+
|
|
794
|
+
```json
|
|
795
|
+
{
|
|
796
|
+
"mcpServers": {
|
|
797
|
+
"my-math-service": {
|
|
798
|
+
"command": "python",
|
|
799
|
+
"args": ["/path/to/math_mcp_server.py"]
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
#### HTTP/ASGI Mode Example
|
|
806
|
+
|
|
807
|
+
MCP can also be mounted to existing ASGI applications:
|
|
808
|
+
|
|
809
|
+
```python
|
|
810
|
+
from pydantic_rpc import ConnecpyASGIApp
|
|
811
|
+
from pydantic_rpc.mcp import MCPExporter
|
|
812
|
+
|
|
813
|
+
# Create Connect-RPC ASGI app
|
|
814
|
+
app = ConnecpyASGIApp()
|
|
815
|
+
app.mount(MathService())
|
|
816
|
+
|
|
817
|
+
# Add MCP support via HTTP/SSE
|
|
818
|
+
mcp = MCPExporter(MathService())
|
|
819
|
+
mcp.mount_to_asgi(app, path="/mcp")
|
|
820
|
+
|
|
821
|
+
# Run with uvicorn
|
|
822
|
+
import uvicorn
|
|
823
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
MCP endpoints will be available at:
|
|
827
|
+
- SSE: `GET http://localhost:8000/mcp/sse`
|
|
828
|
+
- Messages: `POST http://localhost:8000/mcp/messages/`
|
|
829
|
+
|
|
413
830
|
### 🗄️ Protobuf file and code (Python files) generation using CLI
|
|
414
831
|
|
|
415
832
|
You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
|
|
@@ -439,16 +856,61 @@ Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you coul
|
|
|
439
856
|
| subclass of pydantic.BaseModel | message |
|
|
440
857
|
|
|
441
858
|
|
|
859
|
+
## ⚠️ Known Limitations
|
|
860
|
+
|
|
861
|
+
### Union Types with Collections
|
|
862
|
+
|
|
863
|
+
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.
|
|
864
|
+
|
|
865
|
+
**❌ Not Supported:**
|
|
866
|
+
```python
|
|
867
|
+
from typing import Union, List, Dict
|
|
868
|
+
from pydantic_rpc import Message
|
|
869
|
+
|
|
870
|
+
# These will fail during proto compilation
|
|
871
|
+
class MyMessage(Message):
|
|
872
|
+
# Union with list - NOT SUPPORTED
|
|
873
|
+
field1: Union[List[int], str]
|
|
874
|
+
|
|
875
|
+
# Union with dict - NOT SUPPORTED
|
|
876
|
+
field2: Union[Dict[str, int], int]
|
|
877
|
+
|
|
878
|
+
# Union with nested collections - NOT SUPPORTED
|
|
879
|
+
field3: Union[List[Dict[str, int]], str]
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**✅ Workaround - Use Message Wrappers:**
|
|
883
|
+
```python
|
|
884
|
+
from typing import Union, List, Dict
|
|
885
|
+
from pydantic_rpc import Message
|
|
886
|
+
|
|
887
|
+
# Wrap collections in Message types
|
|
888
|
+
class IntList(Message):
|
|
889
|
+
values: List[int]
|
|
890
|
+
|
|
891
|
+
class StringIntMap(Message):
|
|
892
|
+
values: Dict[str, int]
|
|
893
|
+
|
|
894
|
+
class MyMessage(Message):
|
|
895
|
+
# Now these work!
|
|
896
|
+
field1: Union[IntList, str]
|
|
897
|
+
field2: Union[StringIntMap, int]
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
|
|
901
|
+
|
|
902
|
+
|
|
442
903
|
## TODO
|
|
443
|
-
- [
|
|
904
|
+
- [x] Streaming Support
|
|
444
905
|
- [x] unary-stream
|
|
445
|
-
- [
|
|
446
|
-
- [
|
|
906
|
+
- [x] stream-unary
|
|
907
|
+
- [x] stream-stream
|
|
447
908
|
- [ ] Betterproto Support
|
|
448
909
|
- [ ] Sonora-connect Support
|
|
449
910
|
- [ ] Custom Health Check Support
|
|
911
|
+
- [x] MCP (Model Context Protocol) Support via official MCP SDK
|
|
450
912
|
- [ ] Add more examples
|
|
451
|
-
- [
|
|
913
|
+
- [x] Add tests
|
|
452
914
|
|
|
453
915
|
## 📜 License
|
|
454
916
|
|