pydantic-rpc 0.8.0__py3-none-any.whl → 0.10.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.
@@ -0,0 +1,138 @@
1
+ """Decorators for adding protobuf options to RPC methods."""
2
+
3
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
4
+ from functools import wraps
5
+
6
+ from .options import OptionMetadata, OPTION_METADATA_ATTR
7
+
8
+
9
+ F = TypeVar("F", bound=Callable[..., Any])
10
+
11
+
12
+ def http_option(
13
+ method: str,
14
+ path: str,
15
+ body: Optional[str] = None,
16
+ response_body: Optional[str] = None,
17
+ additional_bindings: Optional[List[Dict[str, Any]]] = None,
18
+ ) -> Callable[[F], F]:
19
+ """
20
+ Decorator to add google.api.http option to an RPC method.
21
+
22
+ Args:
23
+ method: HTTP method (GET, POST, PUT, DELETE, PATCH)
24
+ path: URL path template (e.g., "/v1/books/{id}")
25
+ body: Request body mapping (e.g., "*" for entire body)
26
+ response_body: Response body mapping (specific field to return)
27
+ additional_bindings: List of additional HTTP bindings
28
+
29
+ Example:
30
+ @http_option(method="GET", path="/v1/books/{id}")
31
+ async def get_book(self, request: GetBookRequest) -> Book:
32
+ ...
33
+ """
34
+
35
+ def decorator(func: F) -> F:
36
+ # Get or create option metadata
37
+ if not hasattr(func, OPTION_METADATA_ATTR):
38
+ setattr(func, OPTION_METADATA_ATTR, OptionMetadata())
39
+
40
+ metadata: OptionMetadata = getattr(func, OPTION_METADATA_ATTR)
41
+
42
+ # Set HTTP option
43
+ metadata.set_http_option(
44
+ method=method,
45
+ path=path,
46
+ body=body,
47
+ response_body=response_body,
48
+ additional_bindings=additional_bindings,
49
+ )
50
+
51
+ @wraps(func)
52
+ def wrapper(*args, **kwargs):
53
+ return func(*args, **kwargs)
54
+
55
+ # Preserve the metadata on the wrapper
56
+ setattr(wrapper, OPTION_METADATA_ATTR, metadata)
57
+
58
+ return wrapper # type: ignore
59
+
60
+ return decorator
61
+
62
+
63
+ def proto_option(name: str, value: Any) -> Callable[[F], F]:
64
+ """
65
+ Decorator to add a generic protobuf option to an RPC method.
66
+
67
+ Args:
68
+ name: Option name (e.g., "deprecated", "idempotency_level")
69
+ value: Option value
70
+
71
+ Example:
72
+ @proto_option("deprecated", True)
73
+ @proto_option("idempotency_level", "IDEMPOTENT")
74
+ async def old_method(self, request: Request) -> Response:
75
+ ...
76
+ """
77
+
78
+ def decorator(func: F) -> F:
79
+ # Get or create option metadata
80
+ if not hasattr(func, OPTION_METADATA_ATTR):
81
+ setattr(func, OPTION_METADATA_ATTR, OptionMetadata())
82
+
83
+ metadata: OptionMetadata = getattr(func, OPTION_METADATA_ATTR)
84
+
85
+ # Add proto option
86
+ metadata.add_proto_option(name=name, value=value)
87
+
88
+ @wraps(func)
89
+ def wrapper(*args, **kwargs):
90
+ return func(*args, **kwargs)
91
+
92
+ # Preserve the metadata on the wrapper
93
+ setattr(wrapper, OPTION_METADATA_ATTR, metadata)
94
+
95
+ return wrapper # type: ignore
96
+
97
+ return decorator
98
+
99
+
100
+ def get_method_options(method: Callable) -> Optional[OptionMetadata]:
101
+ """
102
+ Get option metadata from a method.
103
+
104
+ Args:
105
+ method: The method to get options from
106
+
107
+ Returns:
108
+ OptionMetadata if present, None otherwise
109
+ """
110
+ return getattr(method, OPTION_METADATA_ATTR, None)
111
+
112
+
113
+ def has_http_option(method: Callable) -> bool:
114
+ """
115
+ Check if a method has an HTTP option.
116
+
117
+ Args:
118
+ method: The method to check
119
+
120
+ Returns:
121
+ True if the method has an HTTP option, False otherwise
122
+ """
123
+ metadata = get_method_options(method)
124
+ return metadata is not None and metadata.http_option is not None
125
+
126
+
127
+ def has_proto_options(method: Callable) -> bool:
128
+ """
129
+ Check if a method has any proto options.
130
+
131
+ Args:
132
+ method: The method to check
133
+
134
+ Returns:
135
+ True if the method has proto options, False otherwise
136
+ """
137
+ metadata = get_method_options(method)
138
+ return metadata is not None and len(metadata.proto_options) > 0
@@ -0,0 +1,134 @@
1
+ """Protocol Buffer options support for pydantic-rpc."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class HttpBinding(BaseModel):
8
+ """HTTP binding configuration for a single HTTP method."""
9
+
10
+ method: str = Field(..., description="HTTP method (get, post, put, delete, patch)")
11
+ path: Optional[str] = Field(None, description="URL path template")
12
+ body: Optional[str] = Field(None, description="Request body mapping")
13
+ response_body: Optional[str] = Field(None, description="Response body mapping")
14
+
15
+ def to_proto_dict(self) -> Dict[str, Any]:
16
+ """Convert to protobuf option format."""
17
+ result = {}
18
+ if self.path:
19
+ result[self.method.lower()] = self.path
20
+ if self.body:
21
+ result["body"] = self.body
22
+ if self.response_body:
23
+ result["response_body"] = self.response_body
24
+ return result
25
+
26
+
27
+ class HttpOption(BaseModel):
28
+ """Google API HTTP option configuration."""
29
+
30
+ method: str = Field(..., description="Primary HTTP method")
31
+ path: str = Field(..., description="Primary URL path template")
32
+ body: Optional[str] = Field(None, description="Request body mapping")
33
+ response_body: Optional[str] = Field(None, description="Response body mapping")
34
+ additional_bindings: List[Dict[str, Any]] = Field(
35
+ default_factory=list, description="Additional HTTP bindings"
36
+ )
37
+
38
+ def to_proto_string(self) -> str:
39
+ """Convert to protobuf option string format."""
40
+ lines = []
41
+ lines.append("option (google.api.http) = {")
42
+
43
+ # Primary binding
44
+ lines.append(f' {self.method.lower()}: "{self.path}"')
45
+
46
+ # Body mapping
47
+ if self.body:
48
+ lines.append(f' body: "{self.body}"')
49
+
50
+ # Response body mapping
51
+ if self.response_body:
52
+ lines.append(f' response_body: "{self.response_body}"')
53
+
54
+ # Additional bindings
55
+ for binding in self.additional_bindings:
56
+ lines.append(" additional_bindings {")
57
+ for key, value in binding.items():
58
+ if key == "body":
59
+ lines.append(f' {key}: "{value}"')
60
+ else:
61
+ lines.append(f' {key}: "{value}"')
62
+ lines.append(" }")
63
+
64
+ lines.append("};")
65
+ return "\n".join(lines)
66
+
67
+
68
+ class ProtoOption(BaseModel):
69
+ """Generic protocol buffer option."""
70
+
71
+ name: str = Field(..., description="Option name")
72
+ value: Any = Field(..., description="Option value")
73
+
74
+ def to_proto_string(self) -> str:
75
+ """Convert to protobuf option string format."""
76
+ if isinstance(self.value, bool):
77
+ value_str = "true" if self.value else "false"
78
+ elif isinstance(self.value, str):
79
+ # Check if it's an enum value (no quotes) or string literal (quotes)
80
+ if self.value.isupper() or "_" in self.value:
81
+ # Likely an enum value
82
+ value_str = self.value
83
+ else:
84
+ value_str = f'"{self.value}"'
85
+ else:
86
+ value_str = str(self.value)
87
+
88
+ return f"option {self.name} = {value_str};"
89
+
90
+
91
+ class OptionMetadata(BaseModel):
92
+ """Metadata container for method/service options."""
93
+
94
+ http_option: Optional[HttpOption] = None
95
+ proto_options: List[ProtoOption] = Field(default_factory=list)
96
+
97
+ def add_proto_option(self, name: str, value: Any) -> None:
98
+ """Add a generic proto option."""
99
+ self.proto_options.append(ProtoOption(name=name, value=value))
100
+
101
+ def set_http_option(
102
+ self,
103
+ method: str,
104
+ path: str,
105
+ body: Optional[str] = None,
106
+ response_body: Optional[str] = None,
107
+ additional_bindings: Optional[List[Dict[str, Any]]] = None,
108
+ ) -> None:
109
+ """Set the HTTP option configuration."""
110
+ self.http_option = HttpOption(
111
+ method=method,
112
+ path=path,
113
+ body=body,
114
+ response_body=response_body,
115
+ additional_bindings=additional_bindings or [],
116
+ )
117
+
118
+ def to_proto_strings(self) -> List[str]:
119
+ """Convert all options to protobuf strings."""
120
+ result = []
121
+
122
+ # Add HTTP option first if present
123
+ if self.http_option:
124
+ result.append(self.http_option.to_proto_string())
125
+
126
+ # Add other proto options
127
+ for option in self.proto_options:
128
+ result.append(option.to_proto_string())
129
+
130
+ return result
131
+
132
+
133
+ # Option metadata attribute name used on methods
134
+ OPTION_METADATA_ATTR = "__pydantic_rpc_options__"
pydantic_rpc/tls.py ADDED
@@ -0,0 +1,96 @@
1
+ """TLS configuration for gRPC servers."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Any
5
+ import grpc
6
+
7
+
8
+ @dataclass
9
+ class GrpcTLSConfig:
10
+ """Configuration for gRPC server TLS.
11
+
12
+ Args:
13
+ cert_chain: The PEM-encoded server certificate chain.
14
+ private_key: The PEM-encoded private key for the server certificate.
15
+ root_certs: Optional PEM-encoded root certificates for verifying client certificates.
16
+ If provided with require_client_cert=True, enables mTLS.
17
+ require_client_cert: If True, requires and verifies client certificates (mTLS).
18
+ """
19
+
20
+ cert_chain: bytes
21
+ private_key: bytes
22
+ root_certs: Optional[bytes] = None
23
+ require_client_cert: bool = False
24
+
25
+ def to_server_credentials(self) -> grpc.ServerCredentials:
26
+ """Convert to gRPC ServerCredentials."""
27
+ private_key_certificate_chain_pairs = [(self.private_key, self.cert_chain)]
28
+
29
+ if self.require_client_cert and self.root_certs:
30
+ # mTLS: require and verify client certificates
31
+ return grpc.ssl_server_credentials(
32
+ private_key_certificate_chain_pairs,
33
+ root_certificates=self.root_certs,
34
+ require_client_auth=True,
35
+ )
36
+ elif self.root_certs:
37
+ # Optional client certificates
38
+ return grpc.ssl_server_credentials(
39
+ private_key_certificate_chain_pairs,
40
+ root_certificates=self.root_certs,
41
+ require_client_auth=False,
42
+ )
43
+ else:
44
+ # Server TLS only, no client certificate validation
45
+ return grpc.ssl_server_credentials(private_key_certificate_chain_pairs)
46
+
47
+
48
+ def extract_peer_identity(context: grpc.ServicerContext) -> Optional[str]:
49
+ """Extract the peer identity from the ServicerContext.
50
+
51
+ For mTLS connections, this returns the client's certificate subject.
52
+
53
+ Args:
54
+ context: The gRPC ServicerContext from a request handler.
55
+
56
+ Returns:
57
+ The peer identity string if available, None otherwise.
58
+ """
59
+ auth_context: Any = context.auth_context()
60
+ if auth_context:
61
+ # The peer identity is typically stored under the 'x509_common_name' key
62
+ # or 'x509_subject_alternative_name' for SANs
63
+ identities = auth_context.get("x509_common_name")
64
+ if identities and len(identities) > 0:
65
+ # Return the first identity (there's usually only one CN)
66
+ # gRPC returns bytes, so we decode to string
67
+ identity_bytes: bytes = identities[0]
68
+ return identity_bytes.decode("utf-8")
69
+
70
+ # Fallback to SAN if CN is not available
71
+ san_identities = auth_context.get("x509_subject_alternative_name")
72
+ if san_identities and len(san_identities) > 0:
73
+ # gRPC returns bytes, so we decode to string
74
+ san_bytes: bytes = san_identities[0]
75
+ return san_bytes.decode("utf-8")
76
+
77
+ return None
78
+
79
+
80
+ def extract_peer_certificate_chain(context: grpc.ServicerContext) -> Optional[bytes]:
81
+ """Extract the peer's certificate chain from the ServicerContext.
82
+
83
+ Args:
84
+ context: The gRPC ServicerContext from a request handler.
85
+
86
+ Returns:
87
+ The peer's certificate chain in PEM format if available, None otherwise.
88
+ """
89
+ auth_context: Any = context.auth_context()
90
+ if auth_context:
91
+ cert_chain = auth_context.get("x509_peer_certificate")
92
+ if cert_chain and len(cert_chain) > 0:
93
+ cert_bytes: bytes = cert_chain[0]
94
+ return cert_bytes
95
+
96
+ return None
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
5
  Author: Yasushi Itoh
