pydantic-rpc 0.10.0__tar.gz → 0.11.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.10.0
3
+ Version: 0.11.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
@@ -62,9 +62,10 @@ class OlympicsLocationAgent:
62
62
 
63
63
 
64
64
  if __name__ == "__main__":
65
- s = AsyncIOServer()
65
+ # New enhanced initialization API (optional - backward compatible)
66
+ s = AsyncIOServer(service=OlympicsLocationAgent(), port=50051)
66
67
  loop = asyncio.get_event_loop()
67
- loop.run_until_complete(s.run(OlympicsLocationAgent()))
68
+ loop.run_until_complete(s.run())
68
69
  ```
69
70
 
70
71
  And here is an example of a simple Connect RPC service that exposes the same agent as an ASGI application:
@@ -106,8 +107,8 @@ class OlympicsLocationAgent:
106
107
  result = await self._agent.run(req.prompt())
107
108
  return result.data
108
109
 
109
- app = ASGIApp()
110
- app.mount(OlympicsLocationAgent())
110
+ # New enhanced initialization API (optional - backward compatible)
111
+ app = ASGIApp(service=OlympicsLocationAgent())
111
112
 
112
113
  ```
113
114
 
@@ -129,6 +130,17 @@ app.mount(OlympicsLocationAgent())
129
130
  - 🛠️ **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
131
  - 🤖 **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
132
 
