pydantic-rpc 0.9.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.
- pydantic_rpc/__init__.py +8 -0
- pydantic_rpc/core.py +35 -4
- pydantic_rpc/tls.py +96 -0
- {pydantic_rpc-0.9.0.dist-info → pydantic_rpc-0.10.0.dist-info}/METADATA +37 -2
- pydantic_rpc-0.10.0.dist-info/RECORD +13 -0
- pydantic_rpc-0.9.0.dist-info/RECORD +0 -12
- {pydantic_rpc-0.9.0.dist-info → pydantic_rpc-0.10.0.dist-info}/WHEEL +0 -0
- {pydantic_rpc-0.9.0.dist-info → pydantic_rpc-0.10.0.dist-info}/entry_points.txt +0 -0
pydantic_rpc/__init__.py
CHANGED
|
@@ -14,6 +14,11 @@ from .decorators import (
|
|
|
14
14
|
get_method_options,
|
|
15
15
|
has_http_option,
|
|
16
16
|
)
|
|
17
|
+
from .tls import (
|
|
18
|
+
GrpcTLSConfig,
|
|
19
|
+
extract_peer_identity,
|
|
20
|
+
extract_peer_certificate_chain,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
__all__ = [
|
|
19
24
|
"Server",
|
|
@@ -26,6 +31,9 @@ __all__ = [
|
|
|
26
31
|
"proto_option",
|
|
27
32
|
"get_method_options",
|
|
28
33
|
"has_http_option",
|
|
34
|
+
"GrpcTLSConfig",
|
|
35
|
+
"extract_peer_identity",
|
|
36
|
+
"extract_peer_certificate_chain",
|
|
29
37
|
]
|
|
30
38
|
|
|
31
39
|
# Optional MCP support
|
pydantic_rpc/core.py
CHANGED
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from posixpath import basename
|
|
16
16
|
from typing import (
|
|
17
17
|
Any,
|
|
18
|
+
Optional,
|
|
18
19
|
TypeAlias,
|
|
19
20
|
get_args,
|
|
20
21
|
get_origin,
|
|
@@ -36,6 +37,7 @@ from grpc_reflection.v1alpha import reflection
|
|
|
36
37
|
from grpc_tools import protoc
|
|
37
38
|
from pydantic import BaseModel, ValidationError
|
|
38
39
|
from .decorators import get_method_options, has_http_option
|
|
40
|
+
from .tls import GrpcTLSConfig
|
|
39
41
|
|
|
40
42
|
###############################################################################
|
|
41
43
|
# 1. Message definitions & converter extensions
|
|
@@ -2602,13 +2604,19 @@ def generate_combined_descriptor_set(
|
|
|
2602
2604
|
class Server:
|
|
2603
2605
|
"""A simple gRPC server that uses ThreadPoolExecutor for concurrency."""
|
|
2604
2606
|
|
|
2605
|
-
def __init__(
|
|
2607
|
+
def __init__(
|
|
2608
|
+
self,
|
|
2609
|
+
max_workers: int = 8,
|
|
2610
|
+
*interceptors: Any,
|
|
2611
|
+
tls: Optional["GrpcTLSConfig"] = None,
|
|
2612
|
+
) -> None:
|
|
2606
2613
|
self._server: grpc.Server = grpc.server(
|
|
2607
2614
|
futures.ThreadPoolExecutor(max_workers), interceptors=interceptors
|
|
2608
2615
|
)
|
|
2609
2616
|
self._service_names: list[str] = []
|
|
2610
2617
|
self._package_name: str = ""
|
|
2611
2618
|
self._port: int = 50051
|
|
2619
|
+
self._tls_config = tls
|
|
2612
2620
|
|
|
2613
2621
|
def set_package_name(self, package_name: str):
|
|
2614
2622
|
"""Set the package name for .proto generation."""
|
|
@@ -2658,7 +2666,16 @@ class Server:
|
|
|
2658
2666
|
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._server)
|
|
2659
2667
|
reflection.enable_server_reflection(SERVICE_NAMES, self._server)
|
|
2660
2668
|
|
|
2661
|
-
self.
|
|
2669
|
+
if self._tls_config:
|
|
2670
|
+
# Use secure port with TLS
|
|
2671
|
+
credentials = self._tls_config.to_server_credentials()
|
|
2672
|
+
self._server.add_secure_port(f"[::]:{self._port}", credentials)
|
|
2673
|
+
print(f"gRPC server starting with TLS on port {self._port}...")
|
|
2674
|
+
else:
|
|
2675
|
+
# Use insecure port
|
|
2676
|
+
self._server.add_insecure_port(f"[::]:{self._port}")
|
|
2677
|
+
print(f"gRPC server starting on port {self._port}...")
|
|
2678
|
+
|
|
2662
2679
|
self._server.start()
|
|
2663
2680
|
|
|
2664
2681
|
def handle_signal(signum: signal.Signals, frame: Any):
|
|
@@ -2680,11 +2697,16 @@ class Server:
|
|
|
2680
2697
|
class AsyncIOServer:
|
|
2681
2698
|
"""An async gRPC server using asyncio."""
|
|
2682
2699
|
|
|
2683
|
-
def __init__(
|
|
2700
|
+
def __init__(
|
|
2701
|
+
self,
|
|
2702
|
+
*interceptors: grpc.ServerInterceptor,
|
|
2703
|
+
tls: Optional["GrpcTLSConfig"] = None,
|
|
2704
|
+
) -> None:
|
|
2684
2705
|
self._server: grpc.aio.Server = grpc.aio.server(interceptors=interceptors)
|
|
2685
2706
|
self._service_names: list[str] = []
|
|
2686
2707
|
self._package_name: str = ""
|
|
2687
2708
|
self._port: int = 50051
|
|
2709
|
+
self._tls_config = tls
|
|
2688
2710
|
|
|
2689
2711
|
def set_package_name(self, package_name: str):
|
|
2690
2712
|
"""Set the package name for .proto generation."""
|
|
@@ -2736,7 +2758,16 @@ class AsyncIOServer:
|
|
|
2736
2758
|
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._server)
|
|
2737
2759
|
reflection.enable_server_reflection(SERVICE_NAMES, self._server)
|
|
2738
2760
|
|
|
2739
|
-
|
|
2761
|
+
if self._tls_config:
|
|
2762
|
+
# Use secure port with TLS
|
|
2763
|
+
credentials = self._tls_config.to_server_credentials()
|
|
2764
|
+
_ = self._server.add_secure_port(f"[::]:{self._port}", credentials)
|
|
2765
|
+
print(f"gRPC server starting with TLS on port {self._port}...")
|
|
2766
|
+
else:
|
|
2767
|
+
# Use insecure port
|
|
2768
|
+
_ = self._server.add_insecure_port(f"[::]:{self._port}")
|
|
2769
|
+
print(f"gRPC server starting on port {self._port}...")
|
|
2770
|
+
|
|
2740
2771
|
await self._server.start()
|
|
2741
2772
|
|
|
2742
2773
|
shutdown_event = asyncio.Event()
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pydantic-rpc
|
|
3
|
-
Version: 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
6
|
Requires-Dist: annotated-types==0.7.0
|
|
@@ -901,8 +901,43 @@ class GoodMessage(Message):
|
|
|
901
901
|
- Test error cases thoroughly
|
|
902
902
|
- Be aware that errors fail silently
|
|
903
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
|
+
|
|
904
940
|
### 🔗 Multiple Services with Custom Interceptors
|
|
905
|
-
>>>>>>> origin/main
|
|
906
941
|
|
|
907
942
|
PydanticRPC supports defining and running multiple gRPC services in a single server:
|
|
908
943
|
|
|
@@ -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,12 +0,0 @@
|
|
|
1
|
-
pydantic_rpc/__init__.py,sha256=cd21549eab234e0bc3b3744713c65486b00a63e9ca8d4696506b819099fc1f79,590
|
|
2
|
-
pydantic_rpc/core.py,sha256=ccc7a5f91dc471a0ecc253a5fd766a24878308be7905532ee7c6b9b51b9a1b8e,111436
|
|
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-0.9.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
|
|
10
|
-
pydantic_rpc-0.9.0.dist-info/entry_points.txt,sha256=72a47b1d2cae3abc045710fe0c2c2e6dfbb051fbf6960c22b62488004e9188ba,57
|
|
11
|
-
pydantic_rpc-0.9.0.dist-info/METADATA,sha256=7a022eb4a99c02b665ceb0dca3d48ad587ade87f9be0f08f7b6c378738bd043e,30976
|
|
12
|
-
pydantic_rpc-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|