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.
- pydantic_rpc/__init__.py +20 -4
- pydantic_rpc/core.py +326 -182
- pydantic_rpc/decorators.py +138 -0
- pydantic_rpc/options.py +134 -0
- pydantic_rpc/tls.py +96 -0
- {pydantic_rpc-0.8.0.dist-info → pydantic_rpc-0.10.0.dist-info}/METADATA +116 -49
- pydantic_rpc-0.10.0.dist-info/RECORD +13 -0
- pydantic_rpc-0.8.0.dist-info/RECORD +0 -10
- {pydantic_rpc-0.8.0.dist-info → pydantic_rpc-0.10.0.dist-info}/WHEEL +0 -0
- {pydantic_rpc-0.8.0.dist-info → pydantic_rpc-0.10.0.dist-info}/entry_points.txt +0 -0
pydantic_rpc/core.py
CHANGED
|
@@ -9,12 +9,13 @@ import sys
|
|
|
9
9
|
import time
|
|
10
10
|
import types
|
|
11
11
|
from typing import Union
|
|
12
|
-
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
12
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
13
13
|
from concurrent import futures
|
|
14
14
|
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,
|
|
@@ -35,8 +36,8 @@ from grpc_health.v1.health import HealthServicer
|
|
|
35
36
|
from grpc_reflection.v1alpha import reflection
|
|
36
37
|
from grpc_tools import protoc
|
|
37
38
|
from pydantic import BaseModel, ValidationError
|
|
38
|
-
from
|
|
39
|
-
from
|
|
39
|
+
from .decorators import get_method_options, has_http_option
|
|
40
|
+
from .tls import GrpcTLSConfig
|
|
40
41
|
|
|
41
42
|
###############################################################################
|
|
42
43
|
# 1. Message definitions & converter extensions
|
|
@@ -838,7 +839,8 @@ def connect_obj_with_stub_connecpy(
|
|
|
838
839
|
if method.__name__.startswith("_"):
|
|
839
840
|
continue
|
|
840
841
|
a_method = implement_stub_method(method)
|
|
841
|
-
|
|
842
|
+
# Use the original snake_case method name for Connecpy v2.2.0 compatibility
|
|
843
|
+
setattr(ConcreteServiceClass, method.__name__, a_method)
|
|
842
844
|
|
|
843
845
|
return ConcreteServiceClass
|
|
844
846
|
|
|
@@ -847,7 +849,7 @@ def connect_obj_with_stub_async_connecpy(
|
|
|
847
849
|
connecpy_module: Any, pb2_module: Any, obj: object
|
|
848
850
|
) -> type:
|
|
849
851
|
"""
|
|
850
|
-
Connect a Python service object to a Connecpy stub for async methods.
|
|
852
|
+
Connect a Python service object to a Connecpy stub for async methods with streaming support.
|
|
851
853
|
"""
|
|
852
854
|
service_class = obj.__class__
|
|
853
855
|
stub_class_name = service_class.__name__
|
|
@@ -857,12 +859,13 @@ def connect_obj_with_stub_async_connecpy(
|
|
|
857
859
|
pass
|
|
858
860
|
|
|
859
861
|
def implement_stub_method(
|
|
860
|
-
method: Callable[...,
|
|
862
|
+
method: Callable[..., Any],
|
|
861
863
|
) -> Callable[[object, Any, Any], Any]:
|
|
862
864
|
sig = inspect.signature(method)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
+
input_type = get_request_arg_type(sig)
|
|
866
|
+
is_input_stream = is_stream_type(input_type)
|
|
865
867
|
response_type = sig.return_annotation
|
|
868
|
+
is_output_stream = is_stream_type(response_type)
|
|
866
869
|
size_of_parameters = len(sig.parameters)
|
|
867
870
|
|
|
868
871
|
match size_of_parameters:
|
|
@@ -891,63 +894,215 @@ def connect_obj_with_stub_async_connecpy(
|
|
|
891
894
|
|
|
892
895
|
return stub_method0
|
|
893
896
|
|
|
894
|
-
case 1:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
897
|
+
case 1 | 2:
|
|
898
|
+
if is_input_stream:
|
|
899
|
+
# Client streaming or bidirectional streaming
|
|
900
|
+
input_item_type = get_args(input_type)[0]
|
|
901
|
+
item_converter = generate_message_converter(input_item_type)
|
|
902
|
+
|
|
903
|
+
async def convert_iterator(
|
|
904
|
+
proto_iter: AsyncIterator[Any],
|
|
905
|
+
) -> AsyncIterator[Message]:
|
|
906
|
+
async for proto in proto_iter:
|
|
907
|
+
result = item_converter(proto)
|
|
908
|
+
if result is None:
|
|
909
|
+
raise TypeError(
|
|
910
|
+
f"Unexpected None result from converter for type {input_item_type}"
|
|
911
|
+
)
|
|
912
|
+
yield result
|
|
913
|
+
|
|
914
|
+
if is_output_stream:
|
|
915
|
+
# Bidirectional streaming
|
|
916
|
+
output_item_type = get_args(response_type)[0]
|
|
917
|
+
|
|
918
|
+
if size_of_parameters == 1:
|
|
919
|
+
|
|
920
|
+
async def stub_method(
|
|
921
|
+
self: object,
|
|
922
|
+
request_iterator: AsyncIterator[Any],
|
|
923
|
+
context: Any,
|
|
924
|
+
) -> AsyncIterator[Any]:
|
|
925
|
+
_ = self
|
|
926
|
+
try:
|
|
927
|
+
arg_iter = convert_iterator(request_iterator)
|
|
928
|
+
async for resp_obj in method(arg_iter):
|
|
929
|
+
yield convert_python_message_to_proto(
|
|
930
|
+
resp_obj, output_item_type, pb2_module
|
|
931
|
+
)
|
|
932
|
+
except ValidationError as e:
|
|
933
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
934
|
+
except Exception as e:
|
|
935
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
936
|
+
else: # size_of_parameters == 2
|
|
937
|
+
|
|
938
|
+
async def stub_method(
|
|
939
|
+
self: object,
|
|
940
|
+
request_iterator: AsyncIterator[Any],
|
|
941
|
+
context: Any,
|
|
942
|
+
) -> AsyncIterator[Any]:
|
|
943
|
+
_ = self
|
|
944
|
+
try:
|
|
945
|
+
arg_iter = convert_iterator(request_iterator)
|
|
946
|
+
async for resp_obj in method(arg_iter, context):
|
|
947
|
+
yield convert_python_message_to_proto(
|
|
948
|
+
resp_obj, output_item_type, pb2_module
|
|
949
|
+
)
|
|
950
|
+
except ValidationError as e:
|
|
951
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
952
|
+
except Exception as e:
|
|
953
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
954
|
+
|
|
955
|
+
return stub_method
|
|
956
|
+
else:
|
|
957
|
+
# Client streaming
|
|
958
|
+
if size_of_parameters == 1:
|
|
959
|
+
|
|
960
|
+
async def stub_method(
|
|
961
|
+
self: object,
|
|
962
|
+
request_iterator: AsyncIterator[Any],
|
|
963
|
+
context: Any,
|
|
964
|
+
) -> Any:
|
|
965
|
+
_ = self
|
|
966
|
+
try:
|
|
967
|
+
arg_iter = convert_iterator(request_iterator)
|
|
968
|
+
resp_obj = await method(arg_iter)
|
|
969
|
+
if is_none_type(response_type):
|
|
970
|
+
return empty_pb2.Empty() # type: ignore
|
|
971
|
+
return convert_python_message_to_proto(
|
|
972
|
+
resp_obj, response_type, pb2_module
|
|
973
|
+
)
|
|
974
|
+
except ValidationError as e:
|
|
975
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
976
|
+
except Exception as e:
|
|
977
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
978
|
+
else: # size_of_parameters == 2
|
|
979
|
+
|
|
980
|
+
async def stub_method(
|
|
981
|
+
self: object,
|
|
982
|
+
request_iterator: AsyncIterator[Any],
|
|
983
|
+
context: Any,
|
|
984
|
+
) -> Any:
|
|
985
|
+
_ = self
|
|
986
|
+
try:
|
|
987
|
+
arg_iter = convert_iterator(request_iterator)
|
|
988
|
+
resp_obj = await method(arg_iter, context)
|
|
989
|
+
if is_none_type(response_type):
|
|
990
|
+
return empty_pb2.Empty() # type: ignore
|
|
991
|
+
return convert_python_message_to_proto(
|
|
992
|
+
resp_obj, response_type, pb2_module
|
|
993
|
+
)
|
|
994
|
+
except ValidationError as e:
|
|
995
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
996
|
+
except Exception as e:
|
|
997
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
998
|
+
|
|
999
|
+
return stub_method
|
|
1000
|
+
else:
|
|
1001
|
+
# Unary request
|
|
1002
|
+
converter = generate_message_converter(input_type)
|
|
1003
|
+
|
|
1004
|
+
if is_output_stream:
|
|
1005
|
+
# Server streaming
|
|
1006
|
+
output_item_type = get_args(response_type)[0]
|
|
1007
|
+
|
|
1008
|
+
if size_of_parameters == 1:
|
|
1009
|
+
|
|
1010
|
+
async def stub_method(
|
|
1011
|
+
self: object,
|
|
1012
|
+
request: Any,
|
|
1013
|
+
context: Any,
|
|
1014
|
+
) -> AsyncIterator[Any]:
|
|
1015
|
+
_ = self
|
|
1016
|
+
try:
|
|
1017
|
+
if is_none_type(input_type):
|
|
1018
|
+
arg = None
|
|
1019
|
+
else:
|
|
1020
|
+
arg = converter(request)
|
|
1021
|
+
async for resp_obj in method(arg):
|
|
1022
|
+
yield convert_python_message_to_proto(
|
|
1023
|
+
resp_obj, output_item_type, pb2_module
|
|
1024
|
+
)
|
|
1025
|
+
except ValidationError as e:
|
|
1026
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
1027
|
+
except Exception as e:
|
|
1028
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
1029
|
+
else: # size_of_parameters == 2
|
|
1030
|
+
|
|
1031
|
+
async def stub_method(
|
|
1032
|
+
self: object,
|
|
1033
|
+
request: Any,
|
|
1034
|
+
context: Any,
|
|
1035
|
+
) -> AsyncIterator[Any]:
|
|
1036
|
+
_ = self
|
|
1037
|
+
try:
|
|
1038
|
+
if is_none_type(input_type):
|
|
1039
|
+
arg = None
|
|
1040
|
+
else:
|
|
1041
|
+
arg = converter(request)
|
|
1042
|
+
async for resp_obj in method(arg, context):
|
|
1043
|
+
yield convert_python_message_to_proto(
|
|
1044
|
+
resp_obj, output_item_type, pb2_module
|
|
1045
|
+
)
|
|
1046
|
+
except ValidationError as e:
|
|
1047
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
1048
|
+
except Exception as e:
|
|
1049
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
1050
|
+
|
|
1051
|
+
return stub_method
|
|
1052
|
+
else:
|
|
1053
|
+
# Unary RPC
|
|
1054
|
+
if size_of_parameters == 1:
|
|
1055
|
+
|
|
1056
|
+
async def stub_method(
|
|
1057
|
+
self: object,
|
|
1058
|
+
request: Any,
|
|
1059
|
+
context: Any,
|
|
1060
|
+
) -> Any:
|
|
1061
|
+
_ = self
|
|
1062
|
+
try:
|
|
1063
|
+
if is_none_type(input_type):
|
|
1064
|
+
resp_obj = await method(None)
|
|
1065
|
+
else:
|
|
1066
|
+
arg = converter(request)
|
|
1067
|
+
resp_obj = await method(arg)
|
|
1068
|
+
|
|
1069
|
+
if is_none_type(response_type):
|
|
1070
|
+
return empty_pb2.Empty() # type: ignore
|
|
1071
|
+
else:
|
|
1072
|
+
return convert_python_message_to_proto(
|
|
1073
|
+
resp_obj, response_type, pb2_module
|
|
1074
|
+
)
|
|
1075
|
+
except ValidationError as e:
|
|
1076
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
1077
|
+
except Exception as e:
|
|
1078
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
1079
|
+
else: # size_of_parameters == 2
|
|
1080
|
+
|
|
1081
|
+
async def stub_method(
|
|
1082
|
+
self: object,
|
|
1083
|
+
request: Any,
|
|
1084
|
+
context: Any,
|
|
1085
|
+
) -> Any:
|
|
1086
|
+
_ = self
|
|
1087
|
+
try:
|
|
1088
|
+
if is_none_type(input_type):
|
|
1089
|
+
resp_obj = await method(None, context)
|
|
1090
|
+
else:
|
|
1091
|
+
arg = converter(request)
|
|
1092
|
+
resp_obj = await method(arg, context)
|
|
1093
|
+
|
|
1094
|
+
if is_none_type(response_type):
|
|
1095
|
+
return empty_pb2.Empty() # type: ignore
|
|
1096
|
+
else:
|
|
1097
|
+
return convert_python_message_to_proto(
|
|
1098
|
+
resp_obj, response_type, pb2_module
|
|
1099
|
+
)
|
|
1100
|
+
except ValidationError as e:
|
|
1101
|
+
await context.abort(Errors.INVALID_ARGUMENT, str(e))
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
await context.abort(Errors.INTERNAL, str(e))
|
|
1104
|
+
|
|
1105
|
+
return stub_method
|
|
951
1106
|
|
|
952
1107
|
case _:
|
|
953
1108
|
raise Exception("Method must have 0, 1, or 2 parameters")
|
|
@@ -955,10 +1110,14 @@ def connect_obj_with_stub_async_connecpy(
|
|
|
955
1110
|
for method_name, method in get_rpc_methods(obj):
|
|
956
1111
|
if method.__name__.startswith("_"):
|
|
957
1112
|
continue
|
|
958
|
-
|
|
959
|
-
|
|
1113
|
+
# Check for async generator functions for streaming support
|
|
1114
|
+
if not (
|
|
1115
|
+
asyncio.iscoroutinefunction(method) or inspect.isasyncgenfunction(method)
|
|
1116
|
+
):
|
|
1117
|
+
raise Exception(f"Method {method_name} must be async or async generator")
|
|
960
1118
|
a_method = implement_stub_method(method)
|
|
961
|
-
|
|
1119
|
+
# Use the original snake_case method name for Connecpy v2.2.0 compatibility
|
|
1120
|
+
setattr(ConcreteServiceClass, method.__name__, a_method)
|
|
962
1121
|
|
|
963
1122
|
return ConcreteServiceClass
|
|
964
1123
|
|
|
@@ -1510,11 +1669,11 @@ def generate_message_definition(
|
|
|
1510
1669
|
fields.append("// length of " + str(metadata_item.len))
|
|
1511
1670
|
case annotated_types.MinLen:
|
|
1512
1671
|
fields.append(
|
|
1513
|
-
"// minimum length of " + str(metadata_item.
|
|
1672
|
+
"// minimum length of " + str(metadata_item.min_length)
|
|
1514
1673
|
)
|
|
1515
1674
|
case annotated_types.MaxLen:
|
|
1516
1675
|
fields.append(
|
|
1517
|
-
"// maximum length of " + str(metadata_item.
|
|
1676
|
+
"// maximum length of " + str(metadata_item.max_length)
|
|
1518
1677
|
)
|
|
1519
1678
|
case _:
|
|
1520
1679
|
fields.append("// " + str(metadata_item))
|
|
@@ -1550,6 +1709,39 @@ def is_generic_alias(annotation: Any) -> bool:
|
|
|
1550
1709
|
return get_origin(annotation) is not None
|
|
1551
1710
|
|
|
1552
1711
|
|
|
1712
|
+
def format_method_options(method: Any) -> list[str]:
|
|
1713
|
+
"""
|
|
1714
|
+
Format protobuf options for a method.
|
|
1715
|
+
|
|
1716
|
+
Args:
|
|
1717
|
+
method: The method to get options from
|
|
1718
|
+
|
|
1719
|
+
Returns:
|
|
1720
|
+
List of formatted option strings
|
|
1721
|
+
"""
|
|
1722
|
+
metadata = get_method_options(method)
|
|
1723
|
+
if metadata is None:
|
|
1724
|
+
return []
|
|
1725
|
+
|
|
1726
|
+
return metadata.to_proto_strings()
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def check_uses_http_options(obj: object) -> bool:
|
|
1730
|
+
"""
|
|
1731
|
+
Check if any method in the service uses HTTP options.
|
|
1732
|
+
|
|
1733
|
+
Args:
|
|
1734
|
+
obj: Service instance
|
|
1735
|
+
|
|
1736
|
+
Returns:
|
|
1737
|
+
True if any method has HTTP options
|
|
1738
|
+
"""
|
|
1739
|
+
for method_name, method in get_rpc_methods(obj):
|
|
1740
|
+
if has_http_option(method):
|
|
1741
|
+
return True
|
|
1742
|
+
return False
|
|
1743
|
+
|
|
1744
|
+
|
|
1553
1745
|
def generate_proto(obj: object, package_name: str = "") -> str:
|
|
1554
1746
|
"""
|
|
1555
1747
|
Generate a .proto definition from a service class.
|
|
@@ -1570,6 +1762,7 @@ def generate_proto(obj: object, package_name: str = "") -> str:
|
|
|
1570
1762
|
uses_timestamp = False
|
|
1571
1763
|
uses_duration = False
|
|
1572
1764
|
uses_empty = False
|
|
1765
|
+
uses_http_options = check_uses_http_options(obj)
|
|
1573
1766
|
|
|
1574
1767
|
def check_and_set_well_known_types_for_fields(py_type: Any):
|
|
1575
1768
|
"""Check well-known types for field annotations (excludes None/Empty)."""
|
|
@@ -1729,9 +1922,24 @@ def generate_proto(obj: object, package_name: str = "") -> str:
|
|
|
1729
1922
|
else output_msg_type.__name__
|
|
1730
1923
|
)
|
|
1731
1924
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1925
|
+
# Get method options
|
|
1926
|
+
method_options = format_method_options(method)
|
|
1927
|
+
|
|
1928
|
+
if method_options:
|
|
1929
|
+
# RPC with options - use block format
|
|
1930
|
+
rpc_definitions.append(
|
|
1931
|
+
f"rpc {method_name} ({input_str}) returns ({output_str}) {{"
|
|
1932
|
+
)
|
|
1933
|
+
for option_str in method_options:
|
|
1934
|
+
# Indent each option line
|
|
1935
|
+
for line in option_str.split("\n"):
|
|
1936
|
+
rpc_definitions.append(f" {line}")
|
|
1937
|
+
rpc_definitions.append("}")
|
|
1938
|
+
else:
|
|
1939
|
+
# RPC without options - use simple format
|
|
1940
|
+
rpc_definitions.append(
|
|
1941
|
+
f"rpc {method_name} ({input_str}) returns ({output_str});"
|
|
1942
|
+
)
|
|
1735
1943
|
|
|
1736
1944
|
if not package_name:
|
|
1737
1945
|
if service_name.endswith("Service"):
|
|
@@ -1741,6 +1949,8 @@ def generate_proto(obj: object, package_name: str = "") -> str:
|
|
|
1741
1949
|
package_name = package_name.lower() + ".v1"
|
|
1742
1950
|
|
|
1743
1951
|
imports: list[str] = []
|
|
1952
|
+
if uses_http_options:
|
|
1953
|
+
imports.append('import "google/api/annotations.proto";')
|
|
1744
1954
|
if uses_timestamp:
|
|
1745
1955
|
imports.append('import "google/protobuf/timestamp.proto";')
|
|
1746
1956
|
if uses_duration:
|
|
@@ -2394,13 +2604,19 @@ def generate_combined_descriptor_set(
|
|
|
2394
2604
|
class Server:
|
|
2395
2605
|
"""A simple gRPC server that uses ThreadPoolExecutor for concurrency."""
|
|
2396
2606
|
|
|
2397
|
-
def __init__(
|
|
2607
|
+
def __init__(
|
|
2608
|
+
self,
|
|
2609
|
+
max_workers: int = 8,
|
|
2610
|
+
*interceptors: Any,
|
|
2611
|
+
tls: Optional["GrpcTLSConfig"] = None,
|
|
2612
|
+
) -> None:
|
|
2398
2613
|
self._server: grpc.Server = grpc.server(
|
|
2399
2614
|
futures.ThreadPoolExecutor(max_workers), interceptors=interceptors
|
|
2400
2615
|
)
|
|
2401
2616
|
self._service_names: list[str] = []
|
|
2402
2617
|
self._package_name: str = ""
|
|
2403
2618
|
self._port: int = 50051
|
|
2619
|
+
self._tls_config = tls
|
|
2404
2620
|
|
|
2405
2621
|
def set_package_name(self, package_name: str):
|
|
2406
2622
|
"""Set the package name for .proto generation."""
|
|
@@ -2450,7 +2666,16 @@ class Server:
|
|
|
2450
2666
|
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._server)
|
|
2451
2667
|
reflection.enable_server_reflection(SERVICE_NAMES, self._server)
|
|
2452
2668
|
|
|
2453
|
-
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
|
+
|
|
2454
2679
|
self._server.start()
|
|
2455
2680
|
|
|
2456
2681
|
def handle_signal(signum: signal.Signals, frame: Any):
|
|
@@ -2472,11 +2697,16 @@ class Server:
|
|
|
2472
2697
|
class AsyncIOServer:
|
|
2473
2698
|
"""An async gRPC server using asyncio."""
|
|
2474
2699
|
|
|
2475
|
-
def __init__(
|
|
2700
|
+
def __init__(
|
|
2701
|
+
self,
|
|
2702
|
+
*interceptors: grpc.ServerInterceptor,
|
|
2703
|
+
tls: Optional["GrpcTLSConfig"] = None,
|
|
2704
|
+
) -> None:
|
|
2476
2705
|
self._server: grpc.aio.Server = grpc.aio.server(interceptors=interceptors)
|
|
2477
2706
|
self._service_names: list[str] = []
|
|
2478
2707
|
self._package_name: str = ""
|
|
2479
2708
|
self._port: int = 50051
|
|
2709
|
+
self._tls_config = tls
|
|
2480
2710
|
|
|
2481
2711
|
def set_package_name(self, package_name: str):
|
|
2482
2712
|
"""Set the package name for .proto generation."""
|
|
@@ -2528,7 +2758,16 @@ class AsyncIOServer:
|
|
|
2528
2758
|
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._server)
|
|
2529
2759
|
reflection.enable_server_reflection(SERVICE_NAMES, self._server)
|
|
2530
2760
|
|
|
2531
|
-
|
|
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
|
+
|
|
2532
2771
|
await self._server.start()
|
|
2533
2772
|
|
|
2534
2773
|
shutdown_event = asyncio.Event()
|
|
@@ -2548,116 +2787,19 @@ class AsyncIOServer:
|
|
|
2548
2787
|
print("gRPC server shutdown.")
|
|
2549
2788
|
|
|
2550
2789
|
|
|
2551
|
-
class WSGIApp:
|
|
2552
|
-
"""
|
|
2553
|
-
A WSGI-compatible application that can serve gRPC via sonora's grpcWSGI.
|
|
2554
|
-
Useful for embedding gRPC within an existing WSGI stack.
|
|
2555
|
-
"""
|
|
2556
|
-
|
|
2557
|
-
def __init__(self, app: Any):
|
|
2558
|
-
self._app: grpcWSGI = grpcWSGI(app)
|
|
2559
|
-
self._service_names: list[str] = []
|
|
2560
|
-
self._package_name: str = ""
|
|
2561
|
-
|
|
2562
|
-
def mount(self, obj: object, package_name: str = ""):
|
|
2563
|
-
"""Generate and compile proto files, then mount the service implementation."""
|
|
2564
|
-
pb2_grpc_module, pb2_module = generate_and_compile_proto(obj, package_name) or (
|
|
2565
|
-
None,
|
|
2566
|
-
None,
|
|
2567
|
-
)
|
|
2568
|
-
self.mount_using_pb2_modules(pb2_grpc_module, pb2_module, obj)
|
|
2569
|
-
|
|
2570
|
-
def mount_using_pb2_modules(
|
|
2571
|
-
self, pb2_grpc_module: Any, pb2_module: Any, obj: object
|
|
2572
|
-
):
|
|
2573
|
-
"""Connect the compiled gRPC modules with the service implementation."""
|
|
2574
|
-
concreteServiceClass = connect_obj_with_stub(pb2_grpc_module, pb2_module, obj)
|
|
2575
|
-
service_name = obj.__class__.__name__
|
|
2576
|
-
service_impl = concreteServiceClass()
|
|
2577
|
-
getattr(pb2_grpc_module, f"add_{service_name}Servicer_to_server")(
|
|
2578
|
-
service_impl, self._app
|
|
2579
|
-
)
|
|
2580
|
-
full_service_name = pb2_module.DESCRIPTOR.services_by_name[
|
|
2581
|
-
service_name
|
|
2582
|
-
].full_name
|
|
2583
|
-
self._service_names.append(full_service_name)
|
|
2584
|
-
|
|
2585
|
-
def mount_objs(self, *objs: object):
|
|
2586
|
-
"""Mount multiple service objects into this WSGI app."""
|
|
2587
|
-
for obj in objs:
|
|
2588
|
-
self.mount(obj, self._package_name)
|
|
2589
|
-
|
|
2590
|
-
def __call__(
|
|
2591
|
-
self,
|
|
2592
|
-
environ: dict[str, Any],
|
|
2593
|
-
start_response: Callable[[str, list[tuple[str, str]]], None],
|
|
2594
|
-
) -> Any:
|
|
2595
|
-
"""WSGI entry point."""
|
|
2596
|
-
return self._app(environ, start_response)
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
class ASGIApp:
|
|
2600
|
-
"""
|
|
2601
|
-
An ASGI-compatible application that can serve gRPC via sonora's grpcASGI.
|
|
2602
|
-
Useful for embedding gRPC within an existing ASGI stack.
|
|
2603
|
-
"""
|
|
2604
|
-
|
|
2605
|
-
def __init__(self, app: Any):
|
|
2606
|
-
self._app: grpcASGI = grpcASGI(app)
|
|
2607
|
-
self._service_names: list[str] = []
|
|
2608
|
-
self._package_name: str = ""
|
|
2609
|
-
|
|
2610
|
-
def mount(self, obj: object, package_name: str = ""):
|
|
2611
|
-
"""Generate and compile proto files, then mount the async service implementation."""
|
|
2612
|
-
pb2_grpc_module, pb2_module = generate_and_compile_proto(obj, package_name) or (
|
|
2613
|
-
None,
|
|
2614
|
-
None,
|
|
2615
|
-
)
|
|
2616
|
-
self.mount_using_pb2_modules(pb2_grpc_module, pb2_module, obj)
|
|
2617
|
-
|
|
2618
|
-
def mount_using_pb2_modules(
|
|
2619
|
-
self, pb2_grpc_module: Any, pb2_module: Any, obj: object
|
|
2620
|
-
):
|
|
2621
|
-
"""Connect the compiled gRPC modules with the async service implementation."""
|
|
2622
|
-
concreteServiceClass = connect_obj_with_stub_async(
|
|
2623
|
-
pb2_grpc_module, pb2_module, obj
|
|
2624
|
-
)
|
|
2625
|
-
service_name = obj.__class__.__name__
|
|
2626
|
-
service_impl = concreteServiceClass()
|
|
2627
|
-
getattr(pb2_grpc_module, f"add_{service_name}Servicer_to_server")(
|
|
2628
|
-
service_impl, self._app
|
|
2629
|
-
)
|
|
2630
|
-
full_service_name = pb2_module.DESCRIPTOR.services_by_name[
|
|
2631
|
-
service_name
|
|
2632
|
-
].full_name
|
|
2633
|
-
self._service_names.append(full_service_name)
|
|
2634
|
-
|
|
2635
|
-
def mount_objs(self, *objs: object):
|
|
2636
|
-
"""Mount multiple service objects into this ASGI app."""
|
|
2637
|
-
for obj in objs:
|
|
2638
|
-
self.mount(obj, self._package_name)
|
|
2639
|
-
|
|
2640
|
-
async def __call__(
|
|
2641
|
-
self,
|
|
2642
|
-
scope: dict[str, Any],
|
|
2643
|
-
receive: Callable[[], Any],
|
|
2644
|
-
send: Callable[[dict[str, Any]], Any],
|
|
2645
|
-
) -> Any:
|
|
2646
|
-
"""ASGI entry point."""
|
|
2647
|
-
_ = await self._app(scope, receive, send)
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
2790
|
def get_connecpy_asgi_app_class(connecpy_module: Any, service_name: str):
|
|
2791
|
+
"""Get the ASGI application class from connecpy module (Connecpy v2.x)."""
|
|
2651
2792
|
return getattr(connecpy_module, f"{service_name}ASGIApplication")
|
|
2652
2793
|
|
|
2653
2794
|
|
|
2654
2795
|
def get_connecpy_wsgi_app_class(connecpy_module: Any, service_name: str):
|
|
2796
|
+
"""Get the WSGI application class from connecpy module (Connecpy v2.x)."""
|
|
2655
2797
|
return getattr(connecpy_module, f"{service_name}WSGIApplication")
|
|
2656
2798
|
|
|
2657
2799
|
|
|
2658
|
-
class
|
|
2800
|
+
class ASGIApp:
|
|
2659
2801
|
"""
|
|
2660
|
-
An ASGI-compatible application that can serve Connect-RPC via Connecpy
|
|
2802
|
+
An ASGI-compatible application that can serve Connect-RPC via Connecpy.
|
|
2661
2803
|
"""
|
|
2662
2804
|
|
|
2663
2805
|
def __init__(self):
|
|
@@ -2727,9 +2869,9 @@ class ConnecpyASGIApp:
|
|
|
2727
2869
|
await send({"type": "http.response.body", "body": b"Not Found"})
|
|
2728
2870
|
|
|
2729
2871
|
|
|
2730
|
-
class
|
|
2872
|
+
class WSGIApp:
|
|
2731
2873
|
"""
|
|
2732
|
-
A WSGI-compatible application that can serve Connect-RPC via Connecpy
|
|
2874
|
+
A WSGI-compatible application that can serve Connect-RPC via Connecpy.
|
|
2733
2875
|
"""
|
|
2734
2876
|
|
|
2735
2877
|
def __init__(self):
|
|
@@ -2774,8 +2916,10 @@ class ConnecpyWSGIApp:
|
|
|
2774
2916
|
def __call__(
|
|
2775
2917
|
self,
|
|
2776
2918
|
environ: dict[str, Any],
|
|
2777
|
-
start_response: Callable[
|
|
2778
|
-
|
|
2919
|
+
start_response: Callable[
|
|
2920
|
+
[str, list[tuple[str, str]]], Callable[[bytes], object]
|
|
2921
|
+
],
|
|
2922
|
+
) -> Iterable[bytes]:
|
|
2779
2923
|
"""WSGI entry point with routing for multiple services."""
|
|
2780
2924
|
path = environ.get("PATH_INFO", "")
|
|
2781
2925
|
|