6
- Requires-Dist: annotated-types>=0.5.0
6
+ Requires-Dist: annotated-types==0.7.0
7
7
  Requires-Dist: pydantic>=2.1.1
8
8
  Requires-Dist: grpcio>=1.56.2
9
9
  Requires-Dist: grpcio-tools>=1.56.2
10
10
  Requires-Dist: grpcio-reflection>=1.56.2
11
11
  Requires-Dist: grpcio-health-checking>=1.56.2
12
- Requires-Dist: sonora>=0.2.3
13
- Requires-Dist: connecpy==2.0.0
12
+ Requires-Dist: connecpy>=2.2.0
14
13
  Requires-Dist: mcp>=1.9.4
15
14
  Requires-Dist: starlette>=0.27.0
16
15
  Requires-Python: >=3.11
@@ -76,7 +75,7 @@ import asyncio
76
75
  from openai import AsyncOpenAI
77
76
  from pydantic_ai import Agent
78
77
  from pydantic_ai.models.openai import OpenAIModel
79
- from pydantic_rpc import ConnecpyASGIApp, Message
78
+ from pydantic_rpc import ASGIApp, Message
80
79
 
81
80
 
82
81
  class CityLocation(Message):
@@ -107,7 +106,7 @@ class OlympicsLocationAgent:
107
106
  result = await self._agent.run(req.prompt())
