Skip to content

Commit a855339

Browse files
authored
fix: map LRO errors to library exception types (googleapis#86)
Fixes googleapis#15 🦕 Errors raised by long running operations are currently always type GoogleAPICallError. Use the status code to create a more specific exception type.
1 parent 000d0a0 commit a855339

File tree

4 files changed

+45
-3
lines changed

4 files changed

+45
-3
lines changed

google/api_core/exceptions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
try:
2828
import grpc
29+
2930
except ImportError: # pragma: NO COVER
3031
grpc = None
3132

@@ -34,6 +35,14 @@
3435
_HTTP_CODE_TO_EXCEPTION = {}
3536
_GRPC_CODE_TO_EXCEPTION = {}
3637

38+
# Additional lookup table to map integer status codes to grpc status code
39+
# grpc does not currently support initializing enums from ints
40+
# i.e., grpc.StatusCode(5) raises an error
41+
_INT_TO_GRPC_CODE = {}
42+
if grpc is not None: # pragma: no branch
43+
for x in grpc.StatusCode:
44+
_INT_TO_GRPC_CODE[x.value[0]] = x
45+
3746

3847
class GoogleAPIError(Exception):
3948
"""Base class for all exceptions raised by Google API Clients."""
@@ -432,7 +441,7 @@ def from_grpc_status(status_code, message, **kwargs):
432441
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.StatusCode`.
433442
434443
Args:
435-
status_code (grpc.StatusCode): The gRPC status code.
444+
status_code (Union[grpc.StatusCode, int]): The gRPC status code.
436445
message (str): The exception message.
437446
kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
438447
constructor.
@@ -441,6 +450,10 @@ def from_grpc_status(status_code, message, **kwargs):
441450
GoogleAPICallError: An instance of the appropriate subclass of
442451
:class:`GoogleAPICallError`.
443452
"""
453+
454+
if isinstance(status_code, int):
455+
status_code = _INT_TO_GRPC_CODE.get(status_code, status_code)
456+
444457
error_class = exception_class_for_grpc_status(status_code)
445458
error = error_class(message, **kwargs)
446459

google/api_core/operation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ def _set_result_from_operation(self):
132132
)
133133
self.set_result(response)
134134
elif self._operation.HasField("error"):
135-
exception = exceptions.GoogleAPICallError(
136-
self._operation.error.message,
135+
exception = exceptions.from_grpc_status(
136+
status_code=self._operation.error.code,
137+
message=self._operation.error.message,
137138
errors=(self._operation.error,),
138139
response=self._operation,
139140
)

tests/unit/test_exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,17 @@ def test_from_grpc_status():
161161
assert exception.errors == []
162162

163163

164+
def test_from_grpc_status_as_int():
165+
message = "message"
166+
exception = exceptions.from_grpc_status(11, message)
167+
assert isinstance(exception, exceptions.BadRequest)
168+
assert isinstance(exception, exceptions.OutOfRange)
169+
assert exception.code == http_client.BAD_REQUEST
170+
assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
171+
assert exception.message == message
172+
assert exception.errors == []
173+
174+
164175
def test_from_grpc_status_with_errors_and_response():
165176
message = "message"
166177
response = mock.sentinel.response

tests/unit/test_operation.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ def test_exception():
146146
assert expected_exception.message in "{!r}".format(exception)
147147

148148

149+
def test_exception_with_error_code():
150+
expected_exception = status_pb2.Status(message="meep", code=5)
151+
responses = [
152+
make_operation_proto(),
153+
# Second operation response includes the error.
154+
make_operation_proto(done=True, error=expected_exception),
155+
]
156+
future, _, _ = make_operation_future(responses)
157+
158+
exception = future.exception()
159+
160+
assert expected_exception.message in "{!r}".format(exception)
161+
# Status Code 5 maps to Not Found
162+
# https://developers.google.com/maps-booking/reference/grpc-api/status_codes
163+
assert isinstance(exception, exceptions.NotFound)
164+
165+
149166
def test_unexpected_result():
150167
responses = [
151168
make_operation_proto(),

0 commit comments

Comments
 (0)