aiohttp-msal 1.0.1__py3-none-any.whl → 1.0.3__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.
@@ -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, partial, partialmethod, wraps
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,
@@ -33,32 +34,10 @@ HTTP_DELETE = "delete"
33
34
  HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
34
35
 
35
36
  DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
36
-
37
-
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
- # These keys will be used on the aiohttp session
57
- TOKEN_CACHE = "token_cache"
58
37
  FLOW_CACHE = "flow_cache"
59
- USER_EMAIL = "mail"
60
38
 
61
39
 
40
+ @attrs.define()
62
41
  class AsyncMSAL:
63
42
  """AsycMSAL class.
64
43
 
@@ -70,53 +49,48 @@ class AsyncMSAL:
70
49
  Use until such time as MSAL Python gets a true async version.
71
50
  """
72
51
 
52
+ session: Session | dict[str, Any]
53
+ save_callback: Callable[[Session | dict[str, Any]], None] | None = None
54
+ """Called if the token cache changes. Optional.
55
+ Not required when the session parameter is an aiohttp_session.Session.
56
+ """
57
+ app_kwargs: dict[str, Any] | None = None
58
+ """ConfidentialClientApplication kwargs."""
73
59
  client_session: ClassVar[ClientSession | None] = None
74
60
 
75
- def __init__(
76
- self,
77
- session: Session | dict[str, Any],
78
- save_callback: Callable[[Session | dict[str, Any]], None] | None = None,
79
- ):
80
- """Init the class.
61
+ token_cache_key: str = "token_cache"
62
+ user_email_key: str = "mail"
81
63
 
82
- **save_callback** will be called if the token cache changes. Optional.
83
- Not required when the session parameter is an aiohttp_session.Session.
84
- """
85
- self.session = session
86
- self.save_callback = save_callback
87
- if not isinstance(session, Session | dict):
88
- raise ValueError(f"session or dict-like object required {session}")
64
+ @cached_property
65
+ def app(self) -> ConfidentialClientApplication:
66
+ """Get the app."""
67
+ kwargs = {
68
+ "client_id": ENV.SP_APP_ID,
69
+ "client_credential": ENV.SP_APP_PW,
70
+ "authority": ENV.SP_AUTHORITY,
71
+ "validate_authority": False,
72
+ "token_cache": self.token_cache,
73
+ }
74
+ if self.app_kwargs:
75
+ kwargs.update(self.app_kwargs)
76
+ return ConfidentialClientApplication(**kwargs)
89
77
 
90
78
  @cached_property
91
79
  def token_cache(self) -> SerializableTokenCache:
92
80
  """Get the token_cache."""
93
81
  res = SerializableTokenCache()
94
- if self.session and self.session.get(TOKEN_CACHE):
95
- res.deserialize(self.session[TOKEN_CACHE])
82
+ if tc := self.session.get(self.token_cache_key):
83
+ res.deserialize(tc)
96
84
  return res
97
85
 
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
86
  def save_token_cache(self) -> None:
113
87
  """Save the token cache if it changed."""
114
88
  if self.token_cache.has_state_changed:
115
- self.session[TOKEN_CACHE] = self.token_cache.serialize()
89
+ self.session[self.token_cache_key] = self.token_cache.serialize()
116
90
  if self.save_callback:
117
91
  self.save_callback(self.session)
118
92
 
119
- def build_auth_code_flow(
93
+ def initiate_auth_code_flow(
120
94
  self,
121
95
  redirect_uri: str,
122
96
  scopes: list[str] | None = None,
@@ -124,8 +98,8 @@ class AsyncMSAL:
124
98
  **kwargs: Any,
125
99
  ) -> str:
126
100
  """First step - Start the flow."""
127
- self.session[TOKEN_CACHE] = None
128
- self.session[USER_EMAIL] = None
101
+ self.session.pop(self.token_cache_key, None)
102
+ self.session.pop(self.user_email_key, None)
129
103
  self.session[FLOW_CACHE] = res = self.app.initiate_auth_code_flow(
130
104
  scopes or DEFAULT_SCOPES,
131
105
  redirect_uri=redirect_uri,
@@ -138,19 +112,23 @@ class AsyncMSAL:
138
112
  # https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.initiate_auth_code_flow
139
113
  return str(res["auth_uri"])
140
114
 
141
- def acquire_token_by_auth_code_flow(self, auth_response: Any) -> None:
115
+ def acquire_token_by_auth_code_flow(
116
+ self, auth_response: Any, scopes: list[str] | None = None
117
+ ) -> None:
142
118
  """Second step - Acquire token."""
143
119
  # Assume we have it in the cache (added by /login)
144
120
  # will raise keryerror if no cache
145
121
  auth_code_flow = self.session.pop(FLOW_CACHE)
146
- result = self.app.acquire_token_by_auth_code_flow(auth_code_flow, auth_response)
122
+ result = self.app.acquire_token_by_auth_code_flow(
123
+ auth_code_flow, auth_response, scopes=scopes
124
+ )
147
125
  if "error" in result:
148
126
  raise web.HTTPBadRequest(text=str(result["error"]))
149
127
  if "id_token_claims" not in result:
150
128
  raise web.HTTPBadRequest(text=f"Expected id_token_claims in {result}")
151
129
  self.save_token_cache()
152
130
  if tok := result.get("id_token_claims"):
153
- self.session[USER_EMAIL] = tok.get("preferred_username")
131
+ self.session[self.user_email_key] = tok.get("preferred_username")
154
132
 
155
133
  async def async_acquire_token_by_auth_code_flow(self, auth_response: Any) -> None:
156
134
  """Second step - Acquire token, async version."""
@@ -222,7 +200,7 @@ class AsyncMSAL:
222
200
  @property
223
201
  def mail(self) -> str:
224
202
  """User email."""
225
- return self.session.get(USER_EMAIL, "")
203
+ return self.session.get(self.user_email_key, "")
226
204
 
227
205
  @property
228
206
  def manager_mail(self) -> str:
@@ -242,4 +220,4 @@ class AsyncMSAL:
242
220
  @property
243
221
  def authenticated(self) -> bool:
244
222
  """If the user is logged in."""
245
- return bool(self.session.get("mail"))
223
+ return bool(self.session.get(self.user_email_key))
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).build_auth_code_flow(redirect_uri=msredirect)
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 = "mydomain.com"
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 = None # type: ignore[assignment]
42
+ database: "Redis" = attrs.field(init=False)
45
43
  """Store the Redis connection when using app_init_redis_session()."""
46
44
 
47
45
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aiohttp-msal
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Helper Library to use the Microsoft Authentication Library (MSAL) with aiohttp
5
5
  Keywords: aiohttp,asyncio,msal,oauth
6
6
  Author: Johann Kellerman
@@ -0,0 +1,11 @@
1
+ aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
2
+ aiohttp_msal/msal_async.py,sha256=urQvaMTi0mJnCboCsj8A9F9VpWcjPOYaechpZ5XrnbY,8153
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.3.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
10
+ aiohttp_msal-1.0.3.dist-info/METADATA,sha256=raujaCawOODO6HYM79crhTECU28f_IjlkzIPmR8ck48,4514
11
+ aiohttp_msal-1.0.3.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,,