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
diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py
index 35f2a6f..d4cb997 100644
--- a/google/api_core/exceptions.py
+++ b/google/api_core/exceptions.py
@@ -142,10 +142,21 @@
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 79d47f0..d474632 100644
--- a/google/api_core/extended_operation.py
+++ b/google/api_core/extended_operation.py
@@ -158,10 +158,16 @@
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 c551bfa..53af520 100644
--- a/tests/unit/test_extended_operation.py
+++ b/tests/unit/test_extended_operation.py
@@ -33,11 +33,23 @@
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 @@
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(