Skip to content

Commit f39dff0

Browse files
authored
Merge pull request #312 from auth0/asyncio
[SDK-3181] Asyncio Support (WIP)
2 parents 901752e + 1650d1f commit f39dff0

44 files changed

Lines changed: 642 additions & 261 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- python/install-packages:
3131
pkg-manager: pip-dist
3232
path-args: ".[test]"
33-
- run: coverage run -m unittest discover
33+
- run: coverage run -m unittest discover -s auth0/v3/test -t .
3434
- run: bash <(curl -s https://codecov.io/bash)
3535

3636
workflows:

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[flake8]
2-
ignore = E501
2+
ignore = E501 F401
33
max-line-length = 88

README.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,54 @@ When consuming methods from the API clients, the requests could fail for a numbe
316316
resets is exposed in the ``reset_at`` property. When the header is unset, this value will be ``-1``.
317317
- 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.
318318
319+
=========================
320+
Asynchronous Environments
321+
=========================
322+
323+
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.
324+
325+
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:
326+
327+
.. code-block:: python
328+
329+
import asyncio
330+
import aiohttp
331+
from auth0.v3.asyncify import asyncify
332+
from auth0.v3.management import Auth0, Users, Connections
333+
from auth0.v3.authentication import Users as AuthUsers
334+
335+
auth0 = Auth0('domain', 'mgmt_api_token')
336+
337+
async def main():
338+
# users = auth0.users.all() <= sync
339+
users = await auth0.users.all_async() # <= async
340+
341+
# To share a session amongst multiple calls to the same service
342+
async with auth0.users as users:
343+
data = await users.get_async(id)
344+
users.update_async(id, data)
345+
346+
# Use asyncify directly on services
347+
Users = asyncify(Users)
348+
Connections = asyncify(Connections)
349+
users = Users(domain, mgmt_api_token)
350+
connections = Connections(domain, mgmt_api_token)
351+
352+
# Create a session and share it among the services
353+
session = aiohttp.ClientSession()
354+
users.set_session(session)
355+
connections.set_session(session)
356+
u = await auth0.users.all_async()
357+
c = await auth0.connections.all_async()
358+
session.close()
359+
360+
# Use auth api
361+
U = asyncify(AuthUsers)
362+
u = U(domain=domain)
363+
await u.userinfo_async(access_token)
364+
365+
366+
asyncio.run(main())
319367
320368
==============
321369
Supported APIs

auth0/v3/asyncify.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import aiohttp
2+
3+
from auth0.v3.rest_async import AsyncRestClient
4+
5+
6+
def _gen_async(client, method):
7+
m = getattr(client, method)
8+
9+
async def closure(*args, **kwargs):
10+
return await m(*args, **kwargs)
11+
12+
return closure
13+
14+
15+
def asyncify(cls):
16+
methods = [
17+
func
18+
for func in dir(cls)
19+
if callable(getattr(cls, func)) and not func.startswith("_")
20+
]
21+
22+
class AsyncClient(cls):
23+
def __init__(
24+
self,
25+
domain,
26+
token,
27+
telemetry=True,
28+
timeout=5.0,
29+
protocol="https",
30+
rest_options=None,
31+
):
32+
if token is None:
33+
# Wrap the auth client
34+
super(AsyncClient, self).__init__(domain, telemetry, timeout, protocol)
35+
else:
36+
# Wrap the mngtmt client
37+
super(AsyncClient, self).__init__(
38+
domain, token, telemetry, timeout, protocol, rest_options
39+
)
40+
self.client = AsyncRestClient(
41+
jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options
42+
)
43+
44+
class Wrapper(cls):
45+
def __init__(
46+
self,
47+
domain,
48+
token=None,
49+
telemetry=True,
50+
timeout=5.0,
51+
protocol="https",
52+
rest_options=None,
53+
):
54+
if token is None:
55+
# Wrap the auth client
56+
super(Wrapper, self).__init__(domain, telemetry, timeout, protocol)
57+
else:
58+
# Wrap the mngtmt client
59+
super(Wrapper, self).__init__(
60+
domain, token, telemetry, timeout, protocol, rest_options
61+
)
62+
63+
self._async_client = AsyncClient(
64+
domain, token, telemetry, timeout, protocol, rest_options
65+
)
66+
for method in methods:
67+
setattr(
68+
self,
69+
"{}_async".format(method),
70+
_gen_async(self._async_client, method),
71+
)
72+
73+
async def __aenter__(self):
74+
"""Automatically create and set session within context manager."""
75+
async_rest_client = self._async_client.client
76+
self._session = aiohttp.ClientSession()
77+
async_rest_client.set_session(self._session)
78+
return self
79+
80+
async def __aexit__(self, exc_type, exc_val, exc_tb):
81+
"""Automatically close session within context manager."""
82+
await self._session.close()
83+
84+
return Wrapper

auth0/v3/authentication/base.py

Lines changed: 8 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import requests
77

8+
from auth0.v3.rest import RestClient, RestClientOptions
9+
810
from ..exceptions import Auth0Error, RateLimitError
911

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

2527
def __init__(self, domain, telemetry=True, timeout=5.0, protocol="https"):
2628
self.domain = domain
27-
self.timeout = timeout
2829
self.protocol = protocol
29-
self.base_headers = {"Content-Type": "application/json"}
30-
31-
if telemetry:
32-
py_version = platform.python_version()
33-
version = sys.modules["auth0"].__version__
34-
35-
auth0_client = json.dumps(
36-
{
37-
"name": "auth0-python",
38-
"version": version,
39-
"env": {
40-
"python": py_version,
41-
},
42-
}
43-
).encode("utf-8")
44-
45-
self.base_headers.update(
46-
{
47-
"User-Agent": "Python/{}".format(py_version),
48-
"Auth0-Client": base64.b64encode(auth0_client),
49-
}
50-
)
30+
self.client = RestClient(
31+
None,
32+
options=RestClientOptions(telemetry=telemetry, timeout=timeout, retries=0),
33+
)
5134

5235
def post(self, url, data=None, headers=None):
53-
request_headers = self.base_headers.copy()
54-
request_headers.update(headers or {})
55-
response = requests.post(
56-
url=url, json=data, headers=request_headers, timeout=self.timeout
57-
)
58-
return self._process_response(response)
36+
return self.client.post(url, data, headers)
5937

6038
def get(self, url, params=None, headers=None):
61-
request_headers = self.base_headers.copy()
62-
request_headers.update(headers or {})
63-
response = requests.get(
64-
url=url, params=params, headers=request_headers, timeout=self.timeout
65-
)
66-
return self._process_response(response)
67-
68-
def _process_response(self, response):
69-
return self._parse(response).content()
70-
71-
def _parse(self, response):
72-
if not response.text:
73-
return EmptyResponse(response.status_code)
74-
try:
75-
return JsonResponse(response)
76-
except ValueError:
77-
return PlainResponse(response)
78-
79-
80-
class Response(object):
81-
def __init__(self, status_code, content, headers):
82-
self._status_code = status_code
83-
self._content = content
84-
self._headers = headers
85-
86-
def content(self):
87-
if not self._is_error():
88-
return self._content
89-
90-
if self._status_code == 429:
91-
reset_at = int(self._headers.get("x-ratelimit-reset", "-1"))
92-
raise RateLimitError(
93-
error_code=self._error_code(),
94-
message=self._error_message(),
95-
reset_at=reset_at,
96-
)
97-
98-
raise Auth0Error(
99-
status_code=self._status_code,
100-
error_code=self._error_code(),
101-
message=self._error_message(),
102-
)
103-
104-
def _is_error(self):
105-
return self._status_code is None or self._status_code >= 400
106-
107-
# Adding these methods to force implementation in subclasses because they are references in this parent class
108-
def _error_code(self):
109-
raise NotImplementedError
110-
111-
def _error_message(self):
112-
raise NotImplementedError
113-
114-
115-
class JsonResponse(Response):
116-
def __init__(self, response):
117-
content = json.loads(response.text)
118-
super(JsonResponse, self).__init__(
119-
response.status_code, content, response.headers
120-
)
121-
122-
def _error_code(self):
123-
if "error" in self._content:
124-
return self._content.get("error")
125-
elif "code" in self._content:
126-
return self._content.get("code")
127-
else:
128-
return UNKNOWN_ERROR
129-
130-
def _error_message(self):
131-
return self._content.get("error_description", "")
132-
133-
134-
class PlainResponse(Response):
135-
def __init__(self, response):
136-
super(PlainResponse, self).__init__(
137-
response.status_code, response.text, response.headers
138-
)
139-
140-
def _error_code(self):
141-
return UNKNOWN_ERROR
142-
143-
def _error_message(self):
144-
return self._content
145-
146-
147-
class EmptyResponse(Response):
148-
def __init__(self, status_code):
149-
super(EmptyResponse, self).__init__(status_code, "", {})
150-
151-
def _error_code(self):
152-
return UNKNOWN_ERROR
153-
154-
def _error_message(self):
155-
return ""
39+
return self.client.get(url, params, headers)

auth0/v3/management/actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .rest import RestClient
1+
from ..rest import RestClient
22

33

44
class Actions(object):

auth0/v3/management/attack_protection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .rest import RestClient
1+
from ..rest import RestClient
22

33

44
class AttackProtection(object):

0 commit comments

Comments
 (0)