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 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__(self, max_workers: int = 8, *interceptors: Any) -> None:
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._server.add_insecure_port(f"[::]:{self._port}")
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__(self, *interceptors: grpc.ServerInterceptor) -> None:
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
- _ = self._server.add_insecure_port(f"[::]:{self._port}")
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.9.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,,