pydantic-rpc 0.10.0__py3-none-any.whl → 0.12.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
@@ -13,6 +13,8 @@ from .decorators import (
13
13
  proto_option,
14
14
  get_method_options,
15
15
  has_http_option,
16
+ error_handler,
17
+ get_error_handlers,
16
18
  )
17
19
  from .tls import (
18
20
  GrpcTLSConfig,
@@ -31,6 +33,8 @@ __all__ = [
31
33
  "proto_option",
32
34
  "get_method_options",
33
35
  "has_http_option",
36
+ "error_handler",
37
+ "get_error_handlers",
34
38
  "GrpcTLSConfig",
35
39
  "extract_peer_identity",
36
40
  "extract_peer_certificate_chain",
pydantic_rpc/core.py CHANGED
@@ -27,7 +27,7 @@ import annotated_types
27
27
  import grpc
28
28
  from grpc import ServicerContext
29
29
  import grpc_tools
30
- from connecpy.code import Code as Errors
30
+ from connectrpc.code import Code as Errors
31
31
 
32
32
  # Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
33
33
  from google.protobuf import duration_pb2, timestamp_pb2, empty_pb2
@@ -726,15 +726,15 @@ def connect_obj_with_stub_async(
726
726
  return ConcreteServiceClass
727
727
 
728
728
 
729
- def connect_obj_with_stub_connecpy(
730
- connecpy_module: Any, pb2_module: Any, obj: object
729
+ def connect_obj_with_stub_connect_python(
730
+ connect_python_module: Any, pb2_module: Any, obj: object
731
731
  ) -> type:
732
732
  """
733
- Connect a Python service object to a Connecpy stub.
733
+ Connect a Python service object to a Connect Python stub.
734
734
  """
735
735
  service_class = obj.__class__
736
736
  stub_class_name = service_class.__name__
737
- stub_class = getattr(connecpy_module, stub_class_name)
737
+ stub_class = getattr(connect_python_module, stub_class_name)
738
738
 
739
739
  class ConcreteServiceClass(stub_class):
740
740
  pass
@@ -845,15 +845,15 @@ def connect_obj_with_stub_connecpy(
845
845
  return ConcreteServiceClass
846
846
 
847
847
 
848
- def connect_obj_with_stub_async_connecpy(
849
- connecpy_module: Any, pb2_module: Any, obj: object
848
+ def connect_obj_with_stub_async_connect_python(
849
+ connect_python_module: Any, pb2_module: Any, obj: object
850
850
  ) -> type:
851
851
  """
852
- Connect a Python service object to a Connecpy stub for async methods with streaming support.
852
+ Connect a Python service object to a Connect Python stub for async methods with streaming support.
853
853
  """
854
854
  service_class = obj.__class__
855
855
  stub_class_name = service_class.__name__
856
- stub_class = getattr(connecpy_module, stub_class_name)
856
+ stub_class = getattr(connect_python_module, stub_class_name)
857
857
 
858
858
  class ConcreteServiceClass(stub_class):
859
859
  pass
@@ -2030,10 +2030,10 @@ def generate_grpc_code(proto_path: Path) -> types.ModuleType | None:
2030
2030
  return module
2031
2031
 
2032
2032
 
2033
- def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2033
+ def generate_connect_python_code(proto_path: Path) -> types.ModuleType | None:
2034
2034
  """
2035
- Run protoc with the Connecpy plugin to generate Python Connecpy code from proto_path.
2036
- Writes foo_connecpy.py next to proto_path, then imports and returns that module.
2035
+ Run protoc with the Connect Python plugin to generate Python Connect code from proto_path.
2036
+ Writes foo_connect_python.py next to proto_path, then imports and returns that module.
2037
2037
  """
2038
2038
  # 1) Ensure the .proto exists
2039
2039
  if not proto_path.is_file():
@@ -2051,7 +2051,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2051
2051
  "protoc", # Dummy program name (required for protoc.main)
2052
2052
  "-I.",
2053
2053
  f"-I{well_known_path}",
2054
- f"--connecpy_out={out_str}",
2054
+ f"--connect-python_out={out_str}",
2055
2055
  proto_path.name,
2056
2056
  ]
2057
2057
 
@@ -2065,7 +2065,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2065
2065
 
2066
2066
  # 4) Locate the generated file
2067
2067
  base_name = proto_path.stem # "foo"
2068
- generated_filename = f"{base_name}_connecpy.py" # "foo_connecpy.py"
2068
+ generated_filename = f"{base_name}_connect.py" # "foo_connect.py"
2069
2069
  generated_filepath = out_dir / generated_filename
2070
2070
 
2071
2071
  # 5) Add out_dir to sys.path so we can import by filename
@@ -2074,7 +2074,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2074
2074
 
2075
2075
  # 6) Load and return the module
2076
2076
  spec = importlib.util.spec_from_file_location(
2077
- base_name + "_connecpy", str(generated_filepath)
2077
+ base_name + "_connect", str(generated_filepath)
2078
2078
  )
2079
2079
  if spec is None or spec.loader is None:
2080
2080
  return None
@@ -2259,7 +2259,7 @@ def get_proto_path(proto_filename: str) -> Path:
2259
2259
  return base / proto_filename
2260
2260
 
2261
2261
 
2262
- def generate_and_compile_proto_using_connecpy(
2262
+ def generate_and_compile_proto_using_connect_python(
2263
2263
  obj: object,
2264
2264
  package_name: str = "",
2265
2265
  existing_proto_path: Path | None = None,
@@ -2268,7 +2268,7 @@ def generate_and_compile_proto_using_connecpy(
2268
2268
  import importlib
2269
2269
 
2270
2270
  pb2_module = None
2271
- connecpy_module = None
2271
+ connect_python_module = None
2272
2272
 
2273
2273
  try:
2274
2274
  pb2_module = importlib.import_module(
@@ -2278,14 +2278,14 @@ def generate_and_compile_proto_using_connecpy(
2278
2278
  pass
2279
2279
 
2280
2280
  try:
2281
- connecpy_module = importlib.import_module(
2282
- f"{obj.__class__.__name__.lower()}_connecpy"
2281
+ connect_python_module = importlib.import_module(
2282
+ f"{obj.__class__.__name__.lower()}_connect"
2283
2283
  )
2284
2284
  except ImportError:
2285
2285
  pass
2286
2286
 
2287
- if connecpy_module is not None and pb2_module is not None:
2288
- return connecpy_module, pb2_module
2287
+ if connect_python_module is not None and pb2_module is not None:
2288
+ return connect_python_module, pb2_module
2289
2289
 
2290
2290
  # If the modules are not found, generate and compile the proto files.
2291
2291
 
@@ -2306,10 +2306,10 @@ def generate_and_compile_proto_using_connecpy(
2306
2306
  if gen_pb is None:
2307
2307
  raise Exception("Generating pb code")
2308
2308
 
2309
- gen_connecpy = generate_connecpy_code(proto_file_path)
2310
- if gen_connecpy is None:
2311
- raise Exception("Generating Connecpy code")
2312
- return gen_connecpy, gen_pb
2309
+ gen_connect_python = generate_connect_python_code(proto_file_path)
2310
+ if gen_connect_python is None:
2311
+ raise Exception("Generating Connect Python code")
2312
+ return gen_connect_python, gen_pb
2313
2313
 
2314
2314
 
2315
2315
  def is_combined_proto_enabled() -> bool:
@@ -2606,6 +2606,9 @@ class Server:
2606
2606
 
2607
2607
  def __init__(
2608
2608
  self,
2609
+ service: Optional[object] = None,
2610
+ port: int = 50051,
2611
+ package_name: str = "",
2609
2612
  max_workers: int = 8,
2610
2613
  *interceptors: Any,
2611
2614
  tls: Optional["GrpcTLSConfig"] = None,
@@ -2614,9 +2617,10 @@ class Server:
2614
2617
  futures.ThreadPoolExecutor(max_workers), interceptors=interceptors
2615
2618
  )
2616
2619
  self._service_names: list[str] = []
2617
- self._package_name: str = ""
2618
- self._port: int = 50051
2620
+ self._package_name: str = package_name
2621
+ self._port: int = port
2619
2622
  self._tls_config = tls
2623
+ self._initial_service = service
2620
2624
 
2621
2625
  def set_package_name(self, package_name: str):
2622
2626
  """Set the package name for .proto generation."""
@@ -2654,6 +2658,11 @@ class Server:
2654
2658
  Mount multiple services and run the gRPC server with reflection and health check.
2655
2659
  Press Ctrl+C or send SIGTERM to stop.
2656
2660
  """
2661
+ # Mount initial service if provided
2662
+ if self._initial_service:
2663
+ self.mount(self._initial_service, self._package_name)
2664
+
2665
+ # Mount additional services
2657
2666
  for obj in objs:
2658
2667
  self.mount(obj, self._package_name)
2659
2668
 
@@ -2699,14 +2708,18 @@ class AsyncIOServer:
2699
2708
 
2700
2709
  def __init__(
2701
2710
  self,
2711
+ service: Optional[object] = None,
2712
+ port: int = 50051,
2713
+ package_name: str = "",
2702
2714
  *interceptors: grpc.ServerInterceptor,
2703
2715
  tls: Optional["GrpcTLSConfig"] = None,
2704
2716
  ) -> None:
2705
2717
  self._server: grpc.aio.Server = grpc.aio.server(interceptors=interceptors)
2706
2718
  self._service_names: list[str] = []
2707
- self._package_name: str = ""
2708
- self._port: int = 50051
2719
+ self._package_name: str = package_name
2720
+ self._port: int = port
2709
2721
  self._tls_config = tls
2722
+ self._initial_service = service
2710
2723
 
2711
2724
  def set_package_name(self, package_name: str):
2712
2725
  """Set the package name for .proto generation."""
@@ -2746,6 +2759,11 @@ class AsyncIOServer:
2746
2759
  Mount multiple async services and run the gRPC server with reflection and health check.
2747
2760
  Press Ctrl+C or send SIGTERM to stop.
2748
2761
  """
2762
+ # Mount initial service if provided
2763
+ if self._initial_service:
2764
+ self.mount(self._initial_service, self._package_name)
2765
+
2766
+ # Mount additional services
2749
2767
  for obj in objs:
2750
2768
  self.mount(obj, self._package_name)
2751
2769
 
@@ -2786,46 +2804,53 @@ class AsyncIOServer:
2786
2804
  await self._server.stop(10)
2787
2805
  print("gRPC server shutdown.")
2788
2806
 
2807
+ async def stop(self, grace: float = 10.0):
2808
+ """Stop the gRPC server gracefully."""
2809
+ await self._server.stop(grace)
2810
+
2789
2811
 
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)."""
2792
- return getattr(connecpy_module, f"{service_name}ASGIApplication")
2812
+ def get_connect_python_asgi_app_class(connect_python_module: Any, service_name: str):
2813
+ """Get the ASGI application class from connect-python module."""
2814
+ return getattr(connect_python_module, f"{service_name}ASGIApplication")
2793
2815
 
2794
2816
 
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)."""
2797
- return getattr(connecpy_module, f"{service_name}WSGIApplication")
2817
+ def get_connect_python_wsgi_app_class(connect_python_module: Any, service_name: str):
2818
+ """Get the WSGI application class from connect-python module."""
2819
+ return getattr(connect_python_module, f"{service_name}WSGIApplication")
2798
2820
 
2799
2821
 
2800
2822
  class ASGIApp:
2801
2823
  """
2802
- An ASGI-compatible application that can serve Connect-RPC via Connecpy.
2824
+ An ASGI-compatible application that can serve Connect-RPC via connect-python.
2803
2825
  """
2804
2826
 
2805
- def __init__(self):
2827
+ def __init__(self, service: Optional[object] = None, package_name: str = ""):
2806
2828
  self._services: list[tuple[Any, str]] = [] # List of (app, path) tuples
2807
2829
  self._service_names: list[str] = []
2808
- self._package_name: str = ""
2830
+ self._package_name: str = package_name
2831
+ self._initial_service = service
2809
2832
 
2810
2833
  def mount(self, obj: object, package_name: str = ""):
2811
2834
  """Generate and compile proto files, then mount the async service implementation."""
2812
- connecpy_module, pb2_module = generate_and_compile_proto_using_connecpy(
2813
- obj, package_name
2835
+ connect_python_module, pb2_module = (
2836
+ generate_and_compile_proto_using_connect_python(obj, package_name)
2814
2837
  )
2815
- self.mount_using_pb2_modules(connecpy_module, pb2_module, obj)
2838
+ self.mount_using_pb2_modules(connect_python_module, pb2_module, obj)
2816
2839
 
2817
2840
  def mount_using_pb2_modules(
2818
- self, connecpy_module: Any, pb2_module: Any, obj: object
2841
+ self, connect_python_module: Any, pb2_module: Any, obj: object
2819
2842
  ):
2820
- """Connect the compiled connecpy and pb2 modules with the async service implementation."""
2821
- concreteServiceClass = connect_obj_with_stub_async_connecpy(
2822
- connecpy_module, pb2_module, obj
2843
+ """Connect the compiled connect-python and pb2 modules with the async service implementation."""
2844
+ concreteServiceClass = connect_obj_with_stub_async_connect_python(
2845
+ connect_python_module, pb2_module, obj
2823
2846
  )
2824
2847
  service_name = obj.__class__.__name__
2825
2848
  service_impl = concreteServiceClass()
2826
2849
 
2827
2850
  # Get the service-specific ASGI application class
2828
- app_class = get_connecpy_asgi_app_class(connecpy_module, service_name)
2851
+ app_class = get_connect_python_asgi_app_class(
2852
+ connect_python_module, service_name
2853
+ )
2829
2854
  app = app_class(service=service_impl)
2830
2855
 
2831
2856
  # Store the app and its path for routing
@@ -2838,6 +2863,11 @@ class ASGIApp:
2838
2863
 
2839
2864
  def mount_objs(self, *objs: object):
2840
2865
  """Mount multiple service objects into this ASGI app."""
2866
+ # Mount initial service if provided
2867
+ if self._initial_service:
2868
+ self.mount(self._initial_service, self._package_name)
2869
+
2870
+ # Mount additional services
2841
2871
  for obj in objs:
2842
2872
  self.mount(obj, self._package_name)
2843
2873
 
@@ -2848,6 +2878,10 @@ class ASGIApp:
2848
2878
  send: Callable[[dict[str, Any]], Any],
2849
2879
  ):
2850
2880
  """ASGI entry point with routing for multiple services."""
2881
+ # Mount initial service on first call if not already mounted
2882
+ if self._initial_service and not self._services:
2883
+ self.mount(self._initial_service, self._package_name)
2884
+
2851
2885
  if scope["type"] != "http":
2852
2886
  await send({"type": "http.response.start", "status": 404})
2853
2887
  await send({"type": "http.response.body", "body": b"Not Found"})
@@ -2871,33 +2905,36 @@ class ASGIApp:
2871
2905
 
2872
2906
  class WSGIApp:
2873
2907
  """
2874
- A WSGI-compatible application that can serve Connect-RPC via Connecpy.
2908
+ A WSGI-compatible application that can serve Connect-RPC via connect-python.
2875
2909
  """
2876
2910
 
2877
- def __init__(self):
2911
+ def __init__(self, service: Optional[object] = None, package_name: str = ""):
2878
2912
  self._services: list[tuple[Any, str]] = [] # List of (app, path) tuples
2879
2913
  self._service_names: list[str] = []
2880
- self._package_name: str = ""
2914
+ self._package_name: str = package_name
2915
+ self._initial_service = service
2881
2916
 
2882
2917
  def mount(self, obj: object, package_name: str = ""):
2883
2918
  """Generate and compile proto files, then mount the sync service implementation."""
2884
- connecpy_module, pb2_module = generate_and_compile_proto_using_connecpy(
2885
- obj, package_name
2919
+ connect_python_module, pb2_module = (
2920
+ generate_and_compile_proto_using_connect_python(obj, package_name)
2886
2921
  )
2887
- self.mount_using_pb2_modules(connecpy_module, pb2_module, obj)
2922
+ self.mount_using_pb2_modules(connect_python_module, pb2_module, obj)
2888
2923
 
2889
2924
  def mount_using_pb2_modules(
2890
- self, connecpy_module: Any, pb2_module: Any, obj: object
2925
+ self, connect_python_module: Any, pb2_module: Any, obj: object
2891
2926
  ):
2892
- """Connect the compiled connecpy and pb2 modules with the sync service implementation."""
2893
- concreteServiceClass = connect_obj_with_stub_connecpy(
2894
- connecpy_module, pb2_module, obj
2927
+ """Connect the compiled connect-python and pb2 modules with the sync service implementation."""
2928
+ concreteServiceClass = connect_obj_with_stub_connect_python(
2929
+ connect_python_module, pb2_module, obj
2895
2930
  )
2896
2931
  service_name = obj.__class__.__name__
2897
2932
  service_impl = concreteServiceClass()
2898
2933
 
2899
2934
  # Get the service-specific WSGI application class
2900
- app_class = get_connecpy_wsgi_app_class(connecpy_module, service_name)
2935
+ app_class = get_connect_python_wsgi_app_class(
2936
+ connect_python_module, service_name
2937
+ )
2901
2938
  app = app_class(service=service_impl)
2902
2939
 
2903
2940
  # Store the app and its path for routing
@@ -2910,6 +2947,11 @@ class WSGIApp:
2910
2947
 
2911
2948
  def mount_objs(self, *objs: object):
2912
2949
  """Mount multiple service objects into this WSGI app."""
2950
+ # Mount initial service if provided
2951
+ if self._initial_service:
2952
+ self.mount(self._initial_service, self._package_name)
2953
+
2954
+ # Mount additional services
2913
2955
  for obj in objs:
2914
2956
  self.mount(obj, self._package_name)
2915
2957
 
@@ -2921,6 +2963,10 @@ class WSGIApp:
2921
2963
  ],
2922
2964
  ) -> Iterable[bytes]:
2923
2965
  """WSGI entry point with routing for multiple services."""
2966
+ # Mount initial service on first call if not already mounted
2967
+ if self._initial_service and not self._services:
2968
+ self.mount(self._initial_service, self._package_name)
2969
+
2924
2970
  path = environ.get("PATH_INFO", "")
2925
2971
 
2926
2972
  # Route to the appropriate service based on path
@@ -1,12 +1,15 @@
1
1
  """Decorators for adding protobuf options to RPC methods."""
2
2
 
3
- from typing import Any, Callable, Dict, List, Optional, TypeVar
3
+ from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
4
4
  from functools import wraps
5
+ import grpc
6
+ from connectrpc.code import Code as ConnectErrors
5
7
 
6
8
  from .options import OptionMetadata, OPTION_METADATA_ATTR
7
9
 
8
10
 
9
11
  F = TypeVar("F", bound=Callable[..., Any])
12
+ ERROR_HANDLER_ATTR = "__pydantic_rpc_error_handlers__"
10
13
 
11
14
 
12
15
  def http_option(
@@ -136,3 +139,71 @@ def has_proto_options(method: Callable) -> bool:
136
139
  """
137
140
  metadata = get_method_options(method)
138
141
  return metadata is not None and len(metadata.proto_options) > 0
142
+
143
+
144
+ def error_handler(
145
+ exception_type: Type[Exception],
146
+ status_code: Optional[grpc.StatusCode] = None,
147
+ connect_code: Optional[ConnectErrors] = None,
148
+ handler: Optional[Callable[[Exception], tuple[str, Any]]] = None,
149
+ ) -> Callable[[F], F]:
150
+ """
151
+ Decorator to add automatic error handling to an RPC method.
152
+
153
+ Args:
154
+ exception_type: The type of exception to handle
155
+ status_code: The gRPC status code to return (for gRPC services)
156
+ connect_code: The Connect error code to return (for Connect services)
157
+ handler: Optional custom handler function that returns (message, details)
158
+
159
+ Example:
160
+ @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
161
+ @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
162
+ async def get_user(self, request: GetUserRequest) -> User:
163
+ ...
164
+ """
165
+
166
+ def decorator(func: F) -> F:
167
+ # Get or create error handlers list
168
+ if not hasattr(func, ERROR_HANDLER_ATTR):
169
+ setattr(func, ERROR_HANDLER_ATTR, [])
170
+
171
+ handlers = getattr(func, ERROR_HANDLER_ATTR)
172
+
173
+ # Add this handler to the list
174
+ handlers.append(
175
+ {
176
+ "exception_type": exception_type,
177
+ "status_code": status_code or grpc.StatusCode.INTERNAL,
178
+ "connect_code": connect_code or ConnectErrors.INTERNAL,
179
+ "handler": handler,
180
+ }
181
+ )
182
+
183
+ @wraps(func)
184
+ def wrapper(*args, **kwargs):
185
+ return func(*args, **kwargs)
186
+
187
+ # Preserve the error handlers on the wrapper
188
+ setattr(wrapper, ERROR_HANDLER_ATTR, handlers)
189
+
190
+ # Preserve any existing option metadata
191
+ if hasattr(func, OPTION_METADATA_ATTR):
192
+ setattr(wrapper, OPTION_METADATA_ATTR, getattr(func, OPTION_METADATA_ATTR))
193
+
194
+ return wrapper # type: ignore
195
+
196
+ return decorator
197
+
198
+
199
+ def get_error_handlers(method: Callable) -> Optional[List[Dict[str, Any]]]:
200
+ """
201
+ Get error handlers from a method.
202
+
203
+ Args:
204
+ method: The method to get error handlers from
205
+
206
+ Returns:
207
+ List of error handler configurations if present, None otherwise
208
+ """
209
+ return getattr(method, ERROR_HANDLER_ATTR, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.10.0
3
+ Version: 0.12.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
@@ -9,7 +9,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: connecpy>=2.2.0
12
+ Requires-Dist: connect-python>=0.5.0
13
+ Requires-Dist: protoc-gen-connect-python>=0.1.0
13
14
  Requires-Dist: mcp>=1.9.4
14
15
  Requires-Dist: starlette>=0.27.0
15
16
  Requires-Python: >=3.11
@@ -62,9 +63,10 @@ class OlympicsLocationAgent:
62
63
 
63
64
 
64
65
  if __name__ == "__main__":
65
- s = AsyncIOServer()
66
+ # New enhanced initialization API (optional - backward compatible)
67
+ s = AsyncIOServer(service=OlympicsLocationAgent(), port=50051)
66
68
  loop = asyncio.get_event_loop()
67
- loop.run_until_complete(s.run(OlympicsLocationAgent()))
69
+ loop.run_until_complete(s.run())
68
70
  ```
69
71
 
70
72
  And here is an example of a simple Connect RPC service that exposes the same agent as an ASGI application:
@@ -106,8 +108,8 @@ class OlympicsLocationAgent:
106
108
  result = await self._agent.run(req.prompt())
107
109
  return result.data
108
110
 
109
- app = ASGIApp()
110
- app.mount(OlympicsLocationAgent())
111
+ # New enhanced initialization API (optional - backward compatible)
112
+ app = ASGIApp(service=OlympicsLocationAgent())
111
113
 
112
114
  ```
113
115
 
@@ -129,6 +131,17 @@ app.mount(OlympicsLocationAgent())
129
131
  - 🛠️ **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.
130
132
  - 🤖 **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.
131
133
 
134
+ ## ⚠️ Important Notes for Connect-RPC
135
+
136
+ When using Connect-RPC with ASGIApp:
137
+
138
+ - **Endpoint Path Format**: Connect-RPC endpoints use CamelCase method names in the path: `/<package>.<service>/<Method>` (e.g., `/chat.v1.ChatService/SendMessage`)
139
+ - **Content-Type**: Set `Content-Type: application/json` or `application/connect+json` for requests
140
+ - **HTTP/2 Requirement**: Bidirectional streaming requires HTTP/2. Use Hypercorn instead of uvicorn for HTTP/2 support
141
+ - **Testing**: Use [buf curl](https://buf.build/docs/ecosystem/cli/curl) for testing Connect-RPC endpoints with proper streaming support
142
+
143
+ For detailed examples and testing instructions, see the [examples directory](examples/).
144
+
132
145
  ## 📦 Installation
133
146
 
134
147
  Install PydanticRPC via pip:
@@ -137,6 +150,54 @@ Install PydanticRPC via pip:
137
150
  pip install pydantic-rpc
138
151
  ```
139
152
 
153
+ For CLI support with built-in server runners:
154
+
155
+ ```bash
156
+ pip install pydantic-rpc-cli # Includes hypercorn and gunicorn
157
+ ```
158
+
159
+ ## 🆕 Enhanced Features (v0.10.0+)
160
+
161
+ **Note: All new features are fully backward compatible. Existing code continues to work without modification.**
162
+
163
+ ### Enhanced Initialization API
164
+ All server classes now support optional initialization with services:
165
+
166
+ ```python
167
+ # Traditional API (still works)
168
+ server = AsyncIOServer()
169
+ server.set_port(50051)
170
+ await server.run(MyService())
171
+
172
+ # New enhanced API (optional)
173
+ server = AsyncIOServer(
174
+ service=MyService(),
175
+ port=50051,
176
+ package_name="my.package"
177
+ )
178
+ await server.run()
179
+
180
+ # Same for ASGI/WSGI apps
181
+ app = ASGIApp(service=MyService(), package_name="my.package")
182
+ ```
183
+
184
+ ### Error Handling with Decorators
185
+ Automatically map exceptions to gRPC/Connect status codes:
186
+
187
+ ```python
188
+ from pydantic_rpc import error_handler
189
+ import grpc
190
+
191
+ class MyService:
192
+ @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
193
+ @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
194
+ async def get_user(self, request: GetUserRequest) -> User:
195
+ # Exceptions are automatically converted to proper status codes
196
+ if request.id not in users_db:
197
+ raise KeyError(f"User {request.id} not found")
198
+ return users_db[request.id]
199
+ ```
200
+
140
201
  ## 🚀 Getting Started
141
202
 
142
203
  PydanticRPC supports two main protocols:
@@ -1090,15 +1151,40 @@ MCP endpoints will be available at:
1090
1151
  - SSE: `GET http://localhost:8000/mcp/sse`
1091
1152
  - Messages: `POST http://localhost:8000/mcp/messages/`
1092
1153
 
1093
- ### 🗄️ Protobuf file and code (Python files) generation using CLI
1154
+ ### 🗄️ CLI Tool (pydantic-rpc-cli)
1155
+
1156
+ The CLI tool provides powerful features for generating protobuf files and running servers. Install it separately:
1157
+
1158
+ ```bash
1159
+ pip install pydantic-rpc-cli
1160
+ ```
1161
+
1162
+ #### Generate Protobuf Files
1094
1163
 
1095
- You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
1164
+ ```bash
1165
+ # Generate .proto file from a service class
1166
+ pydantic-rpc generate myapp.services.UserService --output ./proto/
1167
+
1168
+ # Also compile to Python code
1169
+ pydantic-rpc generate myapp.services.UserService --compile
1170
+ ```
1171
+
1172
+ #### Run Servers Directly
1173
+
1174
+ The CLI can run any type of server:
1096
1175
 
1097
1176
  ```bash
1098
- pydantic-rpc a_module.py aClassName
1177
+ # Run as gRPC server (auto-detects async/sync)
1178
+ pydantic-rpc serve myapp.services.UserService --port 50051
1179
+
1180
+ # Run as Connect-RPC with ASGI (HTTP/2, uses Hypercorn)
1181
+ pydantic-rpc serve myapp.services.UserService --asgi --port 8000
1182
+
1183
+ # Run as Connect-RPC with WSGI (HTTP/1.1, uses Gunicorn)
1184
+ pydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4
1099
1185
  ```
1100
1186
 
1101
- Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you could generate code for any desired language other than Python.
1187
+ Using the generated proto files with tools like `protoc`, `buf` and `BSR`, you can generate code for any desired language.
1102
1188
 
1103
1189
 
1104
1190
  ## 📖 Data Type Mapping
@@ -1162,6 +1248,64 @@ class MyMessage(Message):
1162
1248
 
1163
1249
  This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
1164
1250
 
1251
+ ## 🔧 Development
1252
+
1253
+ This project uses [`just`](https://github.com/casey/just) as a command runner for development tasks.
1254
+
1255
+ ### Installing just
1256
+
1257
+ **macOS:**
1258
+ ```bash
1259
+ brew install just
1260
+ ```
1261
+
1262
+ **Linux:**
1263
+ ```bash
1264
+ curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin
1265
+ ```
1266
+
1267
+ **Windows:**
1268
+ Download from [GitHub releases](https://github.com/casey/just/releases)
1269
+
1270
+ ### Quick Start
1271
+
1272
+ ```bash
1273
+ # Install dependencies
1274
+ just install
1275
+
1276
+ # Run tests
1277
+ just test # or just t
1278
+
1279
+ # Format and lint code
1280
+ just format # or just f
1281
+ just lint # or just l
1282
+
1283
+ # Run all checks (lint + tests)
1284
+ just check # or just c
1285
+
1286
+ # See all available commands
1287
+ just --list
1288
+ ```
1289
+
1290
+ ### Running Examples
1291
+
1292
+ ```bash
1293
+ # Start servers
1294
+ just greeting-server # gRPC server on port 50051
1295
+ just greeting-asgi # Connect RPC ASGI on port 8000
1296
+ just greeting-wsgi # Connect RPC WSGI on port 3000
1297
+
1298
+ # Test with buf curl (in another terminal)
1299
+ just greet # gRPC request
1300
+ just connect-greet # Connect RPC request
1301
+ just wsgi-greet # WSGI request
1302
+
1303
+ # Custom names
1304
+ just greet-name Alice
1305
+ just connect-greet-name Bob
1306
+ ```
1307
+
1308
+ For more development commands and options, see the [Justfile](Justfile) or run `just --list`.
1165
1309
 
1166
1310
  ## TODO
1167
1311
  - [x] Streaming Support
@@ -1,13 +1,12 @@
1
- pydantic_rpc/__init__.py,sha256=a8c4ed1a731b7f601006cbbd4aff16d43bbca0b7ffb02c698f1c79dccefe2f8e,781
2
- pydantic_rpc/core.py,sha256=b2867838460a23b1ce302c9d7e6ef2a2e711c6c84887a50e3ad40b94ff5dc53c,112505
3
- pydantic_rpc/decorators.py,sha256=2f2dc3f642a6e40d05187901e75269415a70b8f18d555e6c7ac923dee149d34c,3852
1
+ pydantic_rpc/__init__.py,sha256=c5e0b5d4ed6de3e3c7c1ada5389e704ce149ea56ca0930392db6abdf222b4e86,871
2
+ pydantic_rpc/core.py,sha256=450c03111224052a3e2b85567ffd16a0ff6dd4d6f02bcfa23c09f6d39cf30861,114578
3
+ pydantic_rpc/decorators.py,sha256=94d87825e034b6303df46dd74e6c8c7ed5c88133aa504d61249a83dbf82f1de3,6219
4
4
  pydantic_rpc/mcp/__init__.py,sha256=f05ad62cc38db5c5972e16870c484523c58c5ac650d8454706f1ce4539cc7a52,123
5
5
  pydantic_rpc/mcp/converter.py,sha256=b60dcaf82e6bff6be4b2ab8b1e9f2d16e09cb98d8f00182a4f6f73d1a78848a4,4158
6
6
  pydantic_rpc/mcp/exporter.py,sha256=20833662f9a973ed9e1a705b9b59aaa2533de6c514ebd87ef66664a430a0d04f,10239
7
7
  pydantic_rpc/options.py,sha256=6094036184a500b92715fd91d31ecc501460a56de47dcf6024c6335ae8e5d6e3,4561
8
8
  pydantic_rpc/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
9
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,,
10
+ pydantic_rpc-0.12.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
11
+ pydantic_rpc-0.12.0.dist-info/METADATA,sha256=daa91c220e04adae53cb3e654d249f2feaca8e461f84e9d03f70fad2b48e403b,36156
12
+ pydantic_rpc-0.12.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- pydantic-rpc = pydantic_rpc.core:main
3
-