108
107
  return result.data
109
108
 
110
- app = ConnecpyASGIApp()
109
+ app = ASGIApp()
111
110
  app.mount(OlympicsLocationAgent())
112
111
 
113
112
  ```
@@ -123,10 +122,10 @@ app.mount(OlympicsLocationAgent())
123
122
  - 💚 **Health Checking:** Built-in support for gRPC health checks using `grpc_health.v1`.
124
123
  - 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
125
124
  - ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
126
- - **For gRPC-Web:**
127
- - 🌐 **WSGI/ASGI Support:** Create gRPC-Web services that can run as WSGI or ASGI applications powered by `Sonora`.
128
125
  - **For Connect-RPC:**
129
- - 🌐 **Connecpy Support:** Partially supports Connect-RPC via `Connecpy`.
126
+ - 🌐 **Full Protocol Support:** Native Connect-RPC support via `Connecpy` v2.2.0+
127
+ - 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
128
+ - 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
130
129
  - 🛠️ **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
130
  - 🤖 **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.
132
131
 
@@ -140,7 +139,11 @@ pip install pydantic-rpc
140
139
 
141
140
  ## 🚀 Getting Started
142
141
 
143
- ### 🔧 Synchronous Service Example
142
+ PydanticRPC supports two main protocols:
143
+ - **gRPC**: Traditional gRPC services with `Server` and `AsyncIOServer`
144
+ - **Connect-RPC**: Modern HTTP-based RPC with `ASGIApp` and `WSGIApp`
145
+
146
+ ### 🔧 Synchronous gRPC Service Example
144
147
 
145
148
  ```python