133
+ ## ⚠️ Important Notes for Connect-RPC
134
+
135
+ When using Connect-RPC with ASGIApp:
136
+
137
+ - **Endpoint Path Format**: Connect-RPC endpoints use CamelCase method names in the path: `/<package>.<service>/<Method>` (e.g., `/chat.v1.ChatService/SendMessage`)
138
+ - **Content-Type**: Set `Content-Type: application/json` or `application/connect+json` for requests
139
+ - **HTTP/2 Requirement**: Bidirectional streaming requires HTTP/2. Use Hypercorn instead of uvicorn for HTTP/2 support
140
+ - **Testing**: Use [buf curl](https://buf.build/docs/ecosystem/cli/curl) for testing Connect-RPC endpoints with proper streaming support
141
+
142
+ For detailed examples and testing instructions, see the [examples directory](examples/).
143
+
132
144
  ## 📦 Installation
133
145
 
134
146
  Install PydanticRPC via pip:
@@ -137,6 +149,54 @@ Install PydanticRPC via pip:
137
149
  pip install pydantic-rpc
138
150
  ```
139
151
 
152
+ For CLI support with built-in server runners:
153
+
154
+ ```bash
155
+ pip install pydantic-rpc-cli # Includes hypercorn and gunicorn
156
+ ```
157
+
158
+ ## 🆕 Enhanced Features (v0.10.0+)
159
+
160
+ **Note: All new features are fully backward compatible. Existing code continues to work without modification.**
161
+
162
+ ### Enhanced Initialization API
163
+ All server classes now support optional initialization with services:
164
+
165
+ ```python
166
+ # Traditional API (still works)
167
+ server = AsyncIOServer()
168
+ server.set_port(50051)
169
+ await server.run(MyService())
170
+
171
+ # New enhanced API (optional)
172
+ server = AsyncIOServer(
173
+ service=MyService(),
174
+ port=50051,
175
+ package_name="my.package"
176
+ )
177
+ await server.run()
178
+
179
+ # Same for ASGI/WSGI apps
180
+ app = ASGIApp(service=MyService(), package_name="my.package")
181
+ ```
182
+
183
+ ### Error Handling with Decorators
184
+ Automatically map exceptions to gRPC/Connect status codes:
185
+
186
+ ```python
187
+ from pydantic_rpc import error_handler
188
+ import grpc
189
+
190
+ class MyService:
191
+ @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
192
+ @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
193
+ async def get_user(self, request: GetUserRequest) -> User:
194
+ # Exceptions are automatically converted to proper status codes
195
+ if request.id not in users_db:
196
+ raise KeyError(f"User {request.id} not found")
197
+ return users_db[request.id]
198
+ ```
199
+
140
200
  ## 🚀 Getting Started
141
201
 
142
202
  PydanticRPC supports two main protocols:
@@ -1090,15 +1150,40 @@ MCP endpoints will be available at:
1090
1150
  - SSE: `GET http://localhost:8000/mcp/sse`
1091
1151
  - Messages: `POST http://localhost:8000/mcp/messages/`
1092
1152
 
1093
- ### 🗄️ Protobuf file and code (Python files) generation using CLI
1153
+ ### 🗄️ CLI Tool (pydantic-rpc-cli)
1154
+
1155
+ The CLI tool provides powerful features for generating protobuf files and running servers. Install it separately:
1156
+
1157
+ ```bash
1158
+ pip install pydantic-rpc-cli
1159
+ ```
1160
+
1161
+ #### Generate Protobuf Files
1094
1162
 
1095
- You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
1163
+ ```bash
1164
+ # Generate .proto file from a service class
1165
+ pydantic-rpc generate myapp.services.UserService --output ./proto/
1166
+
1167
+ # Also compile to Python code
1168
+ pydantic-rpc generate myapp.services.UserService --compile
1169
+ ```
1170
+
1171
+ #### Run Servers Directly
1172
+
1173
+ The CLI can run any type of server:
1096
1174
 
1097
1175
  ```bash
1098
- pydantic-rpc a_module.py aClassName
1176
+ # Run as gRPC server (auto-detects async/sync)
1177
+ pydantic-rpc serve myapp.services.UserService --port 50051
1178
+
1179
+ # Run as Connect-RPC with ASGI (HTTP/2, uses Hypercorn)
1180
+ pydantic-rpc serve myapp.services.UserService --asgi --port 8000
1181
+
1182
+ # Run as Connect-RPC with WSGI (HTTP/1.1, uses Gunicorn)
1183
+ pydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4
1099
1184
  ```
1100
1185
 
1101
- Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you could generate code for any desired language other than Python.
1186
+ Using the generated proto files with tools like `protoc`, `buf` and `BSR`, you can generate code for any desired language.
1102
1187
 
1103
1188
 
1104
1189
  ## 📖 Data Type Mapping
@@ -45,9 +45,10 @@ class OlympicsLocationAgent:
45
45
 
46
46
 
47
47
  if __name__ == "__main__":
48
- s = AsyncIOServer()
48
+ # New enhanced initialization API (optional - backward compatible)
49
+ s = AsyncIOServer(service=OlympicsLocationAgent(), port=50051)
49
50
  loop = asyncio.get_event_loop()
50
- loop.run_until_complete(s.run(OlympicsLocationAgent()))
51
+ loop.run_until_complete(s.run())
51
52
  ```
52
53
 
53
54
  And here is an example of a simple Connect RPC service that exposes the same agent as an ASGI application:
@@ -89,8 +90,8 @@ class OlympicsLocationAgent:
89
90
  result = await self._agent.run(req.prompt())
90
91
  return result.data
91
92
 
92
- app = ASGIApp()
93
- app.mount(OlympicsLocationAgent())
93
+ # New enhanced initialization API (optional - backward compatible)
94
+ app = ASGIApp(service=OlympicsLocationAgent())
94
95
 
95
96
  ```
96
97
 
@@ -112,6 +113,17 @@ app.mount(OlympicsLocationAgent())
112
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.
113
114
  - 🤖 **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.
114
115
 
116
+ ## ⚠️ Important Notes for Connect-RPC
117
+
118
+ When using Connect-RPC with ASGIApp:
119
+
120
+ - **Endpoint Path Format**: Connect-RPC endpoints use CamelCase method names in the path: `/<package>.<service>/<Method>` (e.g., `/chat.v1.ChatService/SendMessage`)
121
+ - **Content-Type**: Set `Content-Type: application/json` or `application/connect+json` for requests
122
+ - **HTTP/2 Requirement**: Bidirectional streaming requires HTTP/2. Use Hypercorn instead of uvicorn for HTTP/2 support
123
+ - **Testing**: Use [buf curl](https://buf.build/docs/ecosystem/cli/curl) for testing Connect-RPC endpoints with proper streaming support
124
+
125
+ For detailed examples and testing instructions, see the [examples directory](examples/).
126
+
115
127
  ## 📦 Installation
116
128
 
117
129
  Install PydanticRPC via pip:
@@ -120,6 +132,54 @@ Install PydanticRPC via pip:
120
132
  pip install pydantic-rpc
121
133
  ```
122
134
 
135
+ For CLI support with built-in server runners:
136
+
137
+ ```bash
138
+ pip install pydantic-rpc-cli # Includes hypercorn and gunicorn
139
+ ```
140
+
141
+ ## 🆕 Enhanced Features (v0.10.0+)
142
+
143
+ **Note: All new features are fully backward compatible. Existing code continues to work without modification.**
144
+
145
+ ### Enhanced Initialization API
146
+ All server classes now support optional initialization with services:
147
+
148
+ ```python
149
+ # Traditional API (still works)
150
+ server = AsyncIOServer()
151
+ server.set_port(50051)
152
+ await server.run(MyService())
153
+
154
+ # New enhanced API (optional)
155
+ server = AsyncIOServer(
156
+ service=MyService(),
157
+ port=50051,
158
+ package_name="my.package"
159
+ )
160
+ await server.run()
161
+
162
+ # Same for ASGI/WSGI apps
163
+ app = ASGIApp(service=MyService(), package_name="my.package")
164
+ ```
165
+
166
+ ### Error Handling with Decorators
167
+ Automatically map exceptions to gRPC/Connect status codes:
168
+
169
+ ```python
170
+ from pydantic_rpc import error_handler
171
+ import grpc
172
+
173
+ class MyService:
174
+ @error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
175
+ @error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
176
+ async def get_user(self, request: GetUserRequest) -> User:
177
+ # Exceptions are automatically converted to proper status codes
178
+ if request.id not in users_db:
179
+ raise KeyError(f"User {request.id} not found")
180
+ return users_db[request.id]
181
+ ```
182
+
123
183
  ## 🚀 Getting Started
124
184
 
125
185
  PydanticRPC supports two main protocols:
@@ -1073,15 +1133,40 @@ MCP endpoints will be available at:
1073
1133
  - SSE: `GET http://localhost:8000/mcp/sse`
1074
1134
  - Messages: `POST http://localhost:8000/mcp/messages/`
1075
1135
 
1076
- ### 🗄️ Protobuf file and code (Python files) generation using CLI
1136
+ ### 🗄️ CLI Tool (pydantic-rpc-cli)
1137
+
1138
+ The CLI tool provides powerful features for generating protobuf files and running servers. Install it separately:
1139
+
1140
+ ```bash
1141
+ pip install pydantic-rpc-cli
1142
+ ```
1143
+
1144
+ #### Generate Protobuf Files
1077
1145
 
1078
- You can genereate protobuf files and code for a given module and a specified class using `pydantic-rpc` CLI command:
1146
+ ```bash
1147
+ # Generate .proto file from a service class
1148
+ pydantic-rpc generate myapp.services.UserService --output ./proto/
1149
+
1150
+ # Also compile to Python code
1151
+ pydantic-rpc generate myapp.services.UserService --compile
1152
+ ```
1153
+
1154
+ #### Run Servers Directly
1155
+
1156
+ The CLI can run any type of server:
1079
1157
 
1080
1158
  ```bash
1081
- pydantic-rpc a_module.py aClassName
1159
+ # Run as gRPC server (auto-detects async/sync)
1160
+ pydantic-rpc serve myapp.services.UserService --port 50051
1161
+
1162
+ # Run as Connect-RPC with ASGI (HTTP/2, uses Hypercorn)
1163
+ pydantic-rpc serve myapp.services.UserService --asgi --port 8000
1164
+
1165
+ # Run as Connect-RPC with WSGI (HTTP/1.1, uses Gunicorn)
1166
+ pydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4
1082
1167
  ```
1083
1168
 
1084
- Using this generated proto file and tools as `protoc`, `buf` and `BSR`, you could generate code for any desired language other than Python.
1169
+ Using the generated proto files with tools like `protoc`, `buf` and `BSR`, you can generate code for any desired language.
1085
1170
 
1086
1171
 
1087
1172
  ## 📖 Data Type Mapping
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-rpc"
3
- version = "0.10.0"
3
+ version = "0.11.0"
4
4
  description = "A Python library for building gRPC/ConnectRPC services with Pydantic models."
5
5
  authors = [
6
6
  { name = "Yasushi Itoh" }
@@ -19,8 +19,6 @@ dependencies = [
19
19
  readme = "README.md"
20
20
  requires-python = ">= 3.11"
21
21
 
22
- [project.scripts]
23
- pydantic-rpc = "pydantic_rpc.core:main"
24
22
 
25
23
  [build-system]
26
24
  requires = ["uv_build>=0.7.21,<0.8.0"]
@@ -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",
@@ -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
 
@@ -2802,10 +2820,11 @@ class ASGIApp:
2802
2820
  An ASGI-compatible application that can serve Connect-RPC via Connecpy.
2803
2821
  """
2804
2822
 
2805
- def __init__(self):
2823
+ def __init__(self, service: Optional[object] = None, package_name: str = ""):
2806
2824
  self._services: list[tuple[Any, str]] = [] # List of (app, path) tuples
2807
2825
  self._service_names: list[str] = []
2808
- self._package_name: str = ""
2826
+ self._package_name: str = package_name
2827
+ self._initial_service = service
2809
2828
 
2810
2829
  def mount(self, obj: object, package_name: str = ""):
2811
2830
  """Generate and compile proto files, then mount the async service implementation."""
@@ -2838,6 +2857,11 @@ class ASGIApp:
2838
2857
 
2839
2858
  def mount_objs(self, *objs: object):
2840
2859
  """Mount multiple service objects into this ASGI app."""
2860
+ # Mount initial service if provided
2861
+ if self._initial_service:
2862
+ self.mount(self._initial_service, self._package_name)
2863
+
2864
+ # Mount additional services
2841
2865
  for obj in objs:
2842
2866
  self.mount(obj, self._package_name)
2843
2867
 
@@ -2848,6 +2872,10 @@ class ASGIApp:
2848
2872
  send: Callable[[dict[str, Any]], Any],
2849
2873
  ):
2850
2874
  """ASGI entry point with routing for multiple services."""
2875
+ # Mount initial service on first call if not already mounted
2876
+ if self._initial_service and not self._services:
2877
+ self.mount(self._initial_service, self._package_name)
2878
+
2851
2879
  if scope["type"] != "http":
2852
2880
  await send({"type": "http.response.start", "status": 404})
2853
2881
  await send({"type": "http.response.body", "body": b"Not Found"})
@@ -2874,10 +2902,11 @@ class WSGIApp:
2874
2902
  A WSGI-compatible application that can serve Connect-RPC via Connecpy.
2875
2903
  """
2876
2904
 
2877
- def __init__(self):
2905
+ def __init__(self, service: Optional[object] = None, package_name: str = ""):
2878
2906
  self._services: list[tuple[Any, str]] = [] # List of (app, path) tuples
2879
2907
  self._service_names: list[str] = []
2880
- self._package_name: str = ""
2908
+ self._package_name: str = package_name
2909
+ self._initial_service = service
2881
2910
 
2882
2911
  def mount(self, obj: object, package_name: str = ""):
2883
2912
  """Generate and compile proto files, then mount the sync service implementation."""
@@ -2910,6 +2939,11 @@ class WSGIApp:
2910
2939
 
2911
2940
  def mount_objs(self, *objs: object):
2912
2941
  """Mount multiple service objects into this WSGI app."""
2942
+ # Mount initial service if provided
2943
+ if self._initial_service:
2944
+ self.mount(self._initial_service, self._package_name)
2945
+
2946
+ # Mount additional services
2913
2947
  for obj in objs:
2914
2948
  self.mount(obj, self._package_name)
2915
2949
 
@@ -2921,6 +2955,10 @@ class WSGIApp:
2921
2955
  ],
2922
2956
  ) -> Iterable[bytes]:
2923
2957
  """WSGI entry point with routing for multiple services."""
2958
+ # Mount initial service on first call if not already mounted
2959
+ if self._initial_service and not self._services:
2960
+ self.mount(self._initial_service, self._package_name)
2961
+
2924
2962
  path = environ.get("PATH_INFO", "")
2925
2963
 
2926
2964
  # 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 connecpy.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)