From 7dfc3a7a439243f05238a11b68a31720fde1769e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 12 Jun 2023 12:00:17 -0400 Subject: [PATCH] fix: add actionable errors for GCE long running operations (#498) * fix: add actionable errors for GCE long running operations * add unit test * mypy * add notes that the workaround should be removed once proposal A from b/284179390 is implemented * fix typo * fix coverage --- google/api_core/exceptions.py | 15 ++++++++-- google/api_core/extended_operation.py | 6 ++++ tests/unit/test_extended_operation.py | 41 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index 35f2a6f8..d4cb9973 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -142,10 +142,21 @@ def __init__(self, message, errors=(), details=(), response=None, error_info=Non self._error_info = error_info def __str__(self): + error_msg = "{} {}".format(self.code, self.message) if self.details: - return "{} {} {}".format(self.code, self.message, self.details) + error_msg = "{} {}".format(error_msg, self.details) + # Note: This else condition can be removed once proposal A from + # b/284179390 is implemented. else: - return "{} {}".format(self.code, self.message) + if self.errors: + errors = [ + f"{error.code}: {error.message}" + for error in self.errors + if hasattr(error, "code") and hasattr(error, "message") + ] + if errors: + error_msg = "{} {}".format(error_msg, "\n".join(errors)) + return error_msg @property def reason(self): diff --git a/google/api_core/extended_operation.py b/google/api_core/extended_operation.py index 79d47f0d..d474632b 100644 --- a/google/api_core/extended_operation.py +++ b/google/api_core/extended_operation.py @@ -158,10 +158,16 @@ def _handle_refreshed_operation(self): return if self.error_code and self.error_message: + # Note: `errors` can be removed once proposal A from + # b/284179390 is implemented. + errors = [] + if hasattr(self, "error") and hasattr(self.error, "errors"): + errors = self.error.errors exception = exceptions.from_http_status( status_code=self.error_code, message=self.error_message, response=self._extended_operation, + errors=errors, ) self.set_exception(exception) elif self.error_code or self.error_message: diff --git a/tests/unit/test_extended_operation.py b/tests/unit/test_extended_operation.py index c551bfa8..53af5204 100644 --- a/tests/unit/test_extended_operation.py +++ b/tests/unit/test_extended_operation.py @@ -33,11 +33,23 @@ class StatusCode(enum.Enum): DONE = 1 PENDING = 2 + class LROCustomErrors: + class LROCustomError: + def __init__(self, code: str = "", message: str = ""): + self.code = code + self.message = message + + def __init__(self, errors: typing.List[LROCustomError] = []): + self.errors = errors + name: str status: StatusCode error_code: typing.Optional[int] = None error_message: typing.Optional[str] = None armor_class: typing.Optional[int] = None + # Note: `error` can be removed once proposal A from + # b/284179390 is implemented. + error: typing.Optional[LROCustomErrors] = None # Note: in generated clients, this property must be generated for each # extended operation message type. @@ -170,6 +182,35 @@ def test_error(): with pytest.raises(exceptions.BadRequest): ex_op.result() + # Test GCE custom LRO Error. See b/284179390 + # Note: This test case can be removed once proposal A from + # b/284179390 is implemented. + _EXCEPTION_CODE = "INCOMPATIBLE_BACKEND_SERVICES" + _EXCEPTION_MESSAGE = "Validation failed for instance group" + responses = [ + CustomOperation( + name=TEST_OPERATION_NAME, + status=CustomOperation.StatusCode.DONE, + error_code=400, + error_message="Bad request", + error=CustomOperation.LROCustomErrors( + errors=[ + CustomOperation.LROCustomErrors.LROCustomError( + code=_EXCEPTION_CODE, message=_EXCEPTION_MESSAGE + ) + ] + ), + ), + ] + + ex_op, _, _ = make_extended_operation(responses) + + # Defaults to CallError when grpc is not installed + with pytest.raises( + exceptions.BadRequest, match=f"{_EXCEPTION_CODE}: {_EXCEPTION_MESSAGE}" + ): + ex_op.result() + # Inconsistent result responses = [ CustomOperation(