Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 57c3157

Browse files
committed
Add asyncio support
1 parent f15520b commit 57c3157

File tree

9 files changed

+243
-3
lines changed

9 files changed

+243
-3
lines changed

.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

auth0/v3/management/asyncify.py

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

auth0/v3/management/rest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(self, jwt, telemetry=True, timeout=5.0, options=None):
9696
self.base_headers.update(
9797
{
9898
"User-Agent": "Python/{}".format(py_version),
99-
"Auth0-Client": base64.b64encode(auth0_client),
99+
"Auth0-Client": base64.b64encode(auth0_client).decode(),
100100
}
101101
)
102102

auth0/v3/management/rest_async.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import asyncio
2+
import json
3+
4+
import aiohttp
5+
6+
from .rest import EmptyResponse, JsonResponse, PlainResponse
7+
from .rest import Response as _Response
8+
from .rest import RestClient
9+
10+
11+
def _clean_params(params):
12+
if params is None:
13+
return params
14+
return {k: v for k, v in params.items() if v is not None}
15+
16+
17+
class AsyncRestClient(RestClient):
18+
"""Provides simple methods for handling all RESTful api endpoints.
19+
20+
Args:
21+
telemetry (bool, optional): Enable or disable Telemetry
22+
(defaults to True)
23+
timeout (float or tuple, optional): Change the requests
24+
connect and read timeout. Pass a tuple to specify
25+
both values separately or a float to set both to it.
26+
(defaults to 5.0 for both)
27+
options (RestClientOptions): Pass an instance of
28+
RestClientOptions to configure additional RestClient
29+
options, such as rate-limit retries. Overrides matching
30+
options passed to the constructor.
31+
(defaults to 3)
32+
"""
33+
34+
def __init__(self, *args, **kwargs):
35+
super(AsyncRestClient, self).__init__(*args, **kwargs)
36+
self._session = None
37+
38+
def set_session(self, session):
39+
"""Set Client Session to improve performance by reusing session.
40+
Session should be closed manually or within context manager.
41+
"""
42+
self._session = session
43+
44+
async def _request(self, *args, **kwargs):
45+
headers = kwargs.get("headers", self.base_headers)
46+
if self._session is not None:
47+
# Request with re-usable session
48+
async with self._session.request(
49+
*args, **kwargs, headers=headers
50+
) as response:
51+
return await self._process_response(response)
52+
else:
53+
# Request without re-usable session
54+
async with aiohttp.ClientSession(headers=headers) as session:
55+
async with session.request(*args, **kwargs) as response:
56+
return await self._process_response(response)
57+
58+
async def get(self, url, params=None):
59+
return await self._request("get", url, params=_clean_params(params))
60+
61+
async def post(self, url, data=None):
62+
return await self._request("post", url, json=data)
63+
64+
async def file_post(self, url, data=None, files=None):
65+
headers = self.base_headers.copy()
66+
headers.pop("Content-Type", None)
67+
return await self._request("post", url, data={**data, **files}, headers=headers)
68+
69+
async def patch(self, url, data=None):
70+
return await self._request("patch", url, json=data)
71+
72+
async def put(self, url, data=None):
73+
return await self._request("put", url, json=data)
74+
75+
async def delete(self, url, params=None, data=None):
76+
return await self._request(
77+
"delete", url, json=data, params=_clean_params(params) or {}
78+
)
79+
80+
async def _process_response(self, response):
81+
parsed_response = await self._parse(response)
82+
return parsed_response.content()
83+
84+
async def _parse(self, response):
85+
text = await response.text()
86+
requests_response = RequestsResponse(response, text)
87+
if not text:
88+
return EmptyResponse(response.status)
89+
try:
90+
return JsonResponse(requests_response)
91+
except ValueError:
92+
return PlainResponse(requests_response)
93+
94+
95+
class RequestsResponse(object):
96+
def __init__(self, response, text):
97+
self.status_code = response.status
98+
self.headers = response.headers
99+
self.text = text