146
149
  from pydantic_rpc import Server, Message
@@ -161,7 +164,7 @@ if __name__ == "__main__":
161
164
  server.run(Greeter())
162
165
  ```
163
166
 
164
- ### ⚙️ Asynchronous Service Example
167
+ ### ⚙️ Asynchronous gRPC Service Example
165
168
 
166
169
  ```python
167
170
  import asyncio
@@ -194,7 +197,7 @@ if __name__ == "__main__":
194
197
 
195
198
  The AsyncIOServer automatically handles graceful shutdown on SIGTERM and SIGINT signals.
196
199
 
197
- ### 🌐 ASGI Application Example
200
+ ### 🌐 Connect-RPC ASGI Application Example
198
201
 
199
202
  ```python
200
203
  from pydantic_rpc import ASGIApp, Message
@@ -206,27 +209,17 @@ class HelloReply(Message):
206
209
  message: str
207
210
 
208
211
  class Greeter:
209
- def say_hello(self, request: HelloRequest) -> HelloReply:
212
+ async def say_hello(self, request: HelloRequest) -> HelloReply:
210
213
  return HelloReply(message=f"Hello, {request.name}!")
211
214
 
212
-
213
- async def app(scope, receive, send):
214
- """ASGI application.
215
-
216
- Args:
217
- scope (dict): The ASGI scope.
218
- receive (callable): The receive function.
219
- send (callable): The send function.
220
- """
221
- pass
222
-
223
- # Please note that `app` is any ASGI application, such as FastAPI or Starlette.
224
-
225
- app = ASGIApp(app)
215
+ app = ASGIApp()
226
216
  app.mount(Greeter())
217
+
218
+ # Run with uvicorn:
219
+ # uvicorn script:app --host 0.0.0.0 --port 8000
227
220
  ```
