pydantic-rpc 0.13.0__tar.gz → 0.15.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.13.0
3
+ Version: 0.15.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
@@ -125,7 +125,7 @@ app = ASGIApp(service=OlympicsLocationAgent())
125
125
  - 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
126
126
  - ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
127
127
  - **For Connect-RPC:**
128
- - 🌐 **Full Protocol Support:** Native Connect-RPC support via `Connecpy` v2.2.0+
128
+ - 🌐 **Full Protocol Support:** Native Connect-RPC support via `connect-python`
129
129
  - 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
130
130
  - 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
131
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.
@@ -304,21 +304,21 @@ app.mount(Greeter())
304
304
 
305
305
  ### 🏆 Connect-RPC with Streaming Example
306
306
 
307
- 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:
307
+ PydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
308
308
 
309
309
  ```bash
310
310
  # Run with uvicorn
311
311
  uv run uvicorn greeting_asgi:app --port 3000
312
312
 
313
313
  # Or run streaming example
314
- uv run python examples/streaming_connecpy.py
314
+ uv run python examples/streaming_connect_python.py
315
315
  ```
316
316
 
317
- This will launch a Connecpy-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
317
+ This will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
318
318
 
319
- #### Streaming Support with Connecpy
319
+ #### Streaming Support with connect-python
320
320
 
321
- Connecpy v2.2.0 provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
321
+ connect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
322
322
 
323
323
  ```python
324
324
  from typing import AsyncIterator
@@ -359,14 +359,7 @@ app.mount(StreamingService())
359
359
  ```
360
360
 
361
361
  > [!NOTE]
362
- > Please install `protoc-gen-connecpy` to run the Connecpy example.
363
- >
364
- > 1. Install Go.
365
- > - Please follow the instruction described in https://go.dev/doc/install.
366
- > 2. Install `protoc-gen-connecpy`:
367
- > ```bash
368
- > go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
369
- > ```
362
+ > Please install `protoc-gen-connect-python` to run the connect-python example.
370
363
 
371
364
  ## ♻️ Skipping Protobuf Generation
372
365
  By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
@@ -375,9 +368,9 @@ By default, PydanticRPC generates .proto files and code at runtime. If you wish
375
368
  export PYDANTIC_RPC_SKIP_GENERATION=true
376
369
  ```
377
370
 
378
- When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
371
+ When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.
379
372
 
380
- ## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
373
+ ## 🪧 Setting Protobuf and Connect RPC/gRPC generation directory
381
374
  By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
382
375
 
383
376
  ```bash
@@ -107,7 +107,7 @@ app = ASGIApp(service=OlympicsLocationAgent())
107
107
  - 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
108
108
  - ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
109
109
  - **For Connect-RPC:**
110
- - 🌐 **Full Protocol Support:** Native Connect-RPC support via `Connecpy` v2.2.0+
110
+ - 🌐 **Full Protocol Support:** Native Connect-RPC support via `connect-python`
111
111
  - 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
112
112
  - 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
113
113
  - 🛠️ **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.
@@ -286,21 +286,21 @@ app.mount(Greeter())
286
286
 
287
287
  ### 🏆 Connect-RPC with Streaming Example
288
288
 
289
- 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:
289
+ PydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
290
290
 
291
291
  ```bash
292
292
  # Run with uvicorn
293
293
  uv run uvicorn greeting_asgi:app --port 3000
294
294
 
295
295
  # Or run streaming example
296
- uv run python examples/streaming_connecpy.py
296
+ uv run python examples/streaming_connect_python.py
297
297
  ```
298
298
 
299
- This will launch a Connecpy-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
299
+ This will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
300
300
 
301
- #### Streaming Support with Connecpy
301
+ #### Streaming Support with connect-python
302
302
 
303
- Connecpy v2.2.0 provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
303
+ connect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
304
304
 
305
305
  ```python
306
306
  from typing import AsyncIterator
@@ -341,14 +341,7 @@ app.mount(StreamingService())
341
341
  ```
342
342
 
343
343
  > [!NOTE]
344
- > Please install `protoc-gen-connecpy` to run the Connecpy example.
345
- >
346
- > 1. Install Go.
347
- > - Please follow the instruction described in https://go.dev/doc/install.
348
- > 2. Install `protoc-gen-connecpy`:
349
- > ```bash
350
- > go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
351
- > ```
344
+ > Please install `protoc-gen-connect-python` to run the connect-python example.
352
345
 
