{"id":24981665,"url":"https://github.com/i2y/pydantic-rpc","last_synced_at":"2026-02-04T01:17:18.178Z","repository":{"id":188067643,"uuid":"678037842","full_name":"i2y/pydantic-rpc","owner":"i2y","description":"PydanticRPC is a Python library that enables you to rapidly expose Pydantic models via gRPC/ConnectRPC services without writing any protobuf files.","archived":false,"fork":false,"pushed_at":"2025-06-11T15:52:51.000Z","size":549,"stargazers_count":25,"open_issues_count":2,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-11T17:13:22.021Z","etag":null,"topics":["connectrpc","grpc","grpc-web","protobuf","pydantic","python","python-library","python3"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/i2y.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-08-13T13:34:05.000Z","updated_at":"2025-06-11T15:52:56.000Z","dependencies_parsed_at":"2025-02-04T07:01:45.371Z","dependency_job_id":"f54083a4-c5bb-43c0-acb3-1d758f909e75","html_url":"https://github.com/i2y/pydantic-rpc","commit_stats":null,"previous_names":["i2y/fastserve"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/i2y/pydantic-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fpydantic-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fpydantic-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fpydantic-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fpydantic-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/i2y","download_url":"https://codeload.github.com/i2y/pydantic-rpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fpydantic-rpc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260090607,"owners_count":22957249,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["connectrpc","grpc","grpc-web","protobuf","pydantic","python","python-library","python3"],"created_at":"2025-02-04T07:01:19.242Z","updated_at":"2026-02-04T01:17:18.171Z","avatar_url":"https://github.com/i2y.png","language":"Python","funding_links":[],"categories":["Web"],"sub_categories":[],"readme":"# 🚀 PydanticRPC\n\n**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.\n\n\nBelow is an example of a simple gRPC service that exposes a [PydanticAI](https://ai.pydantic.dev/) agent:\n\n```python\nimport asyncio\n\nfrom openai import AsyncOpenAI\nfrom pydantic_ai import Agent\nfrom pydantic_ai.models.openai import OpenAIModel\nfrom pydantic_rpc import AsyncIOServer, Message\n\n\n# `Message` is just an alias for Pydantic's `BaseModel` class.\nclass CityLocation(Message):\n    city: str\n    country: str\n\n\nclass Olympics(Message):\n    year: int\n\n    def prompt(self):\n        return f\"Where were the Olympics held in {self.year}?\"\n\n\nclass OlympicsLocationAgent:\n    def __init__(self):\n        client = AsyncOpenAI(\n            base_url=\"http://localhost:11434/v1\",\n            api_key=\"ollama_api_key\",\n        )\n        ollama_model = OpenAIModel(\n            model_name=\"llama3.2\",\n            openai_client=client,\n        )\n        self._agent = Agent(ollama_model)\n\n    async def ask(self, req: Olympics) -\u003e CityLocation:\n        result = await self._agent.run(req.prompt())\n        return result.data\n\n\nif __name__ == \"__main__\":\n    # New enhanced initialization API (optional - backward compatible)\n    s = AsyncIOServer(service=OlympicsLocationAgent(), port=50051)\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(s.run())\n```\n\nAnd here is an example of a simple Connect RPC service that exposes the same agent as an ASGI application:\n\n```python\nimport asyncio\n\nfrom openai import AsyncOpenAI\nfrom pydantic_ai import Agent\nfrom pydantic_ai.models.openai import OpenAIModel\nfrom pydantic_rpc import ASGIApp, Message\n\n\nclass CityLocation(Message):\n    city: str\n    country: str\n\n\nclass Olympics(Message):\n    year: int\n\n    def prompt(self):\n        return f\"Where were the Olympics held in {self.year}?\"\n\n\nclass OlympicsLocationAgent:\n    def __init__(self):\n        client = AsyncOpenAI(\n            base_url=\"http://localhost:11434/v1\",\n            api_key=\"ollama_api_key\",\n        )\n        ollama_model = OpenAIModel(\n            model_name=\"llama3.2\",\n            openai_client=client,\n        )\n        self._agent = Agent(ollama_model, result_type=CityLocation)\n\n    async def ask(self, req: Olympics) -\u003e CityLocation:\n        result = await self._agent.run(req.prompt())\n        return result.data\n\n# New enhanced initialization API (optional - backward compatible)\napp = ASGIApp(service=OlympicsLocationAgent())\n\n```\n\n\n## 💡 Key Features\n\n- 🔄 **Automatic Protobuf Generation:** Automatically creates protobuf files matching the method signatures of your Python objects.\n- ⚙️ **Dynamic Code Generation:** Generates server and client stubs using `grpcio-tools`.\n- ✅ **Pydantic Integration:** Uses `pydantic` for robust type validation and serialization.\n- 📄 **Pprotobuf File Export:** Exports the generated protobuf files for use in other languages.\n- **For gRPC:**\n  - 💚 **Health Checking:** Built-in support for gRPC health checks using `grpc_health.v1`.\n  - 🔎 **Server Reflection:** Built-in support for gRPC server reflection.\n  - ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.\n- **For Connect-RPC:**\n  - 🌐 **Full Protocol Support:** Native Connect-RPC support via `connect-python`\n  - 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming\n  - 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment\n- 🛠️ **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.\n- 🤖 **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.\n\n## ⚠️ Important Notes for Connect-RPC\n\nWhen using Connect-RPC with ASGIApp:\n\n- **Endpoint Path Format**: Connect-RPC endpoints use CamelCase method names in the path: `/\u003cpackage\u003e.\u003cservice\u003e/\u003cMethod\u003e` (e.g., `/chat.v1.ChatService/SendMessage`)\n- **Content-Type**: Set `Content-Type: application/json` or `application/connect+json` for requests\n- **HTTP/2 Requirement**: Bidirectional streaming requires HTTP/2. Use Hypercorn instead of uvicorn for HTTP/2 support\n- **Testing**: Use [buf curl](https://buf.build/docs/ecosystem/cli/curl) for testing Connect-RPC endpoints with proper streaming support\n\nFor detailed examples and testing instructions, see the [examples directory](examples/).\n\n## 📦 Installation\n\nInstall PydanticRPC via pip:\n\n```bash\npip install pydantic-rpc\n```\n\nFor CLI support with built-in server runners:\n\n```bash\npip install pydantic-rpc-cli  # Includes hypercorn and gunicorn\n```\n\n## 🆕 Enhanced Features (v0.10.0+)\n\n**Note: All new features are fully backward compatible. Existing code continues to work without modification.**\n\n### Enhanced Initialization API\nAll server classes now support optional initialization with services:\n\n```python\n# Traditional API (still works)\nserver = AsyncIOServer()\nserver.set_port(50051)\nawait server.run(MyService())\n\n# New enhanced API (optional)\nserver = AsyncIOServer(\n    service=MyService(),\n    port=50051,\n    package_name=\"my.package\"\n)\nawait server.run()\n\n# Same for ASGI/WSGI apps\napp = ASGIApp(service=MyService(), package_name=\"my.package\")\n```\n\n### Error Handling with Decorators\nAutomatically map exceptions to gRPC/Connect status codes:\n\n```python\nfrom pydantic_rpc import error_handler\nimport grpc\n\nclass MyService:\n    @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)\n    @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)\n    async def get_user(self, request: GetUserRequest) -\u003e User:\n        # Exceptions are automatically converted to proper status codes\n        if request.id not in users_db:\n            raise KeyError(f\"User {request.id} not found\")\n        return users_db[request.id]\n```\n\n## 🚀 Getting Started\n\nPydanticRPC supports two main protocols:\n- **gRPC**: Traditional gRPC services with `Server` and `AsyncIOServer`\n- **Connect-RPC**: Modern HTTP-based RPC with `ASGIApp` and `WSGIApp`\n\n### 🔧 Synchronous gRPC Service Example\n\n```python\nfrom pydantic_rpc import Server, Message\n\nclass HelloRequest(Message):\n    name: str\n\nclass HelloReply(Message):\n    message: str\n\nclass Greeter:\n    # Define methods that accepts a request and returns a response.\n    def say_hello(self, request: HelloRequest) -\u003e HelloReply:\n        return HelloReply(message=f\"Hello, {request.name}!\")\n\nif __name__ == \"__main__\":\n    server = Server()\n    server.run(Greeter())\n```\n\n### ⚙️ Asynchronous gRPC Service Example\n\n```python\nimport asyncio\n\nfrom pydantic_rpc import AsyncIOServer, Message\n\n\nclass HelloRequest(Message):\n    name: str\n\n\nclass HelloReply(Message):\n    message: str\n\n\nclass Greeter:\n    async def say_hello(self, request: HelloRequest) -\u003e HelloReply:\n        return HelloReply(message=f\"Hello, {request.name}!\")\n\n\nasync def main():\n    # You can specify a custom port (default is 50051)\n    server = AsyncIOServer(port=50052)\n    await server.run(Greeter())\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThe AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.\n\n### 🌐 Connect-RPC ASGI Application Example\n\n```python\nfrom pydantic_rpc import ASGIApp, Message\n\nclass HelloRequest(Message):\n    name: str\n\nclass HelloReply(Message):\n    message: str\n\nclass Greeter:\n    async def say_hello(self, request: HelloRequest) -\u003e HelloReply:\n        return HelloReply(message=f\"Hello, {request.name}!\")\n\napp = ASGIApp()\napp.mount(Greeter())\n\n# Run with uvicorn:\n# uvicorn script:app --host 0.0.0.0 --port 8000\n```\n\n### 🌐 Connect-RPC WSGI Application Example\n\n```python\nfrom pydantic_rpc import WSGIApp, Message\n\nclass HelloRequest(Message):\n    name: str\n\nclass HelloReply(Message):\n    message: str\n\nclass Greeter:\n    def say_hello(self, request: HelloRequest) -\u003e HelloReply:\n        return HelloReply(message=f\"Hello, {request.name}!\")\n\napp = WSGIApp()\napp.mount(Greeter())\n\n# Run with gunicorn:\n# gunicorn script:app\n```\n\n### 🏆 Connect-RPC with Streaming Example\n\nPydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:\n\n```bash\n# Run with uvicorn\nuv run uvicorn greeting_asgi:app --port 3000\n\n# Or run streaming example\nuv run python examples/streaming_connect_python.py\n```\n\nThis will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.\n\n#### Streaming Support with connect-python\n\nconnect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):\n\n```python\nfrom typing import AsyncIterator\nfrom pydantic_rpc import ASGIApp, Message\n\nclass StreamRequest(Message):\n    text: str\n    count: int\n\nclass StreamResponse(Message):\n    text: str\n    index: int\n\nclass StreamingService:\n    # Server streaming\n    async def server_stream(self, request: StreamRequest) -\u003e AsyncIterator[StreamResponse]:\n        for i in range(request.count):\n            yield StreamResponse(text=f\"{request.text}_{i}\", index=i)\n    \n    # Client streaming\n    async def client_stream(self, requests: AsyncIterator[StreamRequest]) -\u003e StreamResponse:\n        texts = []\n        async for req in requests:\n            texts.append(req.text)\n        return StreamResponse(text=\" \".join(texts), index=len(texts))\n    \n    # Bidirectional streaming\n    async def bidi_stream(\n        self, requests: AsyncIterator[StreamRequest]\n    ) -\u003e AsyncIterator[StreamResponse]:\n        idx = 0\n        async for req in requests:\n            yield StreamResponse(text=f\"Echo: {req.text}\", index=idx)\n            idx += 1\n\napp = ASGIApp()\napp.mount(StreamingService())\n```\n\n\u003e [!NOTE]\n\u003e Please install `protoc-gen-connect-python` to run the connect-python example.\n\n## ♻️ Skipping Protobuf Generation\nBy default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:\n\n```bash\nexport PYDANTIC_RPC_SKIP_GENERATION=true\n```\n\nWhen this variable is set to \"true\", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.\n\n## 🪧 Setting Protobuf and Connect RPC/gRPC generation directory\nBy 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:\n\n```bash\nexport PYDANTIC_RPC_PROTO_PATH=/your/path\n```\n\n## ⚠️ Reserved Fields\n\nYou can also set an environment variable to reserve a set number of fields for proto generation, for backward and forward compatibility.\n\n```bash\nexport PYDANTIC_RPC_RESERVED_FIELDS=1\n```\n\n## 💎 Advanced Features\n\n### 🌊 Response Streaming (gRPC)\nPydanticRPC supports streaming responses for both gRPC and Connect-RPC services.\nIf a service class method's return type is `typing.AsyncIterator[T]`, the method is considered a streaming method.\n\n\nPlease see the sample code below:\n\n```python\nimport asyncio\nfrom typing import Annotated, AsyncIterator\n\nfrom openai import AsyncOpenAI\nfrom pydantic import Field\nfrom pydantic_ai import Agent\nfrom pydantic_ai.models.openai import OpenAIModel\nfrom pydantic_rpc import AsyncIOServer, Message\n\n\n# `Message` is just a pydantic BaseModel alias\nclass CityLocation(Message):\n    city: Annotated[str, Field(description=\"The city where the Olympics were held\")]\n    country: Annotated[\n        str, Field(description=\"The country where the Olympics were held\")\n    ]\n\n\nclass OlympicsQuery(Message):\n    year: Annotated[int, Field(description=\"The year of the Olympics\", ge=1896)]\n\n    def prompt(self):\n        return f\"Where were the Olympics held in {self.year}?\"\n\n\nclass OlympicsDurationQuery(Message):\n    start: Annotated[int, Field(description=\"The start year of the Olympics\", ge=1896)]\n    end: Annotated[int, Field(description=\"The end year of the Olympics\", ge=1896)]\n\n    def prompt(self):\n        return f\"From {self.start} to {self.end}, how many Olympics were held? Please provide the list of countries and cities.\"\n\n\nclass StreamingResult(Message):\n    answer: Annotated[str, Field(description=\"The answer to the query\")]\n\n\nclass OlympicsAgent:\n    def __init__(self):\n        client = AsyncOpenAI(\n            base_url='http://localhost:11434/v1',\n            api_key='ollama_api_key',\n        )\n        ollama_model = OpenAIModel(\n            model_name='llama3.2',\n            openai_client=client,\n        )\n        self._agent = Agent(ollama_model)\n\n    async def ask(self, req: OlympicsQuery) -\u003e CityLocation:\n        result = await self._agent.run(req.prompt(), result_type=CityLocation)\n        return result.data\n\n    async def ask_stream(\n        self, req: OlympicsDurationQuery\n    ) -\u003e AsyncIterator[StreamingResult]:\n        async with self._agent.run_stream(req.prompt(), result_type=str) as result:\n            async for data in result.stream_text(delta=True):\n                yield StreamingResult(answer=data)\n\n\nif __name__ == \"__main__\":\n    s = AsyncIOServer()\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(s.run(OlympicsAgent()))\n```\n\nIn 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.\n\nNow, you can call the `ask_stream` method of the server described above using your preferred gRPC client tool. The example below uses `buf curl`.\n\n\n```console\n% buf curl --data '{\"start\": 1980, \"end\": 2024}' -v http://localhost:50051/olympicsagent.v1.OlympicsAgent/AskStream --protocol grpc --http2-prior-knowledge \n\nbuf: * Using server reflection to resolve \"olympicsagent.v1.OlympicsAgent\"\nbuf: * Dialing (tcp) localhost:50051...\nbuf: * Connected to [::1]:50051\nbuf: \u003e (#1) POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo\nbuf: \u003e (#1) Accept-Encoding: identity\nbuf: \u003e (#1) Content-Type: application/grpc+proto\nbuf: \u003e (#1) Grpc-Accept-Encoding: gzip\nbuf: \u003e (#1) Grpc-Timeout: 119997m\nbuf: \u003e (#1) Te: trailers\nbuf: \u003e (#1) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1\nbuf: \u003e (#1)\nbuf: } (#1) [5 bytes data]\nbuf: } (#1) [32 bytes data]\nbuf: \u003c (#1) HTTP/2.0 200 OK\nbuf: \u003c (#1) Content-Type: application/grpc\nbuf: \u003c (#1) Grpc-Message: Method not found!\nbuf: \u003c (#1) Grpc-Status: 12\nbuf: \u003c (#1)\nbuf: * (#1) Call complete\nbuf: \u003e (#2) POST /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo\nbuf: \u003e (#2) Accept-Encoding: identity\nbuf: \u003e (#2) Content-Type: application/grpc+proto\nbuf: \u003e (#2) Grpc-Accept-Encoding: gzip\nbuf: \u003e (#2) Grpc-Timeout: 119967m\nbuf: \u003e (#2) Te: trailers\nbuf: \u003e (#2) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1\nbuf: \u003e (#2)\nbuf: } (#2) [5 bytes data]\nbuf: } (#2) [32 bytes data]\nbuf: \u003c (#2) HTTP/2.0 200 OK\nbuf: \u003c (#2) Content-Type: application/grpc\nbuf: \u003c (#2) Grpc-Accept-Encoding: identity, deflate, gzip\nbuf: \u003c (#2)\nbuf: { (#2) [5 bytes data]\nbuf: { (#2) [434 bytes data]\nbuf: * Server reflection has resolved file \"olympicsagent.proto\"\nbuf: * Invoking RPC olympicsagent.v1.OlympicsAgent.AskStream\nbuf: \u003e (#3) POST /olympicsagent.v1.OlympicsAgent/AskStream\nbuf: \u003e (#3) Accept-Encoding: identity\nbuf: \u003e (#3) Content-Type: application/grpc+proto\nbuf: \u003e (#3) Grpc-Accept-Encoding: gzip\nbuf: \u003e (#3) Grpc-Timeout: 119947m\nbuf: \u003e (#3) Te: trailers\nbuf: \u003e (#3) User-Agent: grpc-go-connect/1.12.0 (go1.21.4) buf/1.28.1\nbuf: \u003e (#3)\nbuf: } (#3) [5 bytes data]\nbuf: } (#3) [6 bytes data]\nbuf: * (#3) Finished upload\nbuf: \u003c (#3) HTTP/2.0 200 OK\nbuf: \u003c (#3) Content-Type: application/grpc\nbuf: \u003c (#3) Grpc-Accept-Encoding: identity, deflate, gzip\nbuf: \u003c (#3)\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [25 bytes data]\n{\n \"answer\": \"Here's a list of Summer\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [31 bytes data]\n{\n  \"answer\": \" and Winter Olympics from 198\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [29 bytes data]\n{\n  \"answer\": \"0 to 2024:\\n\\nSummer Olympics\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \":\\n1. 1980 - Moscow\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \", Soviet Union\\n2. \"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [32 bytes data]\n{\n  \"answer\": \"1984 - Los Angeles, California\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [15 bytes data]\n{\n  \"answer\": \", USA\\n3. 1988\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [26 bytes data]\n{\n  \"answer\": \" - Seoul, South Korea\\n4.\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [27 bytes data]\n{\n  \"answer\": \" 1992 - Barcelona, Spain\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \"5. 1996 - Atlanta,\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [22 bytes data]\n{\n  \"answer\": \" Georgia, USA\\n6. 200\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [26 bytes data]\n{\n  \"answer\": \"0 - Sydney, Australia\\n7.\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [25 bytes data]\n{\n  \"answer\": \" 2004 - Athens, Greece\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \"8. 2008 - Beijing,\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [18 bytes data]\n{\n  \"answer\": \" China\\n9. 2012 -\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [29 bytes data]\n{\n  \"answer\": \" London, United Kingdom\\n10.\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [24 bytes data]\n{\n  \"answer\": \" 2016 - Rio de Janeiro\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [18 bytes data]\n{\n  \"answer\": \", Brazil\\n11. 202\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [24 bytes data]\n{\n  \"answer\": \"0 - Tokyo, Japan (held\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [21 bytes data]\n{\n  \"answer\": \" in 2021 due to the\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [26 bytes data]\n{\n  \"answer\": \" COVID-19 pandemic)\\n12. \"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [28 bytes data]\n{\n  \"answer\": \"2024 - Paris, France\\n\\nNote\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [41 bytes data]\n{\n  \"answer\": \": The Olympics were held without a host\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [26 bytes data]\n{\n  \"answer\": \" city for one year (2022\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [42 bytes data]\n{\n  \"answer\": \", due to the Russian invasion of Ukraine\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [29 bytes data]\n{\n  \"answer\": \").\\n\\nWinter Olympics:\\n1. 198\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [27 bytes data]\n{\n  \"answer\": \"0 - Lake Placid, New York\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [15 bytes data]\n{\n  \"answer\": \", USA\\n2. 1984\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [27 bytes data]\n{\n  \"answer\": \" - Sarajevo, Yugoslavia (\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [30 bytes data]\n{\n  \"answer\": \"now Bosnia and Herzegovina)\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \"3. 1988 - Calgary,\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [25 bytes data]\n{\n  \"answer\": \" Alberta, Canada\\n4. 199\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [26 bytes data]\n{\n  \"answer\": \"2 - Albertville, France\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [13 bytes data]\n{\n  \"answer\": \"5. 1994 - L\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [24 bytes data]\n{\n  \"answer\": \"illehammer, Norway\\n6. \"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [23 bytes data]\n{\n  \"answer\": \"1998 - Nagano, Japan\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [16 bytes data]\n{\n  \"answer\": \"7. 2002 - Salt\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [24 bytes data]\n{\n  \"answer\": \" Lake City, Utah, USA\\n\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [18 bytes data]\n{\n  \"answer\": \"8. 2006 - Torino\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [17 bytes data]\n{\n  \"answer\": \", Italy\\n9. 2010\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [40 bytes data]\n{\n  \"answer\": \" - Vancouver, British Columbia, Canada\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [13 bytes data]\n{\n  \"answer\": \"\\n10. 2014 -\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [20 bytes data]\n{\n  \"answer\": \" Sochi, Russia\\n11.\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [16 bytes data]\n{\n  \"answer\": \" 2018 - Pyeong\"\n}\nbuf: { (#3) [5 bytes data]\nbuf: { (#3) [24 bytes data]\n{\n  \"answer\": \"chang, South Korea\\n12.\"\n}\nbuf: \u003c (#3)\nbuf: \u003c (#3) Grpc-Message:\nbuf: \u003c (#3) Grpc-Status: 0\nbuf: * (#3) Call complete\nbuf: \u003c (#2)\nbuf: \u003c (#2) Grpc-Message:\nbuf: \u003c (#2) Grpc-Status: 0\nbuf: * (#2) Call complete\n%\n```\n\n### 🪶 Empty Messages\n\nEmpty request/response messages are automatically mapped to `google.protobuf.Empty`:\n\n```python\nfrom pydantic_rpc import AsyncIOServer, Message\n\n\nclass EmptyRequest(Message):\n    pass  # Automatically uses google.protobuf.Empty\n\n\nclass GreetingResponse(Message):\n    message: str\n\n\nclass GreetingService:\n    async def say_hello(self, request: EmptyRequest) -\u003e GreetingResponse:\n        return GreetingResponse(message=\"Hello!\")\n    \n    async def get_default_greeting(self) -\u003e GreetingResponse:\n        # Method with no request parameter (implicitly empty)\n        return GreetingResponse(message=\"Hello, World!\")\n```\n\n### 🎨 Custom Serialization\n\nPydantic's serialization decorators are fully supported:\n\n```python\nfrom typing import Any\nfrom pydantic import field_serializer, model_serializer\nfrom pydantic_rpc import Message\n\n\nclass UserMessage(Message):\n    name: str\n    age: int\n    \n    @field_serializer('name')\n    def serialize_name(self, name: str) -\u003e str:\n        \"\"\"Always uppercase the name when serializing.\"\"\"\n        return name.upper()\n\n\nclass ComplexMessage(Message):\n    value: int\n    multiplier: int\n    \n    @model_serializer\n    def serialize_model(self) -\u003e dict[str, Any]:\n        \"\"\"Custom serialization with computed fields.\"\"\"\n        return {\n            'value': self.value,\n            'multiplier': self.multiplier,\n            'result': self.value * self.multiplier  # Computed field\n        }\n```\n\nThe serializers are automatically applied when converting between Pydantic models and protobuf messages.\n\n#### ⚠️ Limitations and Considerations\n\n**1. Nested Message serializers are now supported (v0.8.0+)**\n```python\nclass Address(Message):\n    city: str\n    \n    @field_serializer(\"city\")\n    def serialize_city(self, city: str) -\u003e str:\n        return city.upper()\n\nclass User(Message):\n    name: str\n    address: Address  # ← Address's serializers ARE applied with DEEP strategy\n    \n    @field_serializer(\"name\")\n    def serialize_name(self, name: str) -\u003e str:\n        return name.upper()  # ← This IS applied\n```\n\n**Serializer Strategy Control:**\nYou can control how nested serializers are applied via environment variable:\n```bash\n# Apply serializers at all nesting levels (default)\nexport PYDANTIC_RPC_SERIALIZER_STRATEGY=deep\n\n# Apply only top-level serializers\nexport PYDANTIC_RPC_SERIALIZER_STRATEGY=shallow\n\n# Disable all serializers\nexport PYDANTIC_RPC_SERIALIZER_STRATEGY=none\n```\n\n**Performance Impact:**\n- DEEP strategy: ~4% overhead for simple nested structures\n- SHALLOW strategy: ~2% overhead (only top-level)\n- NONE strategy: No overhead (serializers disabled)\n\n**2. New fields added by serializers are ignored**\n```python\nclass ComplexMessage(Message):\n    value: int\n    multiplier: int\n    \n    @model_serializer\n    def serialize_model(self) -\u003e dict[str, Any]:\n        return {\n            \"value\": self.value,\n            \"multiplier\": self.multiplier,\n            \"result\": self.value * self.multiplier  # ← Won't appear in protobuf\n        }\n```\n**Problem**: The `result` field doesn't exist in the Message definition, so it's not in the protobuf schema.\n\n**3. Type must remain consistent**\n```python\nclass BadExample(Message):\n    number: int\n    \n    @field_serializer(\"number\")\n    def serialize_number(self, number: int) -\u003e str:  # ❌ int → str\n        return str(number)  # This will cause issues\n```\n\n**4. Union/Optional fields have limited support**\n```python\nclass UnionExample(Message):\n    data: str | int | None  # Union type\n    \n    @field_serializer(\"data\")\n    def serialize_data(self, data: str | int | None) -\u003e str | int | None:\n        # Serializer may not be applied to Union types\n        return data\n```\n\n**5. Errors fail silently with fallback**\n```python\nclass RiskyMessage(Message):\n    value: int\n    \n    @field_serializer(\"value\")\n    def serialize_value(self, value: int) -\u003e int:\n        if value == 0:\n            raise ValueError(\"Cannot serialize zero\")\n        return value * 2\n\n# If error occurs, original value is used (silent fallback)\n```\n\n**6. Circular references are handled gracefully**\n```python\nclass Node(Message):\n    value: str\n    child: \"Node | None\" = None\n    \n    @field_serializer(\"value\")\n    def serialize_value(self, v: str) -\u003e str:\n        return v.upper()\n\n# Circular references are detected and prevented\nnode1 = Node(value=\"first\")\nnode2 = Node(value=\"second\")\nnode1.child = node2\nnode2.child = node1  # Circular reference\n\n# When converting to protobuf:\n# - Circular references are detected\n# - Empty proto is returned for repeated objects\n# - No infinite recursion occurs\n# Note: Pydantic's model_dump() will fail on circular refs,\n#       so serializers won't be applied in this case\n```\n\n**✅ Recommended Usage:**\n```python\nclass GoodMessage(Message):\n    # Use with primitive types\n    name: str\n    age: int\n    \n    @field_serializer(\"name\")\n    def normalize_name(self, name: str) -\u003e str:\n        return name.strip().title()  # Normalization\n    \n    @field_serializer(\"age\")\n    def clamp_age(self, age: int) -\u003e int:\n        return max(0, min(age, 150))  # Range limiting\n```\n\n**Best Practices:**\n- Use serializers primarily for primitive types (str, int, float, bool)\n- Keep type consistency (int → int, str → str)\n- Avoid complex transformations or side effects\n- Test error cases thoroughly\n- Be aware that errors fail silently\n\n### 🔒 TLS/mTLS Support\n\nPydanticRPC provides built-in support for TLS (Transport Layer Security) and mTLS (mutual TLS) for secure gRPC communication.\n\n```python\nfrom pydantic_rpc import AsyncIOServer, GrpcTLSConfig, extract_peer_identity\nimport grpc\n\n# Basic TLS (server authentication only)\ntls_config = GrpcTLSConfig(\n    cert_chain=server_cert_bytes,\n    private_key=server_key_bytes,\n    require_client_cert=False\n)\n\n# mTLS (mutual authentication)\ntls_config = GrpcTLSConfig(\n    cert_chain=server_cert_bytes,\n    private_key=server_key_bytes,\n    root_certs=ca_cert_bytes,  # CA to verify client certificates\n    require_client_cert=True\n)\n\n# Create server with TLS\nserver = AsyncIOServer(tls=tls_config)\n\n# Extract client identity in service methods\nclass SecureService:\n    async def secure_method(self, request, context: grpc.ServicerContext):\n        client_identity = extract_peer_identity(context)\n        if client_identity:\n            print(f\"Authenticated client: {client_identity}\")\n```\n\nFor a complete example, see [examples/tls_server.py](examples/tls_server.py) and [examples/tls_client.py](examples/tls_client.py).\n\n### 🔗 Multiple Services with Custom Interceptors\n\nPydanticRPC supports defining and running multiple gRPC services in a single server:\n\n```python\nfrom datetime import datetime\nimport grpc\nfrom grpc import ServicerContext\n\nfrom pydantic_rpc import Server, Message\n\n\nclass FooRequest(Message):\n    name: str\n    age: int\n    d: dict[str, str]\n\n\nclass FooResponse(Message):\n    name: str\n    age: int\n    d: dict[str, str]\n\n\nclass BarRequest(Message):\n    names: list[str]\n\n\nclass BarResponse(Message):\n    names: list[str]\n\n\nclass FooService:\n    def foo(self, request: FooRequest) -\u003e FooResponse:\n        return FooResponse(name=request.name, age=request.age, d=request.d)\n\n\nclass MyMessage(Message):\n    name: str\n    age: int\n    o: int | datetime\n\n\nclass Request(Message):\n    name: str\n    age: int\n    d: dict[str, str]\n    m: MyMessage\n\n\nclass Response(Message):\n    name: str\n    age: int\n    d: dict[str, str]\n    m: MyMessage | str\n\n\nclass BarService:\n    def bar(self, req: BarRequest, ctx: ServicerContext) -\u003e BarResponse:\n        return BarResponse(names=req.names)\n\n\nclass CustomInterceptor(grpc.ServerInterceptor):\n    def intercept_service(self, continuation, handler_call_details):\n        # do something\n        print(handler_call_details.method)\n        return continuation(handler_call_details)\n\n\nasync def app(scope, receive, send):\n    pass\n\n\nif __name__ == \"__main__\":\n    s = Server(10, CustomInterceptor())\n    s.run(\n        FooService(),\n        BarService(),\n    )\n```\n\n### 🩺 [TODO] Custom Health Check\n\nTODO\n\n### 🤖 MCP (Model Context Protocol) Support\n\nPydanticRPC can expose your services as MCP tools for AI assistants using FastMCP. This enables seamless integration with any MCP-compatible client.\n\n#### Stdio Mode Example\n\n```python\nfrom pydantic_rpc import Message\nfrom pydantic_rpc.mcp import MCPExporter\n\nclass CalculateRequest(Message):\n    expression: str\n\nclass CalculateResponse(Message):\n    result: float\n\nclass MathService:\n    def calculate(self, req: CalculateRequest) -\u003e CalculateResponse:\n        result = eval(req.expression, {\"__builtins__\": {}}, {})\n        return CalculateResponse(result=float(result))\n\n# Run as MCP stdio server\nif __name__ == \"__main__\":\n    service = MathService()\n    mcp = MCPExporter(service)\n    mcp.run_stdio()\n```\n\n#### Configuring MCP Clients\n\nAny MCP-compatible client can connect to your service. For example, to configure Claude Desktop:\n\n```json\n{\n  \"mcpServers\": {\n    \"my-math-service\": {\n      \"command\": \"python\",\n      \"args\": [\"/path/to/math_mcp_server.py\"]\n    }\n  }\n}\n```\n\n#### HTTP/ASGI Mode Example\n\nMCP can also be mounted to existing ASGI applications:\n\n```python\nfrom pydantic_rpc import ASGIApp\nfrom pydantic_rpc.mcp import MCPExporter\n\n# Create Connect-RPC ASGI app\napp = ASGIApp()\napp.mount(MathService())\n\n# Add MCP support via HTTP/SSE\nmcp = MCPExporter(MathService())\nmcp.mount_to_asgi(app, path=\"/mcp\")\n\n# Run with uvicorn\nimport uvicorn\nuvicorn.run(app, host=\"127.0.0.1\", port=8000)\n```\n\nMCP endpoints will be available at:\n- SSE: `GET http://localhost:8000/mcp/sse`\n- Messages: `POST http://localhost:8000/mcp/messages/`\n\n### 🗄️ CLI Tool (pydantic-rpc-cli)\n\nThe CLI tool provides powerful features for generating protobuf files and running servers. Install it separately:\n\n```bash\npip install pydantic-rpc-cli\n```\n\n#### Generate Protobuf Files\n\n```bash\n# Generate .proto file from a service class\npydantic-rpc generate myapp.services.UserService --output ./proto/\n\n# Also compile to Python code\npydantic-rpc generate myapp.services.UserService --compile\n```\n\n#### Run Servers Directly\n\nThe CLI can run any type of server:\n\n```bash\n# Run as gRPC server (auto-detects async/sync)\npydantic-rpc serve myapp.services.UserService --port 50051\n\n# Run as Connect-RPC with ASGI (HTTP/2, uses Hypercorn)\npydantic-rpc serve myapp.services.UserService --asgi --port 8000\n\n# Run as Connect-RPC with WSGI (HTTP/1.1, uses Gunicorn)\npydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4\n```\n\nUsing the generated proto files with tools like `protoc`, `buf` and `BSR`, you can generate code for any desired language.\n\n\n## 📖 Data Type Mapping\n\n| Python Type                    | Protobuf Type             |\n|--------------------------------|---------------------------|\n| str                            | string                    |\n| bytes                          | bytes                     |\n| bool                           | bool                      |\n| int                            | int32                     |\n| float                          | float, double             |\n| list[T], tuple[T]              | repeated T                |\n| dict[K, V]                     | map\u003cK, V\u003e                 |\n| datetime.datetime              | google.protobuf.Timestamp |\n| datetime.timedelta             | google.protobuf.Duration  |\n| typing.Union[A, B]             | oneof A, B                |\n| subclass of enum.Enum          | enum                      |\n| subclass of pydantic.BaseModel | message                   |\n\n\n## ⚠️ Known Limitations\n\n### Union Types with Collections\n\nDue 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.\n\n**❌ Not Supported:**\n```python\nfrom typing import Union, List, Dict\nfrom pydantic_rpc import Message\n\n# These will fail during proto compilation\nclass MyMessage(Message):\n    # Union with list - NOT SUPPORTED\n    field1: Union[List[int], str]\n\n    # Union with dict - NOT SUPPORTED\n    field2: Union[Dict[str, int], int]\n\n    # Union with nested collections - NOT SUPPORTED\n    field3: Union[List[Dict[str, int]], str]\n```\n\n**✅ Workaround - Use Message Wrappers:**\n```python\nfrom typing import Union, List, Dict\nfrom pydantic_rpc import Message\n\n# Wrap collections in Message types\nclass IntList(Message):\n    values: List[int]\n\nclass StringIntMap(Message):\n    values: Dict[str, int]\n\nclass MyMessage(Message):\n    # Now these work!\n    field1: Union[IntList, str]\n    field2: Union[StringIntMap, int]\n```\n\nThis approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.\n\n## 🔧 Development\n\nThis project uses [`just`](https://github.com/casey/just) as a command runner for development tasks.\n\n### Installing just\n\n**macOS:**\n```bash\nbrew install just\n```\n\n**Linux:**\n```bash\ncurl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin\n```\n\n**Windows:**\nDownload from [GitHub releases](https://github.com/casey/just/releases)\n\n### Quick Start\n\n```bash\n# Install dependencies\njust install\n\n# Run tests\njust test  # or just t\n\n# Format and lint code\njust format  # or just f\njust lint    # or just l\n\n# Run all checks (lint + tests)\njust check   # or just c\n\n# See all available commands\njust --list\n```\n\n### Running Examples\n\n```bash\n# Start servers\njust greeting-server  # gRPC server on port 50051\njust greeting-asgi    # Connect RPC ASGI on port 8000\njust greeting-wsgi    # Connect RPC WSGI on port 3000\n\n# Test with buf curl (in another terminal)\njust greet            # gRPC request\njust connect-greet    # Connect RPC request\njust wsgi-greet       # WSGI request\n\n# Custom names\njust greet-name Alice\njust connect-greet-name Bob\n```\n\nFor more development commands and options, see the [Justfile](Justfile) or run `just --list`.\n\n## TODO\n- [x] Streaming Support\n  - [x] unary-stream\n  - [x] stream-unary\n  - [x] stream-stream\n- [x] Empty Message Support (automatic google.protobuf.Empty)\n- [x] Pydantic Serializer Support (@model_serializer, @field_serializer)\n- [ ] Custom Health Check Support\n- [x] MCP (Model Context Protocol) Support via official MCP SDK\n- [ ] Add more examples\n- [x] Add tests\n\n## 📜 License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fpydantic-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fi2y%2Fpydantic-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fpydantic-rpc/lists"}