Skip to content

Commit

Permalink
feat: implement request class for asynchoronous AuthorizedSession API
Browse files Browse the repository at this point in the history
  • Loading branch information
ohmayr committed Aug 11, 2024
1 parent 7224159 commit 17e2e25
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 2 deletions.
48 changes: 47 additions & 1 deletion google/auth/aio/transport/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Transport - Async HTTP client library support.
"""Transport - Asynchronous HTTP client library support.
:mod:`google.auth.aio` is designed to work with various asynchronous client libraries such
as aiohttp. In order to work across these libraries with different
Expand All @@ -23,3 +23,49 @@
:mod:`google.auth` to make asynchronous requests. :class:`Response` defines the interface
for the return value of :class:`Request`.
"""

import abc


class Request(metaclass=abc.ABCMeta):
"""Interface for a callable that makes HTTP requests.
Specific transport implementations should provide an implementation of
this that adapts their specific request / response API.
.. automethod:: __call__
"""

@abc.abstractmethod
async def __call__(
self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
):
"""Make an HTTP request.
Args:
url (str): The URI to be requested.
method (str): The HTTP method to use for the request. Defaults
to 'GET'.
body (bytes): The payload / body in HTTP request.
headers (Mapping[str, str]): Request headers.
timeout (Optional[int]): The number of seconds to wait for a
response from the server. If not specified or if None, the
transport-specific default timeout will be used.
kwargs: Additionally arguments passed on to the transport's
request method.
Returns:
Response: The HTTP response.
Raises:
google.auth.exceptions.TransportError: If any exception occurred.
"""
# pylint: disable=redundant-returns-doc, missing-raises-doc
# (pylint doesn't play well with abstract docstrings.)
raise NotImplementedError("__call__ must be implemented.")

async def close(self):
"""
Close the underlying session.
"""
raise NotImplementedError("close must be implemented.")
100 changes: 100 additions & 0 deletions google/auth/aio/transport/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,103 @@

"""Transport adapter for Asynchronous HTTP Requests.
"""

import asyncio

try:
import aiohttp
except ImportError as caught_exc: # pragma: NO COVER
raise ImportError(
"The aiohttp library is not installed from please install the aiohttp package to use the aiohttp transport."
) from caught_exc
from google.auth import exceptions

_DEFAULT_TIMEOUT = 180 # in seconds


class Request:
"""Asynchronous Requests request adapter.
This class is used internally for making requests using asyncio transports
in a consistent way. If you use :class:`AuthorizedSession` you do not need
to construct or use this class directly.
This class can be useful if you want to manually refresh a
:class:`~google.auth.aio.credentials.Credentials` instance::
import google.auth.aio.transport.aiohttp
request = google.auth.aio.transport.aiohttp.Request()
await credentials.refresh(request)
Args:
session (aiohttp.ClientSession): An instance :class:`aiohttp.ClientSession` used
to make HTTP requests. If not specified, a session will be created.
.. automethod:: __call__
"""

def __init__(self, session=None):
# TODO(ohmayr): Evaluate if we want auto_decompress=False.
# and if we want to update it in the passed in session.
self.session = session or aiohttp.ClientSession(auto_decompress=False)

async def __call__(
self,
url,
method="GET",
body=None,
headers=None,
timeout=_DEFAULT_TIMEOUT,
**kwargs,
):
"""
Make an Asynchronous HTTP request using aiohttp.
Args:
url (str): The URL to be requested.
method (Optional[str]):
The HTTP method to use for the request. Defaults to 'GET'.
body (Optional[bytes]):
The payload or body in HTTP request.
headers (Optional[Mapping[str, str]]):
Request headers.
timeout (Optional[int]): The number of seconds to wait for a
response from the server. If not specified or if None, the
requests default timeout will be used.
kwargs: Additional arguments passed through to the underlying
aiohttp :meth:`aiohttp.Session.request` method.
Returns:
google.auth.aio.transport.aiohttp.Response: The HTTP response.
Raises:
google.auth.exceptions.TransportError: If any exception occurred.
"""

try:
# TODO (ohmayr): verify the timeout type. We may need to pass
# in aiohttp.ClientTimeout. Alternatively, we can incorporate
# per request timeout within the timeout_guard context manager.
response = await self.session.request(
method, url, data=body, headers=headers, timeout=timeout, **kwargs
)
# TODO(ohmayr): Wrap this with Custom Response.
return response

except aiohttp.ClientError as caught_exc:
new_exc = exceptions.TransportError(caught_exc)
raise new_exc from caught_exc

except asyncio.TimeoutError as caught_exc:

# TODO(ohmayr): Raise Custom Timeout Error instead
new_exc = exceptions.TransportError(caught_exc)
raise new_exc from caught_exc

async def close(self):
"""
Close the underlying aiohttp session.
"""
await self.session.close()
46 changes: 45 additions & 1 deletion tests/transport/aio/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,48 @@
# limitations under the License.

import google.auth.aio.transport.aiohttp as auth_aiohttp
import pytest # type: ignore
import pytest # type: ignore
import pytest_asyncio
import asyncio
from google.auth import exceptions
from aioresponses import aioresponses

# TODO (ohmayr): Verify if we want to optionally run these test cases
# if aohttp is installed instead of raising an exception.
try:
import aiohttp
except ImportError as caught_exc: # pragma: NO COVER
raise ImportError(
"The aiohttp library is not installed from please install the aiohttp package to use the aiohttp transport."
) from caught_exc


# TODO (ohmayr): Update tests to incorporate custom response.
@pytest.mark.asyncio
class TestRequest:
@pytest_asyncio.fixture
async def aiohttp_request(self):
request = auth_aiohttp.Request()
yield request
await request.close()

async def test_request_call_success(self, aiohttp_request):
with aioresponses() as m:
m.get("http://example.com", status=400, body="test")
response = await aiohttp_request("http://example.com")
assert response.status == 400

async def test_request_call_raises_client_error(self, aiohttp_request):
with aioresponses() as m:
m.get("http://example.com", exception=aiohttp.ClientError)

with pytest.raises(exceptions.TransportError):
response = await aiohttp_request("http://example.com/api")

async def test_request_call_raises_timeout_error(self, aiohttp_request):
with aioresponses() as m:
m.get("http://example.com", exception=asyncio.TimeoutError)

# TODO(ohmayr): Update this test case to raise custom error
with pytest.raises(exceptions.TransportError):
response = await aiohttp_request("http://example.com")

0 comments on commit 17e2e25

Please sign in to comment.