auth0/v3/management/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import sys
2+
3+
4+
def is_async_available():
5+
if sys.version_info >= (3, 0):
6+
try:
7+
import asyncio
8+
9+
import aiohttp
10+
import async_timeout
11+
12+
return True
13+
except ImportError:
14+
pass
15+
16+
return False

auth0/v3/test_async/__init__.py

Whitespace-only changes.

auth0/v3/test_async/test_asyncify.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import asyncio
2+
import unittest
3+
4+
import mock
5+
from aioresponses import aioresponses
6+
7+
from auth0.v3.management.asyncify import asyncify
8+
from auth0.v3.management.clients import Clients
9+
10+
payload = {"foo": "bar"}
11+
12+
13+
class TestAsyncify(unittest.IsolatedAsyncioTestCase):
14+
@aioresponses()
15+
async def test_all(self, mocked):
16+
mocked.get(
17+
"https://example.com/api/v2/clients?include_fields=true",
18+
status=200,
19+
payload=payload,
20+
)
21+
c = asyncify(Clients)(
22+
domain="example.com",
23+
token="jwt",
24+
)
25+
self.assertEqual(await c.all_async(), payload)
26+
27+
@aioresponses()
28+
async def test_all_shared_session(self, mocked):
29+
mocked.get(
30+
"https://example.com/api/v2/clients?include_fields=true",
31+
status=200,
32+
payload=payload,
33+
)
34+
async with asyncify(Clients)(
35+
domain="example.com",
36+
token="jwt",
37+
) as c:
38+
self.assertEqual(await c.all_async(), payload)

requirements.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
-e .
2+
aiohttp==3.8.1
3+
aioresponses==0.7.3
4+
aiosignal==1.2.0
5+
alabaster==0.7.12
6+
async-timeout==4.0.2
7+
attrs==21.4.0
8+
Authlib==1.0.0
29
Babel==2.9.1
310
black==22.3.0
411
certifi==2021.10.8
@@ -8,30 +15,43 @@ charset-normalizer==2.0.12
815
click==8.0.4
916
coverage==6.3.2
1017
cryptography==36.0.2
18+
Deprecated==1.2.13
1119
distlib==0.3.4
1220
docutils==0.17.1
1321
filelock==3.6.0
1422
flake8==4.0.1
23+
Flask==2.0.3
24+
Flask-Cors==3.0.10
25+
frozenlist==1.3.0
1526
identify==2.4.12
1627
idna==3.3
1728
imagesize==1.3.0
29+
iniconfig==1.1.1
1830
isort==5.10.1
31+
itsdangerous==2.1.1
1932
Jinja2==3.1.1
33+
jwcrypto==1.0
2034
MarkupSafe==2.1.1
2135
mccabe==0.6.1
2236
mock==4.0.3
37+
multidict==6.0.2
2338
mypy-extensions==0.4.3
2439
nodeenv==1.6.0
2540
packaging==21.3
2641
pathspec==0.9.0
2742
platformdirs==2.5.1
43+
pluggy==1.0.0
2844
pre-commit==2.17.0
45+
py==1.11.0
2946
pycodestyle==2.8.0
3047
pycparser==2.21
3148
pyflakes==2.4.0
3249
Pygments==2.11.2
3350
PyJWT==2.3.0
3451
pyparsing==3.0.7
52+
pytest==7.1.0
53+
pytest-mock==3.7.0
54+
python-dotenv==0.19.2
3555
pytz==2022.1
3656
pyupgrade==2.31.1
3757
PyYAML==6.0
@@ -51,3 +71,6 @@ toml==0.10.2
5171
tomli==2.0.1
5272
urllib3==1.26.9
5373
virtualenv==20.13.4
74+
Werkzeug==2.0.3
75+
wrapt==1.14.0
76+
yarl==1.7.2

0 commit comments

Comments
 (0)