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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for 'error_info' #307

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions google/api_core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details.
response (Union[requests.Request, grpc.Call]): The response or
gRPC call metadata.
error_info (Union[error_details_pb2.ErrorInfo, None]): An optional object containing error info
(google.rpc.error_details.ErrorInfo)
"""

code: Union[int, None] = None
Expand All @@ -122,12 +124,13 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
This may be ``None`` if the exception does not match up to a gRPC error.
"""

def __init__(self, message, errors=(), details=(), response=None):
def __init__(self, message, errors=(), details=(), response=None, error_info=None):
super(GoogleAPICallError, self).__init__(message)
self.message = message
"""str: The exception message."""
self._errors = errors
self._details = details
self._error_info = error_info
tseaver marked this conversation as resolved.
Show resolved Hide resolved
self._response = response

def __str__(self):
Expand All @@ -145,6 +148,17 @@ def errors(self):
"""
return list(self._errors)

@property
def error_info(self):
"""Information contained in google.rpc.error_details.ErrorInfo

Reference:
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
tseaver marked this conversation as resolved.
Show resolved Hide resolved
Returns:
Union[error_details_pb2.ErrorInfo, None]: An optional object containing google.rpc.error_details.ErrorInfo.
"""
return self._error_info

@property
def details(self):
"""Information contained in google.rpc.status.details.
Expand Down Expand Up @@ -433,13 +447,25 @@ def from_http_response(response):
errors = payload.get("error", {}).get("errors", ())
# In JSON, details are already formatted in developer-friendly way.
details = payload.get("error", {}).get("details", ())

error_info = list(
filter(
lambda detail: detail.get("@type", "")
== "type.googleapis.com/google.rpc.ErrorInfo",
details,
)
)
Comment on lines +481 to +487
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's more idiomatic to use list comprehensions than list and filter.

error_info_type = "type.googleapis.com/google.rpc.ErrorInfo"
error_info = [d for d in details if d.get("@type", "") == error_info_type]

error_info = error_info[0] if error_info else None
message = "{method} {url}: {error}".format(
method=response.request.method, url=response.request.url, error=error_message
)

exception = from_http_status(
response.status_code, message, errors=errors, details=details, response=response
response.status_code,
message,
errors=errors,
details=details,
response=response,
error_info=error_info,
)
return exception

Expand Down Expand Up @@ -490,10 +516,10 @@ def _parse_grpc_error_details(rpc_exc):
try:
status = rpc_status.from_call(rpc_exc)
except NotImplementedError: # workaround
return []
return [], None

if not status:
return []
return [], None