228
221
 
229
- ### 🌐 WSGI Application Example
222
+ ### 🌐 Connect-RPC WSGI Application Example
230
223
 
231
224
  ```python
232
225
  from pydantic_rpc import WSGIApp, Message
@@ -241,31 +234,69 @@ class Greeter:
241
234
  def say_hello(self, request: HelloRequest) -> HelloReply:
242
235
  return HelloReply(message=f"Hello, {request.name}!")
243
236
 
244
- def app(environ, start_response):
245
- """WSGI application.
246
-
247
- Args:
248
- environ (dict): The WSGI environment.
249
- start_response (callable): The start_response function.
250
- """
251
- pass
252
-
253
- # Please note that `app` is any WSGI application, such as Flask or Django.
254
-
255
- app = WSGIApp(app)
237
+ app = WSGIApp()
256
238
  app.mount(Greeter())
239
+
240
+ # Run with gunicorn:
241
+ # gunicorn script:app
257
242
  ```
258
243
 
259
- ### 🏆 Connecpy (Connect-RPC) Example
244
+ ### 🏆 Connect-RPC with Streaming Example
260
245
 
261
- PydanticRPC also partially supports Connect-RPC via connecpy. Check out “greeting_connecpy.py” for an example:
246
+ PydanticRPC provides native Connect-RPC support via Connecpy v2.2.0+, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
262
247
 
