55
66from __future__ import annotations
77
8+ import datetime
9+
810import grpc
11+ import grpc .aio
12+ from google .rpc import error_details_pb2 , status_pb2
913
1014
1115class DecreeError (Exception ):
1216 """Base exception for all OpenDecree SDK errors."""
1317
14- def __init__ (self , message : str , code : grpc .StatusCode | None = None ) -> None :
15- """Create a DecreeError.
16-
17- Args:
18- message: Human-readable error description.
19- code: The gRPC status code that caused this error, if any.
20- """
18+ def __init__ (
19+ self ,
20+ message : str ,
21+ code : grpc .StatusCode | None = None ,
22+ * ,
23+ trailing_metadata : grpc .aio .Metadata | None = None ,
24+ retry_after : datetime .timedelta | None = None ,
25+ ) -> None :
2126 super ().__init__ (message )
2227 self .code = code
28+ self .trailing_metadata = trailing_metadata
29+ self .retry_after = retry_after
2330
2431
2532class NotFoundError (DecreeError ):
@@ -58,6 +65,22 @@ class TypeMismatchError(DecreeError):
5865 """Raised when a typed getter receives a value of the wrong type."""
5966
6067
68+ class TimeoutError (DecreeError ):
69+ """Raised when the operation deadline was exceeded."""
70+
71+
72+ class ResourceExhaustedError (DecreeError ):
73+ """Raised when a resource quota or rate limit is exceeded."""
74+
75+
76+ class CancelledError (DecreeError ):
77+ """Raised when the operation was cancelled."""
78+
79+
80+ class UnimplementedError (DecreeError ):
81+ """Raised when the server has not implemented the operation."""
82+
83+
6184_STATUS_MAP : dict [grpc .StatusCode , type [DecreeError ]] = {
6285 grpc .StatusCode .NOT_FOUND : NotFoundError ,
6386 grpc .StatusCode .ALREADY_EXISTS : AlreadyExistsError ,
@@ -67,9 +90,27 @@ class TypeMismatchError(DecreeError):
6790 grpc .StatusCode .PERMISSION_DENIED : PermissionDeniedError ,
6891 grpc .StatusCode .UNAUTHENTICATED : PermissionDeniedError ,
6992 grpc .StatusCode .UNAVAILABLE : UnavailableError ,
93+ grpc .StatusCode .DEADLINE_EXCEEDED : TimeoutError ,
94+ grpc .StatusCode .RESOURCE_EXHAUSTED : ResourceExhaustedError ,
95+ grpc .StatusCode .CANCELLED : CancelledError ,
96+ grpc .StatusCode .UNIMPLEMENTED : UnimplementedError ,
7097}
7198
7299
100+ def _parse_retry_after (metadata : grpc .aio .Metadata ) -> datetime .timedelta | None :
101+ for key , value in metadata :
102+ if key != "grpc-status-details-bin" or not isinstance (value , bytes ):
103+ continue
104+ rpc_status = status_pb2 .Status ()
105+ rpc_status .ParseFromString (value )
106+ for detail in rpc_status .details :
107+ retry_info = error_details_pb2 .RetryInfo ()
108+ if detail .Unpack (retry_info ):
109+ d = retry_info .retry_delay
110+ return datetime .timedelta (seconds = d .seconds , microseconds = d .nanos // 1000 )
111+ return None
112+
113+
73114def map_grpc_error (err : grpc .RpcError ) -> DecreeError :
74115 """Convert a gRPC ``RpcError`` to a typed ``DecreeError``.
75116
@@ -78,5 +119,7 @@ def map_grpc_error(err: grpc.RpcError) -> DecreeError:
78119 """
79120 code = err .code ()
80121 details = err .details ()
122+ trailing : grpc .aio .Metadata | None = getattr (err , "trailing_metadata" , lambda : None )()
123+ retry_after = _parse_retry_after (trailing ) if trailing else None
81124 exc_class = _STATUS_MAP .get (code , DecreeError )
82- return exc_class (details or str (err ), code )
125+ return exc_class (details or str (err ), code , trailing_metadata = trailing , retry_after = retry_after )
0 commit comments