Skip to content
52 changes: 38 additions & 14 deletions google/api_core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,22 +476,31 @@ def from_http_status(status_code, message, **kwargs):
return error


def from_http_response(response):
"""Create a :class:`GoogleAPICallError` from a :class:`requests.Response`.
def _format_error_message(error, method, url):
method = method.upper()
message = "{method} {url}: {error}".format(
method=method,
url=url,
error=error,
)
return message


def format_http_response_error(response, method, url, payload=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: add type hints
nit: add code comments to clarify the reason that we're moving away from from_http_response

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed with a comment.

"""Create a :class:`GoogleAPICallError` from a google auth rest response.

Args:
response (requests.Response): The HTTP response.
response Union[google.auth.transport.Response, google.auth.aio.transport.Response]: The HTTP response.
method Optional(str): The HTTP request method.
url Optional(str): The HTTP request url.
payload Optional(str): The HTTP response payload. If not passed in, it is read from response for a response type of google.auth.transport.Response.

Returns:
GoogleAPICallError: An instance of the appropriate subclass of
:class:`GoogleAPICallError`, with the message and errors populated
from the response.
"""
try:
payload = response.json()
except ValueError:
payload = {"error": {"message": response.text or "unknown error"}}

payload = {} if not payload else payload
error_message = payload.get("error", {}).get("message", "unknown error")
errors = payload.get("error", {}).get("errors", ())
# In JSON, details are already formatted in developer-friendly way.
Expand All @@ -504,12 +513,7 @@ def from_http_response(response):
)
)
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,
)
message = _format_error_message(error_message, method, url)

exception = from_http_status(
response.status_code,
Expand All @@ -522,6 +526,26 @@ def from_http_response(response):
return exception


def from_http_response(response):
"""Create a :class:`GoogleAPICallError` from a :class:`requests.Response`.

Args:
response (requests.Response): The HTTP response.

Returns:
GoogleAPICallError: An instance of the appropriate subclass of
:class:`GoogleAPICallError`, with the message and errors populated
from the response.
"""
try:
payload = response.json()
except ValueError:
payload = {"error": {"message": response.text or "unknown error"}}
return format_http_response_error(
response, response.request.method, response.request.url, payload
)


def exception_class_for_grpc_status(status_code):
"""Return the exception class for a specific :class:`grpc.StatusCode`.

Expand Down
4 changes: 3 additions & 1 deletion google/api_core/gapic_v1/method_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def wrap_method(
default_timeout=None,
default_compression=None,
client_info=client_info.DEFAULT_CLIENT_INFO,
kind="grpc",
Copy link
Contributor

Choose a reason for hiding this comment

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

OK, and when we call wrap_method from the GAPIC, it will specify this parameter, right? So that will be in a follow-up PR in the generator repo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct! Note to myself to update the default value to grpc_asyncio.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Update the PR description with the right default value.

):
"""Wrap an async RPC method with common behavior.

Expand All @@ -40,7 +41,8 @@ def wrap_method(
and ``compression`` arguments and applies the common error mapping,
retry, timeout, metadata, and compression behavior to the low-level RPC method.
"""
func = grpc_helpers_async.wrap_errors(func)
if kind == "grpc":
func = grpc_helpers_async.wrap_errors(func)

metadata = [client_info.to_grpc_metadata()] if client_info is not None else None

Expand Down
11 changes: 11 additions & 0 deletions tests/asyncio/gapic/test_method_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,14 @@ async def test_wrap_method_with_overriding_timeout_as_a_number():

assert result == 42
method.assert_called_once_with(timeout=22, metadata=mock.ANY)


@pytest.mark.asyncio
async def test_wrap_method_without_wrap_errors():
fake_call = mock.AsyncMock()

wrapped_method = gapic_v1.method_async.wrap_method(fake_call, kind="rest")
with mock.patch("google.api_core.grpc_helpers_async.wrap_errors") as method:
await wrapped_method()

method.assert_not_called()