diff options
author | Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> | 2020-10-06 15:30:47 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-06 15:30:47 -0600 |
commit | a85533903c57be4809fe76435e298409e0903931 (patch) | |
tree | fd08ccaf4d4e366031582c5bf69314ea620c23f5 | |
parent | 000d0a04b6c539aa6a6aae0a487665341be484c4 (diff) | |
download | python-api-core-a85533903c57be4809fe76435e298409e0903931.tar.gz |
fix: map LRO errors to library exception types (#86)
Fixes #15 🦕
Errors raised by long running operations are currently always type GoogleAPICallError. Use the status code to create a more specific exception type.
-rw-r--r-- | google/api_core/exceptions.py | 15 | ||||
-rw-r--r-- | google/api_core/operation.py | 5 | ||||
-rw-r--r-- | tests/unit/test_exceptions.py | 11 | ||||
-rw-r--r-- | tests/unit/test_operation.py | 17 |
4 files changed, 45 insertions, 3 deletions
diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index b9c46ca..a29266d 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -26,6 +26,7 @@ from six.moves import http_client try: import grpc + except ImportError: # pragma: NO COVER grpc = None @@ -34,6 +35,14 @@ except ImportError: # pragma: NO COVER _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.""" @@ -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. @@ -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) diff --git a/google/api_core/operation.py b/google/api_core/operation.py index e6407b8..55adbdd 100644 --- a/google/api_core/operation.py +++ b/google/api_core/operation.py @@ -132,8 +132,9 @@ class Operation(polling.PollingFuture): ) 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, ) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 040ac8a..fb29015 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -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 diff --git a/tests/unit/test_operation.py b/tests/unit/test_operation.py index 14b95cb..829a3f3 100644 --- a/tests/unit/test_operation.py +++ b/tests/unit/test_operation.py @@ -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(), |