Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: map LRO errors to library exception types #86

Merged
merged 8 commits into from Oct 6, 2020
15 changes: 14 additions & 1 deletion google/api_core/exceptions.py
Expand Up @@ -26,6 +26,7 @@

try:
import grpc

except ImportError: # pragma: NO COVER
grpc = None

Expand All @@ -34,6 +35,14 @@
_HTTP_CODE_TO_EXCEPTION = {}
_GRPC_CODE_TO_EXCEPTION = {}

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


class GoogleAPIError(Exception):
"""Base class for all exceptions raised by Google API Clients."""
Expand Down Expand Up @@ -432,7 +441,7 @@ def from_grpc_status(status_code, message, **kwargs):
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.StatusCode`.

Args:
status_code (grpc.StatusCode): The gRPC status code.
status_code (Union[grpc.StatusCode, int]): The gRPC status code.
message (str): The exception message.
kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
constructor.
Expand All @@ -441,6 +450,10 @@ def from_grpc_status(status_code, message, **kwargs):
GoogleAPICallError: An instance of the appropriate subclass of
:class:`GoogleAPICallError`.
"""

if isinstance(status_code, int):
status_code = _INT_TO_GRPC_CODE.get(status_code, status_code)

error_class = exception_class_for_grpc_status(status_code)
error = error_class(message, **kwargs)

Expand Down
5 changes: 3 additions & 2 deletions google/api_core/operation.py
Expand Up @@ -132,8 +132,9 @@ def _set_result_from_operation(self):
)
self.set_result(response)
elif self._operation.HasField("error"):
exception = exceptions.GoogleAPICallError(
self._operation.error.message,
exception = exceptions.from_grpc_status(
status_code=self._operation.error.code,
message=self._operation.error.message,
errors=(self._operation.error,),
response=self._operation,
)
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/test_exceptions.py
Expand Up @@ -161,6 +161,17 @@ def test_from_grpc_status():
assert exception.errors == []


def test_from_grpc_status_as_int():
message = "message"
exception = exceptions.from_grpc_status(11, message)
assert isinstance(exception, exceptions.BadRequest)
assert isinstance(exception, exceptions.OutOfRange)
assert exception.code == http_client.BAD_REQUEST
assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
assert exception.message == message
assert exception.errors == []


def test_from_grpc_status_with_errors_and_response():
message = "message"
response = mock.sentinel.response
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/test_operation.py
Expand Up @@ -146,6 +146,23 @@ def test_exception():
assert expected_exception.message in "{!r}".format(exception)


def test_exception_with_error_code():
expected_exception = status_pb2.Status(message="meep", code=5)
responses = [
make_operation_proto(),
# Second operation response includes the error.
make_operation_proto(done=True, error=expected_exception),
]
future, _, _ = make_operation_future(responses)

exception = future.exception()

assert expected_exception.message in "{!r}".format(exception)
# Status Code 5 maps to Not Found
# https://developers.google.com/maps-booking/reference/grpc-api/status_codes
assert isinstance(exception, exceptions.NotFound)


def test_unexpected_result():
responses = [
make_operation_proto(),
Expand Down