353
346
  ## ♻️ Skipping Protobuf Generation
354
347
  By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
@@ -357,9 +350,9 @@ By default, PydanticRPC generates .proto files and code at runtime. If you wish
357
350
  export PYDANTIC_RPC_SKIP_GENERATION=true
358
351
  ```
359
352
 
360
- When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating them on the fly.
353
+ When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.
361
354
 
362
- ## 🪧 Setting Protobuf and Connecpy/gRPC generation directory
355
+ ## 🪧 Setting Protobuf and Connect RPC/gRPC generation directory
363
356
  By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
364
357
 
365
358
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-rpc"
3
- version = "0.13.0"
3
+ version = "0.15.0"
4
4
  description = "A Python library for building gRPC/ConnectRPC services with Pydantic models."
5
5
  authors = [
6
6
  { name = "Yasushi Itoh" }
@@ -15,6 +15,7 @@ from .decorators import (
15
15
  has_http_option,
16
16
  error_handler,
17
17
  get_error_handlers,
18
+ invoke_error_handler,
18
19
  )
19
20
  from .tls import (
20
21
  GrpcTLSConfig,
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "has_http_option",
36
37
  "error_handler",
37
38
  "get_error_handlers",
39
+ "invoke_error_handler",
38
40
  "GrpcTLSConfig",
39
41
  "extract_peer_identity",
40
42
  "extract_peer_certificate_chain",
@@ -11,9 +11,11 @@ import signal
11
11
  import sys
12
12
  import time
13
13
  import types
14
- from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
14
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
15
15
  from concurrent import futures
16
16
  from connectrpc.code import Code as Errors
17
+ from connectrpc.errors import ConnectError
18
+
17
19
  # Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
18
20
  from google.protobuf import duration_pb2, timestamp_pb2, empty_pb2
19
21
  from grpc import ServicerContext
@@ -32,12 +34,17 @@ from typing import (
32
34
  get_origin,
33
35
  cast,
34
36
  TypeGuard,
37
+ Union,
38
+ Tuple,
35
39
  )
36
- from typing import Union
37
- from typing import Union, Sequence, Tuple
38
40
  from concurrent.futures import Executor
39
41
 
40
- from .decorators import get_method_options, has_http_option
42
+ from .decorators import (
43
+ get_method_options,
44
+ has_http_option,
45
+ get_error_handlers,
46
+ invoke_error_handler,
47
+ )
41
48
  from .tls import GrpcTLSConfig
42
49
 
43
50
  ###############################################################################
@@ -309,6 +316,116 @@ def generate_message_converter(
309
316
  return converter
310
317
 
311
318
 
319
+ def handle_validation_error_sync(
320
+ exc: ValidationError,
321
+ method: Callable,
322
+ context: Any,
323
+ request: Any = None,
324
+ is_grpc: bool = True,
325
+ ) -> Any:
326
+ """
327
+ Handle ValidationError with custom error handlers or default behavior (sync version).
328
+
329
+ Args:
330
+ exc: The ValidationError that was raised
331
+ method: The RPC method being called
332
+ context: The gRPC or Connect context
333
+ request: Optional raw request data
334
+ is_grpc: True for gRPC, False for Connect RPC
335
+
336
+ Returns:
337
+ Result of context.abort() call
338
+ """
339
+ error_handlers = get_error_handlers(method)
340
+
341
+ if error_handlers:
342
+ # Check if there's a handler for ValidationError
343
+ for handler_config in error_handlers:
344
+ if isinstance(exc, handler_config["exception_type"]):
345
+ if handler_config["handler"]:
346
+ # Custom handler function
347
+ try:
348
+ msg, _details = invoke_error_handler(
349
+ handler_config["handler"], exc, request
350
+ )
351
+ except Exception:
352
+ # Handler failed, fall back to default
353
+ msg = str(exc)
354
+ else:
355
+ # No custom handler, use default message
356
+ msg = str(exc)
357
+
358
+ # Use the configured status code
359
+ if is_grpc:
360
+ status_code = handler_config["status_code"]
361
+ return context.abort(status_code, msg)
362
+ else:
363
+ status_code = handler_config["connect_code"]
364
+ raise ConnectError(code=status_code, message=msg)
365
+
366
+ # No handler found, use default behavior
367
+ if is_grpc:
368
+ return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
369
+ else:
370
+ raise ConnectError(code=Errors.INVALID_ARGUMENT, message=str(exc))
371
+
372
+
373
+ async def handle_validation_error_async(
374
+ exc: ValidationError,
375
+ method: Callable,
376
+ context: Any,
377
+ request: Any = None,
378
+ is_grpc: bool = True,
379
+ ) -> Any:
380
+ """
381
+ Handle ValidationError with custom error handlers or default behavior (async version).
382
+
383
+ Args:
384
+ exc: The ValidationError that was raised
385
+ method: The RPC method being called
386
+ context: The gRPC or Connect context
387
+ request: Optional raw request data
388
+ is_grpc: True for gRPC, False for Connect RPC
389
+
390
+ Returns:
391
+ Result of context.abort() call
392
+ """
393
+ error_handlers = get_error_handlers(method)
394
+
395
+ if error_handlers:
396
+ # Check if there's a handler for ValidationError
397
+ for handler_config in error_handlers:
398
+ if isinstance(exc, handler_config["exception_type"]):
399
+ # Found a matching handler
400
+ if handler_config["handler"]:
401
+ # Custom handler function
402
+ try:
403
+ msg, _details = invoke_error_handler(
404
+ handler_config["handler"], exc, request
405
+ )
406
+ except Exception:
407
+ # Handler failed, fall back to default
408
+ msg = str(exc)
409
+ else:
410
+ # No custom handler, use default message
411
+ msg = str(exc)
412
+
413
+ # Use the configured status code
414
+ if is_grpc:
415
+ status_code = handler_config["status_code"]
416
+ await context.abort(status_code, msg)
417
+ return
418
+ else:
419
+ status_code = handler_config["connect_code"]
420
+ raise ConnectError(code=status_code, message=msg)
421
+
422
+ # No handler found, use default behavior
423
+ if is_grpc:
424
+ await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
425
+ else:
426
+ raise ConnectError(code=Errors.INVALID_ARGUMENT, message=str(exc))
427
+
428
+
312
429
  def python_value_to_proto_value(field_type: type[Any], value: Any) -> Any:
313
430
  """
314
431
  Converts Python values to protobuf values.
@@ -394,7 +511,9 @@ def connect_obj_with_stub(
394
511
  resp_obj, response_type, pb2_module
395
512
  )
396
513
  except ValidationError as e:
397
- return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
514
+ return handle_validation_error_sync(
515
+ e, original, context, request, is_grpc=True
516
+ )
398
517
  except Exception as e:
399
518
  return context.abort(grpc.StatusCode.INTERNAL, str(e))
400
519
 
@@ -431,7 +550,9 @@ def connect_obj_with_stub(
431
550
  resp_obj, response_type, pb2_module
432
551
  )
433
552
  except ValidationError as e:
434
- return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
553
+ return handle_validation_error_sync(
554
+ e, original, context, request, is_grpc=True
555
+ )
435
556
  except Exception as e:
436
557
  return context.abort(grpc.StatusCode.INTERNAL, str(e))
437
558
 
@@ -513,8 +634,8 @@ def connect_obj_with_stub_async(
513
634
  resp_obj, output_item_type, pb2_module
514
635
  )
515
636
  except ValidationError as e:
516
- await context.abort(
517
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
637
+ await handle_validation_error_async(
638
+ e, method, context, None, is_grpc=True
518
639
  )
519
640
  except Exception as e:
520
641
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -534,8 +655,8 @@ def connect_obj_with_stub_async(
534
655
  resp_obj, output_item_type, pb2_module
535
656
  )
536
657
  except ValidationError as e:
537
- await context.abort(
538
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
658
+ await handle_validation_error_async(
659
+ e, method, context, None, is_grpc=True
539
660
  )
540
661
  except Exception as e:
541
662
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -559,8 +680,8 @@ def connect_obj_with_stub_async(
559
680
  resp_obj, response_type, pb2_module
560
681
  )
561
682
  except ValidationError as e:
562
- await context.abort(
563
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
683
+ await handle_validation_error_async(
684
+ e, method, context, None, is_grpc=True
564
685
  )
565
686
  except Exception as e:
566
687
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -580,8 +701,8 @@ def connect_obj_with_stub_async(
580
701
  resp_obj, response_type, pb2_module
581
702
  )
582
703
  except ValidationError as e:
583
- await context.abort(
584
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
704
+ await handle_validation_error_async(
705
+ e, method, context, None, is_grpc=True
585
706
  )
586
707
  except Exception as e:
587
708
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -611,8 +732,8 @@ def connect_obj_with_stub_async(
611
732
  resp_obj, output_item_type, pb2_module
612
733
  )
613
734
  except ValidationError as e:
614
- await context.abort(
615
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
735
+ await handle_validation_error_async(
736
+ e, method, context, request, is_grpc=True
616
737
  )
617
738
  except Exception as e:
618
739
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -632,8 +753,8 @@ def connect_obj_with_stub_async(
632
753
  resp_obj, output_item_type, pb2_module
633
754
  )
634
755
  except ValidationError as e:
635
- await context.abort(
636
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
756
+ await handle_validation_error_async(
757
+ e, method, context, request, is_grpc=True
637
758
  )
638
759
  except Exception as e:
639
760
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -674,8 +795,8 @@ def connect_obj_with_stub_async(
674
795
  resp_obj, response_type, pb2_module
675
796
  )
676
797
  except ValidationError as e:
677
- await context.abort(
678
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
798
+ await handle_validation_error_async(
799
+ e, method, context, request, is_grpc=True
679
800
  )
680
801
  except Exception as e:
681
802
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -709,8 +830,8 @@ def connect_obj_with_stub_async(
709
830
  resp_obj, response_type, pb2_module
710
831
  )
711
832
  except ValidationError as e:
712
- await context.abort(
713
- grpc.StatusCode.INVALID_ARGUMENT, str(e)
833
+ await handle_validation_error_async(
834
+ e, method, context, request, is_grpc=True
714
835
  )
715
836
  except Exception as e:
716
837
  await context.abort(grpc.StatusCode.INTERNAL, str(e))
@@ -769,9 +890,11 @@ def connect_obj_with_stub_connect_python(
769
890
  resp_obj, response_type, pb2_module
770
891
  )
771
892
  except ValidationError as e:
772
- return context.abort(Errors.INVALID_ARGUMENT, str(e))
893
+ return handle_validation_error_sync(
894
+ e, method, context, request, is_grpc=False
895
+ )
773
896
  except Exception as e:
774
- return context.abort(Errors.INTERNAL, str(e))
897
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
775
898
 
776
899
  return stub_method0
777
900
 
@@ -798,9 +921,11 @@ def connect_obj_with_stub_connect_python(
798
921
  resp_obj, response_type, pb2_module
799
922
  )
800
923
  except ValidationError as e:
801
- return context.abort(Errors.INVALID_ARGUMENT, str(e))
924
+ return handle_validation_error_sync(
925
+ e, method, context, request, is_grpc=False
926
+ )
802
927
  except Exception as e:
803
- return context.abort(Errors.INTERNAL, str(e))
928
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
804
929
 
805
930
  return stub_method1
806
931
 
@@ -827,9 +952,11 @@ def connect_obj_with_stub_connect_python(
827
952
  resp_obj, response_type, pb2_module
828
953
  )
829
954
  except ValidationError as e:
830
- return context.abort(Errors.INVALID_ARGUMENT, str(e))
955
+ return handle_validation_error_sync(
956
+ e, method, context, request, is_grpc=False
957
+ )
831
958
  except Exception as e:
832
- return context.abort(Errors.INTERNAL, str(e))
959
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
833
960
 
834
961
  return stub_method2
835
962
 
@@ -889,9 +1016,11 @@ def connect_obj_with_stub_async_connect_python(
889
1016
  resp_obj, response_type, pb2_module
890
1017
  )
891
1018
  except ValidationError as e:
892
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1019
+ await handle_validation_error_async(
1020
+ e, method, context, request, is_grpc=False
1021
+ )
893
1022
  except Exception as e:
894
- await context.abort(Errors.INTERNAL, str(e))
1023
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
895
1024
 
896
1025
  return stub_method0
897
1026
 
@@ -931,9 +1060,11 @@ def connect_obj_with_stub_async_connect_python(
931
1060
  resp_obj, output_item_type, pb2_module
932
1061
  )
933
1062
  except ValidationError as e:
934
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1063
+ await handle_validation_error_async(
1064
+ e, method, context, None, is_grpc=False
1065
+ )
935
1066
  except Exception as e:
936
- await context.abort(Errors.INTERNAL, str(e))
1067
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
937
1068
  else: # size_of_parameters == 2
938
1069
 
939
1070
  async def stub_method(
@@ -949,9 +1080,11 @@ def connect_obj_with_stub_async_connect_python(
949
1080
  resp_obj, output_item_type, pb2_module
950
1081
  )
951
1082
  except ValidationError as e:
952
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1083
+ await handle_validation_error_async(
1084
+ e, method, context, None, is_grpc=False
1085
+ )
953
1086
  except Exception as e:
954
- await context.abort(Errors.INTERNAL, str(e))
1087
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
955
1088
 
956
1089
  return stub_method
957
1090
  else:
@@ -973,9 +1106,11 @@ def connect_obj_with_stub_async_connect_python(
973
1106
  resp_obj, response_type, pb2_module
974
1107
  )
975
1108
  except ValidationError as e:
976
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1109
+ await handle_validation_error_async(
1110
+ e, method, context, None, is_grpc=False
1111
+ )
977
1112
  except Exception as e:
978
- await context.abort(Errors.INTERNAL, str(e))
1113
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
979
1114
  else: # size_of_parameters == 2
980
1115
 
981
1116
  async def stub_method(
@@ -993,9 +1128,11 @@ def connect_obj_with_stub_async_connect_python(
993
1128
  resp_obj, response_type, pb2_module
994
1129
  )
995
1130
  except ValidationError as e:
996
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1131
+ await handle_validation_error_async(
1132
+ e, method, context, None, is_grpc=False
1133
+ )
997
1134
  except Exception as e:
998
- await context.abort(Errors.INTERNAL, str(e))
1135
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
999
1136
 
1000
1137
  return stub_method
1001
1138
  else:
@@ -1024,9 +1161,11 @@ def connect_obj_with_stub_async_connect_python(
1024
1161
  resp_obj, output_item_type, pb2_module
1025
1162
  )
1026
1163
  except ValidationError as e:
1027
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1164
+ await handle_validation_error_async(
1165
+ e, method, context, request, is_grpc=False
1166
+ )
1028
1167
  except Exception as e:
1029
- await context.abort(Errors.INTERNAL, str(e))
1168
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
1030
1169
  else: # size_of_parameters == 2
1031
1170
 
1032
1171
  async def stub_method(
@@ -1045,9 +1184,11 @@ def connect_obj_with_stub_async_connect_python(
1045
1184
  resp_obj, output_item_type, pb2_module
1046
1185
  )
1047
1186
  except ValidationError as e:
1048
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1187
+ await handle_validation_error_async(
1188
+ e, method, context, request, is_grpc=False
1189
+ )
1049
1190
  except Exception as e:
1050
- await context.abort(Errors.INTERNAL, str(e))
1191
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
1051
1192
 
1052
1193
  return stub_method
1053
1194
  else:
@@ -1074,9 +1215,11 @@ def connect_obj_with_stub_async_connect_python(
1074
1215
  resp_obj, response_type, pb2_module
1075
1216
  )
1076
1217
  except ValidationError as e:
1077
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1218
+ await handle_validation_error_async(
1219
+ e, method, context, request, is_grpc=False
1220
+ )
1078
1221
  except Exception as e:
1079
- await context.abort(Errors.INTERNAL, str(e))
1222
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
1080
1223
  else: # size_of_parameters == 2
1081
1224
 
1082
1225
  async def stub_method(
@@ -1099,9 +1242,11 @@ def connect_obj_with_stub_async_connect_python(
1099
1242
  resp_obj, response_type, pb2_module
1100
1243
  )
1101
1244
  except ValidationError as e:
1102
- await context.abort(Errors.INVALID_ARGUMENT, str(e))
1245
+ await handle_validation_error_async(
1246
+ e, method, context, request, is_grpc=False
1247
+ )
1103
1248
  except Exception as e:
1104
- await context.abort(Errors.INTERNAL, str(e))
1249
+ raise ConnectError(code=Errors.INTERNAL, message=str(e))
1105
1250
 
1106
1251
  return stub_method
1107
1252
 
@@ -2611,11 +2756,20 @@ class Server:
2611
2756
  port: int = 50051,
2612
2757
  package_name: str = "",
2613
2758
  max_workers: int = 8,
2614
- *interceptors: Any,
2615
2759
  tls: Optional["GrpcTLSConfig"] = None,
2760
+ interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
2761
+ handlers: Optional[Sequence[grpc.GenericRpcHandler]] = None,
2762
+ options: Optional[Sequence[Tuple[str, Any]]] = None,
2763
+ maximum_concurrent_rpcs: Optional[int] = None,
2764
+ compression: Optional[grpc.Compression] = None,
2616
2765
  ) -> None:
2617
2766
  self._server: grpc.Server = grpc.server(
2618
- futures.ThreadPoolExecutor(max_workers), interceptors=interceptors
2767
+ futures.ThreadPoolExecutor(max_workers),
2768
+ handlers=handlers,
2769
+ interceptors=interceptors,
2770
+ options=options,
2771
+ maximum_concurrent_rpcs=maximum_concurrent_rpcs,
2772
+ compression=compression,
2619
2773
  )
2620
2774
  self._service_names: list[str] = []
2621
2775
  self._package_name: str = package_name
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
4
4
  from functools import wraps
5
+ import inspect
5
6
  import grpc
6
7
  from connectrpc.code import Code as ConnectErrors
7
8
 
@@ -145,7 +146,10 @@ def error_handler(
145
146
  exception_type: Type[Exception],
146
147
  status_code: Optional[grpc.StatusCode] = None,
147
148
  connect_code: Optional[ConnectErrors] = None,
148
- handler: Optional[Callable[[Exception], tuple[str, Any]]] = None,
149
+ handler: Optional[
150
+ Callable[[Exception], tuple[str, Any]]
151
+ | Callable[[Exception, Any], tuple[str, Any]]
152
+ ] = None,
149
153
  ) -> Callable[[F], F]:
150
154
  """
151
155
  Decorator to add automatic error handling to an RPC method.
@@ -154,13 +158,22 @@ def error_handler(
154
158
  exception_type: The type of exception to handle
155
159
  status_code: The gRPC status code to return (for gRPC services)
156
160
  connect_code: The Connect error code to return (for Connect services)
157
- handler: Optional custom handler function that returns (message, details)
161
+ handler: Optional custom handler function that returns (message, details).
162
+ Can accept either (exception) or (exception, request_data) as parameters.
158
163
 
159
164
  Example:
160
165
  @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
161
166
  @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
162
167
  async def get_user(self, request: GetUserRequest) -> User:
163
168
  ...
169
+
170
+ # With custom handler that accesses request data
171
+ def validation_handler(exc: ValidationError, request_data: Any) -> tuple[str, dict]:
172
+ return f"Validation failed for {request_data}", {"errors": exc.errors()}
173
+
174
+ @error_handler(ValidationError, handler=validation_handler)
175
+ async def create_user(self, request: CreateUserRequest) -> User:
176
+ ...
164
177
  """
165
178
 
166
179
  def decorator(func: F) -> F:
@@ -180,18 +193,11 @@ def error_handler(
180
193
  }
181
194
  )
182
195
 
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)
196
+ # Set the error handlers directly on the function
197
+ # No need for a wrapper - we're just storing metadata
198
+ setattr(func, ERROR_HANDLER_ATTR, handlers)
189
199
 
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
200
+ return func # type: ignore
195
201
 
196
202
  return decorator
197
203
 
@@ -207,3 +213,31 @@ def get_error_handlers(method: Callable) -> Optional[List[Dict[str, Any]]]:
207
213
  List of error handler configurations if present, None otherwise
208
214
  """
209
215
  return getattr(method, ERROR_HANDLER_ATTR, None)
216
+
217
+
218
+ def invoke_error_handler(
219
+ handler_func: Callable, exception: Exception, request_data: Any = None
220
+ ) -> tuple[str, Any]:
221
+ """
222
+ Invoke an error handler function with appropriate parameters based on its signature.
223
+
224
+ Args:
225
+ handler_func: The error handler function to invoke
226
+ exception: The exception that was raised
227
+ request_data: Optional request data (raw protobuf request)
228
+
229
+ Returns:
230
+ Tuple of (error_message, error_details)
231
+ """
232
+ sig = inspect.signature(handler_func)
233
+ param_count = len(sig.parameters)
234
+
235
+ if param_count == 1:
236
+ # Handler only accepts exception
237
+ return handler_func(exception)
238
+ elif param_count == 2:
239
+ # Handler accepts exception and request_data
240
+ return handler_func(exception, request_data)
241
+ else:
242
+ # Invalid signature, fall back to exception only
243
+ return handler_func(exception)