possible_errors = [
error_details_pb2.BadRequest,
Expand All @@ -507,6 +533,7 @@ def _parse_grpc_error_details(rpc_exc):
error_details_pb2.Help,
error_details_pb2.LocalizedMessage,
]
error_info = None
error_details = []
for detail in status.details:
matched_detail_cls = list(
Expand All @@ -519,7 +546,9 @@ def _parse_grpc_error_details(rpc_exc):
info = matched_detail_cls[0]()
detail.Unpack(info)
error_details.append(info)
return error_details
if isinstance(info, error_details_pb2.ErrorInfo):
error_info = info
return error_details, error_info


def from_grpc_error(rpc_exc):
Expand All @@ -535,12 +564,14 @@ def from_grpc_error(rpc_exc):
# NOTE(lidiz) All gRPC error shares the parent class grpc.RpcError.
# However, check for grpc.RpcError breaks backward compatibility.
if isinstance(rpc_exc, grpc.Call) or _is_informative_grpc_error(rpc_exc):
details, err_info = _parse_grpc_error_details(rpc_exc)
return from_grpc_status(
rpc_exc.code(),
rpc_exc.details(),
errors=(rpc_exc,),
details=_parse_grpc_error_details(rpc_exc),
details=details,
response=rpc_exc,
error_info=err_info,
)
else:
return GoogleAPICallError(str(rpc_exc), errors=(rpc_exc,), response=rpc_exc)
49 changes: 40 additions & 9 deletions tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,31 +275,56 @@ def create_bad_request_details():
return status_detail


def create_error_info_details():
info = error_details_pb2.ErrorInfo(
reason="SERVICE_DISABLED",
domain="googleapis.com",
metadata={
"consumer": "projects/455411330361",
"service": "translate.googleapis.com",
},
)
tseaver marked this conversation as resolved.
Show resolved Hide resolved
status_detail = any_pb2.Any()
status_detail.Pack(info)
return status_detail


def test_error_details_from_rest_response():
bad_request_detail = create_bad_request_details()
error_info_detail = create_error_info_details()
status = status_pb2.Status()
status.code = 3
status.message = (
"3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set."
)
status.details.append(bad_request_detail)
status.details.append(error_info_detail)

# See JSON schema in https://cloud.google.com/apis/design/errors#http_mapping
http_response = make_response(
json.dumps({"error": json.loads(json_format.MessageToJson(status))}).encode(
json.dumps({"error": json.loads(json_format.MessageToJson(status, sort_keys=True))}).encode(
"utf-8"
)
)
exception = exceptions.from_http_response(http_response)
want_error_details = [json.loads(json_format.MessageToJson(bad_request_detail))]
want_error_details = [
json.loads(json_format.MessageToJson(bad_request_detail)),
json.loads(json_format.MessageToJson(error_info_detail)),
]
assert want_error_details == exception.details

# 404 POST comes from make_response.
assert str(exception) == (
"404 POST https://example.com/: 3 INVALID_ARGUMENT:"
" One of content, or gcs_content_uri must be set."
" [{'@type': 'type.googleapis.com/google.rpc.BadRequest',"
" 'fieldViolations': [{'field': 'document.content',"
" 'description': 'Must have some text content to annotate.'}]}]"
" 'fieldViolations': [{'description': 'Must have some text content to annotate.',"
" 'field': 'document.content'}]},"
" {'@type': 'type.googleapis.com/google.rpc.ErrorInfo',"
" 'domain': 'googleapis.com',"
" 'metadata': {'consumer': 'projects/455411330361',"
" 'service': 'translate.googleapis.com'},"
" 'reason': 'SERVICE_DISABLED'}]"
)


Expand All @@ -311,6 +336,7 @@ def test_error_details_from_v1_rest_response():
)
exception = exceptions.from_http_response(response)
assert exception.details == []
assert exception.error_info is None


@pytest.mark.skipif(grpc is None, reason="gRPC not importable")
Expand All @@ -320,9 +346,10 @@ def test_error_details_from_grpc_response():
status.message = (
"3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set."
)
status_detail = create_bad_request_details()
status.details.append(status_detail)

status_br_detail = create_bad_request_details()
status_ei_detail = create_error_info_details()
status.details.append(status_br_detail)
status.details.append(status_ei_detail)
# Actualy error doesn't matter as long as its grpc.Call,
# because from_call is mocked.
error = mock.create_autospec(grpc.Call, instance=True)
Expand All @@ -331,8 +358,11 @@ def test_error_details_from_grpc_response():
exception = exceptions.from_grpc_error(error)

bad_request_detail = error_details_pb2.BadRequest()
status_detail.Unpack(bad_request_detail)
assert exception.details == [bad_request_detail]
error_info_detail = error_details_pb2.ErrorInfo()
status_br_detail.Unpack(bad_request_detail)
status_ei_detail.Unpack(error_info_detail)
assert exception.details == [bad_request_detail, error_info_detail]
assert exception.error_info == error_info_detail


@pytest.mark.skipif(grpc is None, reason="gRPC not importable")
Expand All @@ -351,3 +381,4 @@ def test_error_details_from_grpc_response_unknown_error():
m.return_value = status
exception = exceptions.from_grpc_error(error)
assert exception.details == [status_detail]
assert exception.error_info is None