11import logging
22
3- from collections .abc import AsyncGenerator , Callable , Iterable
3+ from collections .abc import AsyncGenerator , Callable
44from functools import wraps
55from typing import Any , NoReturn , cast
66
77from a2a .client .errors import A2AClientError , A2AClientTimeoutError
88from a2a .client .middleware import ClientCallContext
9- from a2a .utils .errors import JSON_RPC_ERROR_CODE_MAP , A2AError
109
1110
1211try :
1312 import grpc # type: ignore[reportMissingModuleSource]
13+
14+ from grpc_status import rpc_status
1415except ImportError as e :
1516 raise ImportError (
16- 'A2AGrpcClient requires grpcio and grpcio-tools to be installed. '
17+ 'A2AGrpcClient requires grpcio, grpcio-tools, and grpcio-status to be installed. '
1718 'Install with: '
1819 "'pip install a2a-sdk[grpc]'"
1920 ) from e
2021
2122
2223from google .rpc import ( # type: ignore[reportMissingModuleSource]
2324 error_details_pb2 ,
24- status_pb2 ,
2525)
2626
2727from a2a .client .client import ClientConfig
5555
5656logger = logging .getLogger (__name__ )
5757
58- _A2A_ERROR_NAME_TO_CLS = {
59- error_type .__name__ : error_type for error_type in JSON_RPC_ERROR_CODE_MAP
60- }
6158
59+ def _map_grpc_error (e : grpc .aio .AioRpcError ) -> NoReturn :
60+
61+ if e .code () == grpc .StatusCode .DEADLINE_EXCEEDED :
62+ raise A2AClientTimeoutError ('Client Request timed out' ) from e
63+
64+ # 1. Use grpc_status to cleanly extract the rich Status from the call
65+ status = rpc_status .from_call (cast ('grpc.Call' , e ))
6266
63- def _parse_rich_grpc_error (
64- value : bytes , original_error : grpc .aio .AioRpcError
65- ) -> None :
66- try :
67- status = status_pb2 .Status .FromString (value )
67+ if status is not None :
6868 for detail in status .details :
6969 if detail .Is (error_details_pb2 .ErrorInfo .DESCRIPTOR ):
7070 error_info = error_details_pb2 .ErrorInfo ()
@@ -73,34 +73,8 @@ def _parse_rich_grpc_error(
7373 if error_info .domain == 'a2a-protocol.org' :
7474 exception_cls = A2A_REASON_TO_ERROR .get (error_info .reason )
7575 if exception_cls :
76- raise exception_cls (status .message ) from original_error # noqa: TRY301
77- except Exception as parse_e :
78- # Don't swallow A2A errors generated above
79- if isinstance (parse_e , (A2AError , A2AClientError )):
80- raise parse_e
81- logger .warning (
82- 'Failed to parse grpc-status-details-bin' , exc_info = parse_e
83- )
84-
85-
86- def _map_grpc_error (e : grpc .aio .AioRpcError ) -> NoReturn :
87- if e .code () == grpc .StatusCode .DEADLINE_EXCEEDED :
88- raise A2AClientTimeoutError ('Client Request timed out' ) from e
76+ raise exception_cls (status .message ) from e
8977
90- metadata = e .trailing_metadata ()
91- if metadata :
92- iterable_metadata = cast ('Iterable[tuple[str, str | bytes]]' , metadata )
93- for key , value in iterable_metadata :
94- if key == 'grpc-status-details-bin' and isinstance (value , bytes ):
95- _parse_rich_grpc_error (value , e )
96-
97- details = e .details ()
98- if isinstance (details , str ) and ': ' in details :
99- error_type_name , error_message = details .split (': ' , 1 )
100- # Leaving as fallback for errors that don't use the rich error details.
101- exception_cls = _A2A_ERROR_NAME_TO_CLS .get (error_type_name )
102- if exception_cls :
103- raise exception_cls (error_message ) from e
10478 raise A2AClientError (f'gRPC Error { e .code ().name } : { e .details ()} ' ) from e
10579
10680
0 commit comments