263
248
  ```bash
264
- uv run greeting_connecpy.py
249
+ # Run with uvicorn
250
+ uv run uvicorn greeting_asgi:app --port 3000
251
+
252
+ # Or run streaming example
253
+ uv run python examples/streaming_connecpy.py
265
254
  ```
266
255
 
267
256
  This will launch a Connecpy-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
268
257
 
258
+ #### Streaming Support with Connecpy
259
+
260
+ Connecpy v2.2.0 provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
261
+
262
+ ```python
263
+ from typing import AsyncIterator
264
+ from pydantic_rpc import ASGIApp, Message
265
+
266
+ class StreamRequest(Message):
267
+ text: str
268
+ count: int
269
+
270
+ class StreamResponse(Message):
271
+ text: str
272
+ index: int
273
+
274
+ class StreamingService:
275
+ # Server streaming
276
+ async def server_stream(self, request: StreamRequest) -> AsyncIterator[StreamResponse]:
277
+ for i in range(request.count):
278
+ yield StreamResponse(text=f"{request.text}_{i}", index=i)
279
+
280
+ # Client streaming
281
+ async def client_stream(self, requests: AsyncIterator[StreamRequest]) -> StreamResponse:
282
+ texts = []
283
+ async for req in requests:
284
+ texts.append(req.text)
285
+ return StreamResponse(text=" ".join(texts), index=len(texts))
286
+
287
+ # Bidirectional streaming
288
+ async def bidi_stream(
289
+ self, requests: AsyncIterator[StreamRequest]
290
+ ) -> AsyncIterator[StreamResponse]:
291
+ idx = 0
292
+ async for req in requests:
293
+ yield StreamResponse(text=f"Echo: {req.text}", index=idx)
294
+ idx += 1
295
+
296
+ app = ASGIApp()
297
+ app.mount(StreamingService())
298
+ ```
299
+
269
300
  > [!NOTE]
270
301
  > Please install `protoc-gen-connecpy` to run the Connecpy example.
271
302
  >
@@ -302,9 +333,9 @@ export PYDANTIC_RPC_RESERVED_FIELDS=1
302
333
 
303
334
  ## 💎 Advanced Features
304
335
 
305
- ### 🌊 Response Streaming
306
- PydanticRPC supports streaming responses only for asynchronous gRPC and gRPC-Web services.
307
- If a service class methods return type is `typing.AsyncIterator[T]`, the method is considered a streaming method.
336
+ ### 🌊 Response Streaming (gRPC)
337
+ PydanticRPC supports streaming responses for both gRPC and Connect-RPC services.
338
+ If a service class method's return type is `typing.AsyncIterator[T]`, the method is considered a streaming method.
308
339
 
309
340
 
310
341
  Please see the sample code below:
@@ -870,9 +901,45 @@ class GoodMessage(Message):
870
901
  - Test error cases thoroughly
871
902
  - Be aware that errors fail silently
872
903
 
