Skip to content

Commit

Permalink
feat: Add support for creating exceptions from an asynchronous respon…
Browse files Browse the repository at this point in the history
…se (#688)

* feat: add suport for mapping exceptions to rest callables

* avoid wrapping errors for rest callable

* fix lint issues

* add test coverage

* address PR comments

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* fix lint issues

* fix for none type method

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
ohmayr and gcf-owl-bot[bot] authored Sep 10, 2024
1 parent 082bce2 commit 1c4b0d0
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 16 deletions.
60 changes: 45 additions & 15 deletions google/api_core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from __future__ import unicode_literals

import http.client
from typing import Dict
from typing import Optional, Dict
from typing import Union
import warnings

Expand Down Expand Up @@ -476,22 +476,37 @@ 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_rest_error_message(error, method, url):
method = method.upper() if method else None
message = "{method} {url}: {error}".format(
method=method,
url=url,
error=error,
)
return message


# NOTE: We're moving away from `from_http_status` because it expects an aiohttp response compared
# to `format_http_response_error` which expects a more abstract response from google.auth and is
# compatible with both sync and async response types.
# TODO(https://github.com/googleapis/python-api-core/issues/691): Add type hint for response.
def format_http_response_error(
response, method: str, url: str, payload: Optional[Dict] = None
):
"""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(dict): 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 +519,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_rest_error_message(error_message, method, url)

exception = from_http_status(
response.status_code,
Expand All @@ -522,6 +532,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
6 changes: 5 additions & 1 deletion google/api_core/gapic_v1/method_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
from google.api_core.gapic_v1.method import DEFAULT # noqa: F401
from google.api_core.gapic_v1.method import USE_DEFAULT_METADATA # noqa: F401

_DEFAULT_ASYNC_TRANSPORT_KIND = "grpc_asyncio"


def wrap_method(
func,
default_retry=None,
default_timeout=None,
default_compression=None,
client_info=client_info.DEFAULT_CLIENT_INFO,
kind=_DEFAULT_ASYNC_TRANSPORT_KIND,
):
"""Wrap an async RPC method with common behavior.
Expand All @@ -40,7 +43,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 == _DEFAULT_ASYNC_TRANSPORT_KIND:
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()

0 comments on commit 1c4b0d0

Please sign in to comment.