Skip to content

Commit

Permalink
Merge pull request #312 from auth0/asyncio
Browse files Browse the repository at this point in the history
[SDK-3181] Asyncio Support (WIP)
  • Loading branch information
adamjmcgrath authored May 4, 2022
2 parents 901752e + 1650d1f commit f39dff0
Show file tree
Hide file tree
Showing 44 changed files with 642 additions and 261 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- python/install-packages:
pkg-manager: pip-dist
path-args: ".[test]"
- run: coverage run -m unittest discover
- run: coverage run -m unittest discover -s auth0/v3/test -t .
- run: bash <(curl -s https://codecov.io/bash)

workflows:
Expand Down
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
ignore = E501
ignore = E501 F401
max-line-length = 88
48 changes: 48 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,54 @@ When consuming methods from the API clients, the requests could fail for a numbe
resets is exposed in the ``reset_at`` property. When the header is unset, this value will be ``-1``.
- Network timeouts: Adjustable by passing a ``timeout`` argument to the client. See the `rate limit docs <https://auth0.com/docs/policies/rate-limits>`__ for details.
=========================
Asynchronous Environments
=========================
This SDK provides async methods built on top of `asyncio <https://docs.python.org/3/library/asyncio.html>`__. To make them available you must have Python >=3.6 and the `aiohttp <https://docs.aiohttp.org/en/stable/>`__ module installed.
Then additional methods with the ``_async`` suffix will be added to modules created by the ``management.Auth0`` class or to classes that are passed to the ``asyncify`` method. For example:
.. code-block:: python
import asyncio
import aiohttp
from auth0.v3.asyncify import asyncify
from auth0.v3.management import Auth0, Users, Connections
from auth0.v3.authentication import Users as AuthUsers
auth0 = Auth0('domain', 'mgmt_api_token')
async def main():
# users = auth0.users.all() <= sync
users = await auth0.users.all_async() # <= async
# To share a session amongst multiple calls to the same service
async with auth0.users as users:
data = await users.get_async(id)
users.update_async(id, data)
# Use asyncify directly on services
Users = asyncify(Users)
Connections = asyncify(Connections)
users = Users(domain, mgmt_api_token)
connections = Connections(domain, mgmt_api_token)
# Create a session and share it among the services
session = aiohttp.ClientSession()
users.set_session(session)
connections.set_session(session)
u = await auth0.users.all_async()
c = await auth0.connections.all_async()
session.close()
# Use auth api
U = asyncify(AuthUsers)
u = U(domain=domain)
await u.userinfo_async(access_token)
asyncio.run(main())
==============
Supported APIs
Expand Down
84 changes: 84 additions & 0 deletions auth0/v3/asyncify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import aiohttp

from auth0.v3.rest_async import AsyncRestClient


def _gen_async(client, method):
m = getattr(client, method)

async def closure(*args, **kwargs):
return await m(*args, **kwargs)

return closure


def asyncify(cls):
methods = [
func
for func in dir(cls)
if callable(getattr(cls, func)) and not func.startswith("_")
]

class AsyncClient(cls):
def __init__(
self,
domain,
token,
telemetry=True,
timeout=5.0,
protocol="https",
rest_options=None,
):
if token is None:
# Wrap the auth client
super(AsyncClient, self).__init__(domain, telemetry, timeout, protocol)
else:
# Wrap the mngtmt client
super(AsyncClient, self).__init__(
domain, token, telemetry, timeout, protocol, rest_options
)
self.client = AsyncRestClient(
jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options
)

class Wrapper(cls):
def __init__(
self,
domain,
token=None,
telemetry=True,
timeout=5.0,
protocol="https",
rest_options=None,
):
if token is None:
# Wrap the auth client
super(Wrapper, self).__init__(domain, telemetry, timeout, protocol)
else:
# Wrap the mngtmt client
super(Wrapper, self).__init__(
domain, token, telemetry, timeout, protocol, rest_options
)

self._async_client = AsyncClient(
domain, token, telemetry, timeout, protocol, rest_options
)
for method in methods:
setattr(
self,
"{}_async".format(method),
_gen_async(self._async_client, method),
)

async def __aenter__(self):
"""Automatically create and set session within context manager."""
async_rest_client = self._async_client.client
self._session = aiohttp.ClientSession()
async_rest_client.set_session(self._session)
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Automatically close session within context manager."""
await self._session.close()

return Wrapper
132 changes: 8 additions & 124 deletions auth0/v3/authentication/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import requests

from auth0.v3.rest import RestClient, RestClientOptions

from ..exceptions import Auth0Error, RateLimitError

UNKNOWN_ERROR = "a0.sdk.internal.unknown"
Expand All @@ -24,132 +26,14 @@ class AuthenticationBase(object):

def __init__(self, domain, telemetry=True, timeout=5.0, protocol="https"):
self.domain = domain
self.timeout = timeout
self.protocol = protocol
self.base_headers = {"Content-Type": "application/json"}

if telemetry:
py_version = platform.python_version()
version = sys.modules["auth0"].__version__

auth0_client = json.dumps(
{
"name": "auth0-python",
"version": version,
"env": {
"python": py_version,
},
}
).encode("utf-8")

self.base_headers.update(
{
"User-Agent": "Python/{}".format(py_version),
"Auth0-Client": base64.b64encode(auth0_client),
}
)
self.client = RestClient(
None,
options=RestClientOptions(telemetry=telemetry, timeout=timeout, retries=0),
)

def post(self, url, data=None, headers=None):
request_headers = self.base_headers.copy()
request_headers.update(headers or {})
response = requests.post(
url=url, json=data, headers=request_headers, timeout=self.timeout
)
return self._process_response(response)
return self.client.post(url, data, headers)

def get(self, url, params=None, headers=None):
request_headers = self.base_headers.copy()
request_headers.update(headers or {})
response = requests.get(
url=url, params=params, headers=request_headers, timeout=self.timeout
)
return self._process_response(response)

def _process_response(self, response):
return self._parse(response).content()

def _parse(self, response):
if not response.text:
return EmptyResponse(response.status_code)
try:
return JsonResponse(response)
except ValueError:
return PlainResponse(response)


class Response(object):
def __init__(self, status_code, content, headers):
self._status_code = status_code
self._content = content
self._headers = headers

def content(self):
if not self._is_error():
return self._content

if self._status_code == 429:
reset_at = int(self._headers.get("x-ratelimit-reset", "-1"))
raise RateLimitError(
error_code=self._error_code(),
message=self._error_message(),
reset_at=reset_at,
)

raise Auth0Error(
status_code=self._status_code,
error_code=self._error_code(),
message=self._error_message(),
)

def _is_error(self):
return self._status_code is None or self._status_code >= 400

# Adding these methods to force implementation in subclasses because they are references in this parent class
def _error_code(self):
raise NotImplementedError

def _error_message(self):
raise NotImplementedError


class JsonResponse(Response):
def __init__(self, response):
content = json.loads(response.text)
super(JsonResponse, self).__init__(
response.status_code, content, response.headers
)

def _error_code(self):
if "error" in self._content:
return self._content.get("error")
elif "code" in self._content:
return self._content.get("code")
else:
return UNKNOWN_ERROR

def _error_message(self):
return self._content.get("error_description", "")


class PlainResponse(Response):
def __init__(self, response):
super(PlainResponse, self).__init__(
response.status_code, response.text, response.headers
)

def _error_code(self):
return UNKNOWN_ERROR

def _error_message(self):
return self._content


class EmptyResponse(Response):
def __init__(self, status_code):
super(EmptyResponse, self).__init__(status_code, "", {})

def _error_code(self):
return UNKNOWN_ERROR

def _error_message(self):
return ""
return self.client.get(url, params, headers)
2 changes: 1 addition & 1 deletion auth0/v3/management/actions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .rest import RestClient
from ..rest import RestClient


class Actions(object):
Expand Down
2 changes: 1 addition & 1 deletion auth0/v3/management/attack_protection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .rest import RestClient
from ..rest import RestClient


class AttackProtection(object):
Expand Down
Loading

0 comments on commit f39dff0

Please sign in to comment.