904
+ ### 🔒 TLS/mTLS Support
905
+
906
+ PydanticRPC provides built-in support for TLS (Transport Layer Security) and mTLS (mutual TLS) for secure gRPC communication.
907
+
908
+ ```python
909
+ from pydantic_rpc import AsyncIOServer, GrpcTLSConfig, extract_peer_identity
910
+ import grpc
911
+
912
+ # Basic TLS (server authentication only)
913
+ tls_config = GrpcTLSConfig(
914
+ cert_chain=server_cert_bytes,
915
+ private_key=server_key_bytes,
916
+ require_client_cert=False
917
+ )
918
+
919
+ # mTLS (mutual authentication)
920
+ tls_config = GrpcTLSConfig(
921
+ cert_chain=server_cert_bytes,
922
+ private_key=server_key_bytes,
923
+ root_certs=ca_cert_bytes, # CA to verify client certificates
924
+ require_client_cert=True
925
+ )
926
+
927
+ # Create server with TLS
928
+ server = AsyncIOServer(tls=tls_config)
929
+
930
+ # Extract client identity in service methods
931
+ class SecureService:
932
+ async def secure_method(self, request, context: grpc.ServicerContext):
933
+ client_identity = extract_peer_identity(context)
934
+ if client_identity:
935
+ print(f"Authenticated client: {client_identity}")
936
+ ```
937
+
938
+ For a complete example, see [examples/tls_server.py](examples/tls_server.py) and [examples/tls_client.py](examples/tls_client.py).
939
+
873
940
  ### 🔗 Multiple Services with Custom Interceptors
874
941
 
875
- PydanticRPC supports defining and running multiple services in a single server:
942
+ PydanticRPC supports defining and running multiple gRPC services in a single server:
876
943
 
877
944
  ```python
878
945
  from datetime import datetime
@@ -1003,11 +1070,11 @@ Any MCP-compatible client can connect to your service. For example, to configure
1003
1070
  MCP can also be mounted to existing ASGI applications:
1004
1071
 
1005
1072
  ```python
1006
- from pydantic_rpc import ConnecpyASGIApp
1073
+ from pydantic_rpc import ASGIApp
1007
1074
  from pydantic_rpc.mcp import MCPExporter
1008
1075
 
1009
1076
  # Create Connect-RPC ASGI app
1010
- app = ConnecpyASGIApp()
1077
+ app = ASGIApp()
1011
1078
  app.mount(MathService())
1012
1079
 
1013
1080
  # Add MCP support via HTTP/SSE
@@ -0,0 +1,13 @@
1
+ pydantic_rpc/__init__.py,sha256=a8c4ed1a731b7f601006cbbd4aff16d43bbca0b7ffb02c698f1c79dccefe2f8e,781
2
+ pydantic_rpc/core.py,sha256=b2867838460a23b1ce302c9d7e6ef2a2e711c6c84887a50e3ad40b94ff5dc53c,112505
3
+ pydantic_rpc/decorators.py,sha256=2f2dc3f642a6e40d05187901e75269415a70b8f18d555e6c7ac923dee149d34c,3852
4
+ pydantic_rpc/mcp/__init__.py,sha256=f05ad62cc38db5c5972e16870c484523c58c5ac650d8454706f1ce4539cc7a52,123
5
+ pydantic_rpc/mcp/converter.py,sha256=b60dcaf82e6bff6be4b2ab8b1e9f2d16e09cb98d8f00182a4f6f73d1a78848a4,4158
6
+ pydantic_rpc/mcp/exporter.py,sha256=20833662f9a973ed9e1a705b9b59aaa2533de6c514ebd87ef66664a430a0d04f,10239
7
+ pydantic_rpc/options.py,sha256=6094036184a500b92715fd91d31ecc501460a56de47dcf6024c6335ae8e5d6e3,4561
8
+ pydantic_rpc/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
9
+ pydantic_rpc/tls.py,sha256=0eb290cc09c8ebedc71e7a97c01736d9675a314f70ebd80a2ce73e3f1689d359,3567
10
+ pydantic_rpc-0.10.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
11
+ pydantic_rpc-0.10.0.dist-info/entry_points.txt,sha256=72a47b1d2cae3abc045710fe0c2c2e6dfbb051fbf6960c22b62488004e9188ba,57
12
+ pydantic_rpc-0.10.0.dist-info/METADATA,sha256=8819d19940dcb5a2718e9455725e976babebeeee32ba2b016135f29ba3dda67c,32096
13
+ pydantic_rpc-0.10.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
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,,