aiohttp-msal 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiohttp_msal/msal_async.py +31 -50
- aiohttp_msal/routes.py +1 -1
- aiohttp_msal/settings.py +2 -4
- aiohttp_msal/settings_base.py +0 -5
- aiohttp_msal/user_info.py +1 -28
- aiohttp_msal/utils.py +51 -0
- {aiohttp_msal-1.0.1.dist-info → aiohttp_msal-1.0.2.dist-info}/METADATA +1 -1
- aiohttp_msal-1.0.2.dist-info/RECORD +11 -0
- aiohttp_msal-1.0.1.dist-info/RECORD +0 -10
- {aiohttp_msal-1.0.1.dist-info → aiohttp_msal-1.0.2.dist-info}/WHEEL +0 -0
aiohttp_msal/msal_async.py
CHANGED
|
@@ -8,9 +8,10 @@ Once you have the OAuth tokens store in the session, you are free to make reques
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
-
from functools import cached_property,
|
|
11
|
+
from functools import cached_property, partialmethod
|
|
12
12
|
from typing import Any, ClassVar, Literal, Unpack
|
|
13
13
|
|
|
14
|
+
import attrs
|
|
14
15
|
from aiohttp import web
|
|
15
16
|
from aiohttp.client import (
|
|
16
17
|
ClientResponse,
|
|
@@ -35,30 +36,13 @@ HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
|
|
|
35
36
|
DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def async_wrap(func: Callable) -> Callable:
|
|
39
|
-
"""Wrap a function doing I/O to run in an executor thread."""
|
|
40
|
-
|
|
41
|
-
@wraps(func)
|
|
42
|
-
async def run(
|
|
43
|
-
*args: Any,
|
|
44
|
-
loop: asyncio.AbstractEventLoop | None = None,
|
|
45
|
-
executor: Any = None,
|
|
46
|
-
**kwargs: dict[str, Any],
|
|
47
|
-
) -> Callable:
|
|
48
|
-
if loop is None:
|
|
49
|
-
loop = asyncio.get_event_loop()
|
|
50
|
-
pfunc = partial(func, *args, **kwargs)
|
|
51
|
-
return await loop.run_in_executor(executor, pfunc)
|
|
52
|
-
|
|
53
|
-
return run
|
|
54
|
-
|
|
55
|
-
|
|
56
39
|
# These keys will be used on the aiohttp session
|
|
57
40
|
TOKEN_CACHE = "token_cache"
|
|
58
41
|
FLOW_CACHE = "flow_cache"
|
|
59
42
|
USER_EMAIL = "mail"
|
|
60
43
|
|
|
61
44
|
|
|
45
|
+
@attrs.define()
|
|
62
46
|
class AsyncMSAL:
|
|
63
47
|
"""AsycMSAL class.
|
|
64
48
|
|
|
@@ -70,22 +54,29 @@ class AsyncMSAL:
|
|
|
70
54
|
Use until such time as MSAL Python gets a true async version.
|
|
71
55
|
"""
|
|
72
56
|
|
|
57
|
+
session: Session | dict[str, Any]
|
|
58
|
+
save_callback: Callable[[Session | dict[str, Any]], None] | None = None
|
|
59
|
+
"""Called if the token cache changes. Optional.
|
|
60
|
+
Not required when the session parameter is an aiohttp_session.Session.
|
|
61
|
+
"""
|
|
62
|
+
app: ConfidentialClientApplication = attrs.field(init=False)
|
|
63
|
+
|
|
64
|
+
app_kwargs: ClassVar[dict[str, Any] | None] = None
|
|
65
|
+
"""ConfidentialClientApplication kwargs."""
|
|
73
66
|
client_session: ClassVar[ClientSession | None] = None
|
|
74
67
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self.
|
|
87
|
-
if not isinstance(session, Session | dict):
|
|
88
|
-
raise ValueError(f"session or dict-like object required {session}")
|
|
68
|
+
def __attrs_post_init__(self) -> None:
|
|
69
|
+
"""Init."""
|
|
70
|
+
kwargs = dict(self.app_kwargs) if self.app_kwargs else {}
|
|
71
|
+
for key, val in {
|
|
72
|
+
"client_id": ENV.SP_APP_ID,
|
|
73
|
+
"client_credential": ENV.SP_APP_PW,
|
|
74
|
+
"authority": ENV.SP_AUTHORITY,
|
|
75
|
+
"validate_authority": False,
|
|
76
|
+
"token_cache": self.token_cache,
|
|
77
|
+
}.items():
|
|
78
|
+
kwargs.setdefault(key, val)
|
|
79
|
+
self.app = ConfidentialClientApplication(**kwargs)
|
|
89
80
|
|
|
90
81
|
@cached_property
|
|
91
82
|
def token_cache(self) -> SerializableTokenCache:
|
|
@@ -95,20 +86,6 @@ class AsyncMSAL:
|
|
|
95
86
|
res.deserialize(self.session[TOKEN_CACHE])
|
|
96
87
|
return res
|
|
97
88
|
|
|
98
|
-
@cached_property
|
|
99
|
-
def app(self) -> ConfidentialClientApplication:
|
|
100
|
-
"""Create the application using the cache.
|
|
101
|
-
|
|
102
|
-
Based on: https://github.com/Azure-Samples/ms-identity-python-webapp/blob/master/app.py#L76
|
|
103
|
-
"""
|
|
104
|
-
return ConfidentialClientApplication(
|
|
105
|
-
client_id=ENV.SP_APP_ID,
|
|
106
|
-
client_credential=ENV.SP_APP_PW,
|
|
107
|
-
authority=ENV.SP_AUTHORITY, # common/oauth2/v2.0/token'
|
|
108
|
-
validate_authority=False,
|
|
109
|
-
token_cache=self.token_cache,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
89
|
def save_token_cache(self) -> None:
|
|
113
90
|
"""Save the token cache if it changed."""
|
|
114
91
|
if self.token_cache.has_state_changed:
|
|
@@ -116,7 +93,7 @@ class AsyncMSAL:
|
|
|
116
93
|
if self.save_callback:
|
|
117
94
|
self.save_callback(self.session)
|
|
118
95
|
|
|
119
|
-
def
|
|
96
|
+
def initiate_auth_code_flow(
|
|
120
97
|
self,
|
|
121
98
|
redirect_uri: str,
|
|
122
99
|
scopes: list[str] | None = None,
|
|
@@ -138,12 +115,16 @@ class AsyncMSAL:
|
|
|
138
115
|
# https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.initiate_auth_code_flow
|
|
139
116
|
return str(res["auth_uri"])
|
|
140
117
|
|
|
141
|
-
def acquire_token_by_auth_code_flow(
|
|
118
|
+
def acquire_token_by_auth_code_flow(
|
|
119
|
+
self, auth_response: Any, scopes: list[str] | None = None
|
|
120
|
+
) -> None:
|
|
142
121
|
"""Second step - Acquire token."""
|
|
143
122
|
# Assume we have it in the cache (added by /login)
|
|
144
123
|
# will raise keryerror if no cache
|
|
145
124
|
auth_code_flow = self.session.pop(FLOW_CACHE)
|
|
146
|
-
result = self.app.acquire_token_by_auth_code_flow(
|
|
125
|
+
result = self.app.acquire_token_by_auth_code_flow(
|
|
126
|
+
auth_code_flow, auth_response, scopes=scopes
|
|
127
|
+
)
|
|
147
128
|
if "error" in result:
|
|
148
129
|
raise web.HTTPBadRequest(text=str(result["error"]))
|
|
149
130
|
if "id_token_claims" not in result:
|
aiohttp_msal/routes.py
CHANGED
|
@@ -44,7 +44,7 @@ async def user_login(request: web.Request) -> web.Response:
|
|
|
44
44
|
session[SESSION_REDIRECT] = urljoin(_to, request.match_info.get("to", ""))
|
|
45
45
|
|
|
46
46
|
msredirect = get_route(request, URI_USER_AUTHORIZED.lstrip("/"))
|
|
47
|
-
redir = AsyncMSAL(session).
|
|
47
|
+
redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
|
|
48
48
|
return web.HTTPFound(redir)
|
|
49
49
|
|
|
50
50
|
|
aiohttp_msal/settings.py
CHANGED
|
@@ -9,8 +9,6 @@ from aiohttp_msal.settings_base import VAR_REQ, VAR_REQ_HIDE, SettingsBase
|
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from redis.asyncio import Redis
|
|
12
|
-
else:
|
|
13
|
-
Redis = None
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
@attrs.define
|
|
@@ -28,7 +26,7 @@ class MSALSettings(SettingsBase):
|
|
|
28
26
|
"https://login.microsoftonline.com/common" # For multi-tenant app
|
|
29
27
|
"https://login.microsoftonline.com/Tenant_Name_or_UUID_Here"."""
|
|
30
28
|
|
|
31
|
-
DOMAIN: str =
|
|
29
|
+
DOMAIN: str = attrs.field(metadata=VAR_REQ, default="")
|
|
32
30
|
"""Your domain. Used by routes & Redis functions."""
|
|
33
31
|
|
|
34
32
|
COOKIE_NAME: str = "AIOHTTP_SESSION"
|
|
@@ -41,7 +39,7 @@ class MSALSettings(SettingsBase):
|
|
|
41
39
|
|
|
42
40
|
REDIS: str = "redis://redis1:6379"
|
|
43
41
|
"""OPTIONAL: Redis database connection used by app_init_redis_session()."""
|
|
44
|
-
database: Redis =
|
|
42
|
+
database: "Redis" = attrs.field(init=False)
|
|
45
43
|
"""Store the Redis connection when using app_init_redis_session()."""
|
|
46
44
|
|
|
47
45
|
|
aiohttp_msal/settings_base.py
CHANGED
|
@@ -14,11 +14,6 @@ VAR_REQ = {KEY_REQ: True}
|
|
|
14
14
|
VAR_HIDE = {KEY_HIDE: True}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _is_hidden(atr: attrs.Attribute) -> bool:
|
|
18
|
-
"""Is this field hidden."""
|
|
19
|
-
return bool(atr.metadata.get(KEY_HIDE))
|
|
20
|
-
|
|
21
|
-
|
|
22
17
|
@attrs.define
|
|
23
18
|
class SettingsBase:
|
|
24
19
|
"""Retrieve Settings from environment variables.
|
aiohttp_msal/user_info.py
CHANGED
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
"""Graph User Info."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
|
-
from functools import wraps
|
|
6
|
-
from typing import ParamSpec, TypeVar
|
|
7
|
-
|
|
8
3
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
9
|
-
|
|
10
|
-
_T = TypeVar("_T")
|
|
11
|
-
_P = ParamSpec("_P")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def retry(func: Callable[_P, Awaitable[_T]]) -> Callable[_P, Awaitable[_T]]:
|
|
15
|
-
"""Retry if tenacity is installed."""
|
|
16
|
-
|
|
17
|
-
@wraps(func)
|
|
18
|
-
async def _retry(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
19
|
-
"""Retry the request."""
|
|
20
|
-
retries = [2, 4, 8]
|
|
21
|
-
while True:
|
|
22
|
-
try:
|
|
23
|
-
res = await func(*args, **kwargs)
|
|
24
|
-
return res
|
|
25
|
-
except Exception as err:
|
|
26
|
-
if retries:
|
|
27
|
-
await asyncio.sleep(retries.pop())
|
|
28
|
-
else:
|
|
29
|
-
raise err
|
|
30
|
-
|
|
31
|
-
return _retry
|
|
4
|
+
from aiohttp_msal.utils import retry
|
|
32
5
|
|
|
33
6
|
|
|
34
7
|
@retry
|
aiohttp_msal/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Graph User Info."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import ParamSpec, TypeVar, Any
|
|
7
|
+
from functools import partial
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
P = ParamSpec("P")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def async_wrap(
|
|
15
|
+
func: Callable[..., T],
|
|
16
|
+
) -> Callable[..., Awaitable[T]]:
|
|
17
|
+
"""Wrap a function doing I/O to run in an executor thread."""
|
|
18
|
+
|
|
19
|
+
@wraps(func)
|
|
20
|
+
async def run(
|
|
21
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
22
|
+
executor: Any = None,
|
|
23
|
+
*args: Any,
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
) -> T:
|
|
26
|
+
if loop is None:
|
|
27
|
+
loop = asyncio.get_event_loop()
|
|
28
|
+
pfunc = partial(func, *args, **kwargs)
|
|
29
|
+
return await loop.run_in_executor(executor, pfunc)
|
|
30
|
+
|
|
31
|
+
return run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
35
|
+
"""Retry if tenacity is installed."""
|
|
36
|
+
|
|
37
|
+
@wraps(func)
|
|
38
|
+
async def _retry(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
39
|
+
"""Retry the request."""
|
|
40
|
+
retries = [2, 4, 8]
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
res = await func(*args, **kwargs)
|
|
44
|
+
return res
|
|
45
|
+
except Exception as err:
|
|
46
|
+
if retries:
|
|
47
|
+
await asyncio.sleep(retries.pop())
|
|
48
|
+
else:
|
|
49
|
+
raise err
|
|
50
|
+
|
|
51
|
+
return _retry
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
+
aiohttp_msal/msal_async.py,sha256=JUtyTro57rLiHQYqgYq8LFv3keznyhISP1emgN3y31E,8232
|
|
3
|
+
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
4
|
+
aiohttp_msal/routes.py,sha256=WyLBuoPMkkG6Cx4gFUu_ER71FyJbeXKhOQRQu5ALG2M,8138
|
|
5
|
+
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
6
|
+
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
7
|
+
aiohttp_msal/user_info.py,sha256=lxjFxjm16rvC-0LS81y7SG5pCOa5Zl0s62uxi97yu_k,1171
|
|
8
|
+
aiohttp_msal/utils.py,sha256=SgGpE1eFdVh48FaKvtbnQqJKTReXa9OPBKiYGY7SYq8,1303
|
|
9
|
+
aiohttp_msal-1.0.2.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
+
aiohttp_msal-1.0.2.dist-info/METADATA,sha256=b9HRcY4HaOKhXVhBQIuYK-h0Cia9g922FjOdTyOuPpw,4514
|
|
11
|
+
aiohttp_msal-1.0.2.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
-
aiohttp_msal/msal_async.py,sha256=p0OkF48ZDmCAUYN4qjn3UdMyH1oS4iBQSB5Z_C3yIBU,8816
|
|
3
|
-
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
4
|
-
aiohttp_msal/routes.py,sha256=8wU2jU9qSlqH5fq9kvkBZHAgrxQdmB-DvtQC-WlXbh0,8135
|
|
5
|
-
aiohttp_msal/settings.py,sha256=ttbqGb2X1r7DsLvKb1AlDDKBYZWjIwHLHI-Sa-8K-lI,1562
|
|
6
|
-
aiohttp_msal/settings_base.py,sha256=tRbjgphR1tvHCrFCcfOUhoFAyUnq_XnJBVO4NNiPdNg,3150
|
|
7
|
-
aiohttp_msal/user_info.py,sha256=tO-vA_kxPseHseWxNlhGc_NlDfgJGdf1OMCaGmvDf8Q,1875
|
|
8
|
-
aiohttp_msal-1.0.1.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
9
|
-
aiohttp_msal-1.0.1.dist-info/METADATA,sha256=NJEzFuQyWHai3QcqvyTfeAWWfjq98nucwoYFt3qIOvc,4514
|
|
10
|
-
aiohttp_msal-1.0.1.dist-info/RECORD,,
|
|
File without changes
|