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.
@@ -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,
@@ -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 __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.
81
-
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}")
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 build_auth_code_flow(
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(self, auth_response: Any) -> None:
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(auth_code_flow, auth_response)
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).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.2
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=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,,