aiohttp-msal 1.0.3__py3-none-any.whl → 1.0.5__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/__init__.py CHANGED
@@ -1,10 +1,11 @@
1
1
  """aiohttp_msal."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from collections.abc import Awaitable, Callable
5
6
  from functools import wraps
6
7
  from inspect import getfullargspec, iscoroutinefunction
7
- from typing import TypeVar, TypeVarTuple, cast
8
+ from typing import Any, TypeVar, TypeVarTuple, cast
8
9
 
9
10
  from aiohttp import ClientSession, web
10
11
  from aiohttp_session import get_session
@@ -12,6 +13,7 @@ from aiohttp_session import setup as _setup
12
13
 
13
14
  from aiohttp_msal.msal_async import AsyncMSAL
14
15
  from aiohttp_msal.settings import ENV
16
+ from aiohttp_msal.utils import retry
15
17
 
16
18
  _LOGGER = logging.getLogger(__name__)
17
19
 
@@ -87,7 +89,11 @@ def auth_or(
87
89
 
88
90
 
89
91
  async def app_init_redis_session(
90
- app: web.Application, max_age: int = 3600 * 24 * 90
92
+ app: web.Application,
93
+ max_age: int = 3600 * 24 * 90,
94
+ check_proxy_cb: Callable[[], Awaitable[None]] | None = None,
95
+ encoder: Callable[[object], str] = json.dumps,
96
+ decoder: Callable[[str], Any] = json.loads,
91
97
  ) -> None:
92
98
  """Init an aiohttp_session with Redis storage helper.
93
99
 
@@ -96,7 +102,10 @@ async def app_init_redis_session(
96
102
  from aiohttp_session import redis_storage
97
103
  from redis.asyncio import from_url
98
104
 
99
- await check_proxy()
105
+ if check_proxy_cb:
106
+ await check_proxy_cb()
107
+ else:
108
+ await check_proxy()
100
109
 
101
110
  _LOGGER.info("Connect to Redis %s", ENV.REDIS)
102
111
  try:
@@ -114,10 +123,13 @@ async def app_init_redis_session(
114
123
  secure=True,
115
124
  domain=ENV.DOMAIN,
116
125
  cookie_name=ENV.COOKIE_NAME,
126
+ encoder=encoder,
127
+ decoder=decoder,
117
128
  )
118
129
  _setup(app, storage)
119
130
 
120
131
 
132
+ @retry
121
133
  async def check_proxy() -> None:
122
134
  """Test if we have Internet connectivity through proxies etc."""
123
135
  try:
@@ -0,0 +1,76 @@
1
+ """Graph User Info."""
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from aiohttp import web
7
+
8
+ from aiohttp_msal.settings import ENV
9
+ from aiohttp_msal.utils import retry
10
+
11
+ if TYPE_CHECKING:
12
+ from aiohttp_msal.msal_async import AsyncMSAL
13
+
14
+
15
+ @retry
16
+ async def get_user_info(aiomsal: "AsyncMSAL") -> None:
17
+ """Load user info from MS graph API. Requires User.Read permissions."""
18
+ async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
19
+ body = await res.json()
20
+ try:
21
+ aiomsal.mail = body["mail"]
22
+ aiomsal.name = body["displayName"]
23
+ except KeyError as err:
24
+ raise KeyError(
25
+ f"Unexpected return from Graph endpoint: {body}: {err}"
26
+ ) from err
27
+
28
+
29
+ @retry
30
+ async def get_manager_info(aiomsal: "AsyncMSAL") -> None:
31
+ """Load manager info from MS graph API. Requires User.Read.All permissions."""
32
+ async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
33
+ body = await res.json()
34
+ try:
35
+ aiomsal.manager_mail = body["mail"]
36
+ aiomsal.manager_name = body["displayName"]
37
+ except KeyError as err:
38
+ raise KeyError(
39
+ f"Unexpected return from Graph endpoint: {body}: {err}"
40
+ ) from err
41
+
42
+
43
+ def html_table(items: Mapping[Any, Any]) -> str:
44
+ """Return a table HTML."""
45
+ res = "<table style='width:80%;border:1px solid black;'>"
46
+ for key, val in items.items():
47
+ res += f"<tr><td>{key}</td><td>{val}</td></tr>"
48
+ res += "</table>"
49
+ return res
50
+
51
+
52
+ def html_wrap(msgs: Sequence[str]) -> str:
53
+ """Return proper HTML when login fails."""
54
+ html = "</li><li>".join(msgs)
55
+ return f"""
56
+ <h2>Login failed</h2>
57
+
58
+ <p>Retry at <a href='/user/login'>/user/login</a></p>
59
+
60
+ <p>Try clearing the cookies for <b>.{ENV.DOMAIN}<b> by navigating to the correct
61
+ address for your browser:
62
+ <ul>
63
+ <li>chrome://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
64
+ <li>brave://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
65
+ <li>edge://settings/siteData (you will have to search for {ENV.DOMAIN} cookies)</li>
66
+ </ul></p>
67
+
68
+ <h4>Debug info</h4>
69
+ <ul><li>{html}</li></ul>
70
+ """
71
+
72
+
73
+ def get_url(request: web.Request, path: str = "", https_proxy: bool = True) -> str:
74
+ """Return the full outside URL."""
75
+ res = str(request.url.with_path(path))
76
+ return res.replace("http://", "https://") if https_proxy else res
@@ -7,9 +7,10 @@ Once you have the OAuth tokens store in the session, you are free to make reques
7
7
 
8
8
  import asyncio
9
9
  import json
10
+ import logging
10
11
  from collections.abc import Callable
11
12
  from functools import cached_property, partialmethod
12
- from typing import Any, ClassVar, Literal, Unpack
13
+ from typing import Any, ClassVar, Literal, Self, TypeVar, Unpack, cast
13
14
 
14
15
  import attrs
15
16
  from aiohttp import web
@@ -20,10 +21,14 @@ from aiohttp.client import (
20
21
  _RequestOptions,
21
22
  )
22
23
  from aiohttp.typedefs import StrOrURL
23
- from aiohttp_session import Session
24
+ from aiohttp_session import Session, get_session, new_session
24
25
  from msal import ConfidentialClientApplication, SerializableTokenCache
25
26
 
27
+ from aiohttp_msal import helpers
26
28
  from aiohttp_msal.settings import ENV
29
+ from aiohttp_msal.utils import dict_property
30
+
31
+ _LOG = logging.getLogger(__name__)
27
32
 
28
33
  HttpMethods = Literal["get", "post", "put", "patch", "delete"]
29
34
  HTTP_GET = "get"
@@ -33,11 +38,10 @@ HTTP_PATCH = "patch"
33
38
  HTTP_DELETE = "delete"
34
39
  HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
35
40
 
36
- DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
37
- FLOW_CACHE = "flow_cache"
41
+ T = TypeVar("T")
38
42
 
39
43
 
40
- @attrs.define()
44
+ @attrs.define(slots=False)
41
45
  class AsyncMSAL:
42
46
  """AsycMSAL class.
43
47
 
@@ -56,10 +60,45 @@ class AsyncMSAL:
56
60
  """
57
61
  app_kwargs: dict[str, Any] | None = None
58
62
  """ConfidentialClientApplication kwargs."""
63
+
59
64
  client_session: ClassVar[ClientSession | None] = None
65
+ token_cache_key: ClassVar[str] = "token_cache"
66
+ user_email_key: ClassVar[str] = "mail"
67
+ flow_cache_key: ClassVar[str] = "flow_cache"
68
+ redirect_key: ClassVar[str] = "redirect"
69
+ default_scopes: ClassVar[list[str]] = ["User.Read", "User.Read.All"]
70
+
71
+ @classmethod
72
+ async def from_request(
73
+ cls,
74
+ request: web.Request,
75
+ /,
76
+ allow_new: bool = False,
77
+ app_kwargs: dict[str, Any] | None = None,
78
+ ) -> Self:
79
+ """Get the session or raise an exception."""
80
+ try:
81
+ session = await get_session(request)
82
+ return cls(session, app_kwargs=app_kwargs)
83
+ except TypeError as err:
84
+ cookie = request.get(ENV.COOKIE_NAME)
85
+ _LOG.error(
86
+ "Invalid session, %s: %s [cookie: %s]",
87
+ "create new" if allow_new else "fail",
88
+ err,
89
+ request.get(ENV.COOKIE_NAME),
90
+ cookie,
91
+ )
92
+ if allow_new:
93
+ return cls(await new_session(request), app_kwargs=app_kwargs)
94
+
95
+ text = "Invalid session. Login for a new session: '/usr/login'"
60
96
 
61
- token_cache_key: str = "token_cache"
62
- user_email_key: str = "mail"
97
+ if not cookie:
98
+ cookies = dict(request.cookies.items())
99
+ text = f"Cookie empty. Expected '{ENV.COOKIE_NAME}' in cookies: {list(cookies)}. Cookie should be set with Samesite:None"
100
+
101
+ raise web.HTTPException(text=text) from None
63
102
 
64
103
  @cached_property
65
104
  def app(self) -> ConfidentialClientApplication:
@@ -100,8 +139,8 @@ class AsyncMSAL:
100
139
  """First step - Start the flow."""
101
140
  self.session.pop(self.token_cache_key, None)
102
141
  self.session.pop(self.user_email_key, None)
103
- self.session[FLOW_CACHE] = res = self.app.initiate_auth_code_flow(
104
- scopes or DEFAULT_SCOPES,
142
+ self.session[self.flow_cache_key] = res = self.app.initiate_auth_code_flow(
143
+ scopes or self.default_scopes,
105
144
  redirect_uri=redirect_uri,
106
145
  response_mode="form_post",
107
146
  prompt=prompt,
@@ -117,8 +156,8 @@ class AsyncMSAL:
117
156
  ) -> None:
118
157
  """Second step - Acquire token."""
119
158
  # Assume we have it in the cache (added by /login)
120
- # will raise keryerror if no cache
121
- auth_code_flow = self.session.pop(FLOW_CACHE)
159
+ # will raise KeyError if not in cache
160
+ auth_code_flow = self.session.pop(self.flow_cache_key)
122
161
  result = self.app.acquire_token_by_auth_code_flow(
123
162
  auth_code_flow, auth_response, scopes=scopes
124
163
  )
@@ -141,7 +180,7 @@ class AsyncMSAL:
141
180
  accounts = self.app.get_accounts()
142
181
  if accounts:
143
182
  result = self.app.acquire_token_silent(
144
- scopes=scopes or DEFAULT_SCOPES, account=accounts[0]
183
+ scopes=scopes or self.default_scopes, account=accounts[0]
145
184
  )
146
185
  self.save_token_cache()
147
186
  return result
@@ -198,26 +237,76 @@ class AsyncMSAL:
198
237
  post = partialmethod(request_ctx, HTTP_POST)
199
238
 
200
239
  @property
201
- def mail(self) -> str:
202
- """User email."""
203
- return self.session.get(self.user_email_key, "")
240
+ def authenticated(self) -> bool:
241
+ """If the user is logged in."""
242
+ return bool(self.session.get(self.user_email_key))
204
243
 
205
- @property
206
- def manager_mail(self) -> str:
207
- """User's manager's email."""
208
- return self.session.get("m_mail", "")
244
+ name = cast(str, dict_property("session", "name"))
245
+ mail = dict_property("session", user_email_key)
246
+ manager_name = dict_property("session", "m_name")
247
+ manager_mail = dict_property("session", "m_mail")
248
+ redirect = dict_property("session", redirect_key)
209
249
 
210
- @property
211
- def manager_name(self) -> str:
212
- """User's manager's name."""
213
- return self.session.get("m_name", "")
250
+ async def async_acquire_token_by_auth_code_flow_plus(
251
+ self,
252
+ request: web.Request,
253
+ get_info: Literal["user", "manager", ""] = "manager",
254
+ ) -> tuple[bool, list[str]]:
255
+ """Enhanced version of async_acquire_token_by_auth_code_flow. Returns issues.
214
256
 
215
- @property
216
- def name(self) -> str:
217
- """User's display name."""
218
- return self.session.get("name", "")
257
+ Parse the auth response from the request, checks for valid keys,
258
+ acquire the token and get_info.
219
259
 
220
- @property
221
- def authenticated(self) -> bool:
222
- """If the user is logged in."""
223
- return bool(self.session.get(self.user_email_key))
260
+ response_mode for the auth_code flow should be "form_post"
261
+ """
262
+ auth_response = dict(await request.post())
263
+ assert isinstance(self.session, Session)
264
+
265
+ msg = list[str]()
266
+
267
+ # Ensure all expected variables were returned...
268
+ if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
269
+ msg.append("Expected 'code', 'session_state', 'state' in auth_response")
270
+ msg.append(f"Received auth_response: {list(auth_response)}")
271
+ return False, msg
272
+
273
+ if self.session.new:
274
+ msg.append(
275
+ "Warning: This is a new session and may not have all expected values."
276
+ )
277
+
278
+ if not self.session.get(self.flow_cache_key):
279
+ msg.append(f"<b>Expected '{self.flow_cache_key}' in session</b>")
280
+ msg.append(helpers.html_table(self.session))
281
+
282
+ self.redirect = "/" + self.redirect.lstrip("/")
283
+
284
+ if msg:
285
+ return False, msg
286
+
287
+ try:
288
+ await self.async_acquire_token_by_auth_code_flow(auth_response)
289
+ except Exception as err:
290
+ msg.append(
291
+ "<b>Could not get token</b> - async_acquire_token_by_auth_code_flow"
292
+ )
293
+ msg.append(str(err))
294
+ return False, msg
295
+
296
+ if not msg:
297
+ try:
298
+ if get_info in ("user", "manager"):
299
+ await helpers.get_user_info(self)
300
+ if get_info == "manager":
301
+ await helpers.get_manager_info(self)
302
+ except Exception as err:
303
+ msg.append("Could not get org info from MS graph")
304
+ msg.append(str(err))
305
+ self.mail = ""
306
+ self.name = ""
307
+
308
+ if self.session.get("mail"):
309
+ for lcb in ENV.login_callback:
310
+ await lcb(self)
311
+
312
+ return True, msg
aiohttp_msal/routes.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """The user blueprint."""
2
2
 
3
3
  import time
4
- from collections.abc import Mapping, Sequence
5
4
  from inspect import iscoroutinefunction
6
5
  from typing import Any
7
6
  from urllib.parse import urljoin
@@ -9,9 +8,9 @@ from urllib.parse import urljoin
9
8
  from aiohttp import web
10
9
  from aiohttp_session import get_session, new_session
11
10
 
12
- from aiohttp_msal import _LOGGER, ENV, auth_ok, msal_session
13
- from aiohttp_msal.msal_async import FLOW_CACHE, AsyncMSAL
14
- from aiohttp_msal.user_info import get_manager_info, get_user_info
11
+ from aiohttp_msal import ENV, auth_ok, msal_session
12
+ from aiohttp_msal.helpers import get_manager_info, get_url, get_user_info, html_wrap
13
+ from aiohttp_msal.msal_async import AsyncMSAL
15
14
 
16
15
  ROUTES = web.RouteTableDef()
17
16
 
@@ -20,17 +19,6 @@ URI_USER_AUTHORIZED = "/user/authorized"
20
19
  SESSION_REDIRECT = "redirect"
21
20
 
22
21
 
23
- def get_route(request: web.Request, url: str) -> str:
24
- """Retrieve server route from request.
25
-
26
- localhost and production on http:// with nginx proxy that adds TLS/SSL.
27
- """
28
- url = str(request.url.origin() / url)
29
- if "localhost" not in url:
30
- url = url.replace("p:", "ps:", 1)
31
- return url
32
-
33
-
34
22
  @ROUTES.get(URI_USER_LOGIN)
35
23
  @ROUTES.get(f"{URI_USER_LOGIN}/{{to:.+$}}")
36
24
  async def user_login(request: web.Request) -> web.Response:
@@ -43,85 +31,38 @@ async def user_login(request: web.Request) -> web.Response:
43
31
  _to = "/"
44
32
  session[SESSION_REDIRECT] = urljoin(_to, request.match_info.get("to", ""))
45
33
 
46
- msredirect = get_route(request, URI_USER_AUTHORIZED.lstrip("/"))
34
+ msredirect = get_url(request, URI_USER_AUTHORIZED.lstrip("/"))
47
35
  redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
48
- return web.HTTPFound(redir)
36
+ raise web.HTTPFound(redir)
49
37
 
50
38
 
51
39
  @ROUTES.post(URI_USER_AUTHORIZED)
52
40
  async def user_authorized(request: web.Request) -> web.Response:
53
41
  """Complete the auth code flow."""
54
- session = await get_session(request)
55
-
56
- # build a plain dict from the aiohttp server request's url parameters
57
- # pre-0.1.18. Now we have response_mode="form_post"
58
- # auth_response = dict(request.rel_url.query.items())
59
- auth_response = dict(await request.post())
60
-
61
- msg = []
62
- response_cookie = 0
63
-
64
- # Ensure all expected variables were returned...
65
- if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
66
- msg.append(
67
- f"<b>Expecting code,state,session_state in post body.</b>auth_response: {auth_response}"
68
- )
69
-
70
- if not request.cookies.get(ENV.COOKIE_NAME):
71
- cookies = dict(request.cookies.items())
72
- msg.append(f"<b>Expecting '{ENV.COOKIE_NAME}' in cookies</b>")
73
- _LOGGER.fatal("Cookie should be set with Samesite:None")
74
- msg.append(html_table(cookies))
75
-
76
- elif not session.get(FLOW_CACHE):
77
- msg.append(f"<b>Expecting '{FLOW_CACHE}' in session</b>")
78
- msg.append(f"- Session.new: {session.new}")
79
- msg.append(html_table(session))
80
-
81
- aiomsal = AsyncMSAL(session)
42
+ aiomsal = await AsyncMSAL.from_request(request)
43
+ ok, msg = await aiomsal.async_acquire_token_by_auth_code_flow_plus(request)
82
44
 
83
- if not msg:
45
+ if ok and not msg:
84
46
  try:
85
- await aiomsal.async_acquire_token_by_auth_code_flow(auth_response)
86
- except Exception as err:
87
- msg.append(
88
- "<b>Could not get token</b> - async_acquire_token_by_auth_code_flow"
89
- )
90
- msg.append(str(err))
91
-
92
- if not msg:
93
- session.pop("mail", None)
94
- session.pop("name", None)
95
- try:
96
- await get_user_info(aiomsal)
97
- await get_manager_info(aiomsal)
98
- except Exception as err:
99
- msg.append("Could not get org info from MS graph")
100
- msg.append(str(err))
101
- if session.get("mail"):
102
- for lcb in ENV.login_callback:
103
- await lcb(aiomsal)
104
-
105
- if msg:
106
- resp = web.Response(
107
- body=html_wrap(msg),
108
- content_type="text/html",
47
+ raise web.HTTPFound(aiomsal.redirect)
48
+ finally:
49
+ aiomsal.redirect = ""
50
+
51
+ resp = web.Response(
52
+ body=html_wrap(msg),
53
+ content_type="text/html",
54
+ )
55
+ if aiomsal is None:
56
+ resp.set_cookie(
57
+ ENV.COOKIE_NAME,
58
+ "",
59
+ path="/",
60
+ httponly=True,
61
+ secure=True,
62
+ samesite="Strict",
63
+ domain=ENV.DOMAIN,
109
64
  )
110
- if response_cookie:
111
- resp.set_cookie(
112
- ENV.COOKIE_NAME,
113
- "",
114
- path="/",
115
- httponly=True,
116
- secure=True,
117
- samesite="Strict",
118
- domain=ENV.DOMAIN,
119
- )
120
- return resp
121
-
122
- redirect = session.pop(SESSION_REDIRECT, "") or "/fr/"
123
-
124
- return web.HTTPFound(redirect)
65
+ return resp
125
66
 
126
67
 
127
68
  @ROUTES.get("/user/debug")
@@ -193,7 +134,7 @@ async def user_logout(request: web.Request, ses: AsyncMSAL) -> web.Response:
193
134
  if ref:
194
135
  _to = urljoin(ref, _to)
195
136
  else:
196
- _to = get_route(request, _to)
137
+ _to = get_url(request, _to)
197
138
 
198
139
  return web.HTTPFound(
199
140
  f"https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri={_to}"
@@ -226,33 +167,3 @@ async def user_photo(request: web.Request, ses: AsyncMSAL) -> web.StreamResponse
226
167
 
227
168
  # await response.write_eof()
228
169
  return response
229
-
230
-
231
- def html_table(items: Mapping[Any, Any]) -> str:
232
- """Return a table HTML."""
233
- res = "<table style='width:80%;border:1px solid black;'>"
234
- for key, val in items.items():
235
- res += f"<tr><td>{key}</td><td>{val}</td></tr>"
236
- res += "</table>"
237
- return res
238
-
239
-
240
- def html_wrap(msgs: Sequence[str]) -> str:
241
- """Return proper HTML when login fails."""
242
- html = "</li><li>".join(msgs)
243
- return f"""
244
- <h2>Login failed</h2>
245
-
246
- <p>Retry at <a href='/user/login'>/user/login</a></p>
247
-
248
- <p>Try clearing the cookies for <b>.{ENV.DOMAIN}<b> by navigating to the correct
249
- address for your browser:
250
- <ul>
251
- <li>chrome://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
252
- <li>brave://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
253
- <li>edge://settings/siteData (you will have to search for {ENV.DOMAIN} cookies)</li>
254
- </ul></p>
255
-
256
- <h4>Debug info</h4>
257
- <ul><li>{html}</li></ul>
258
- """
aiohttp_msal/utils.py CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  import asyncio
4
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
-
5
+ from functools import partial, wraps
6
+ from typing import Any, ParamSpec, TypeVar
9
7
 
10
8
  T = TypeVar("T")
11
9
  P = ParamSpec("P")
@@ -31,6 +29,36 @@ def async_wrap(
31
29
  return run
32
30
 
33
31
 
32
+ class dict_property(property):
33
+ """Property."""
34
+
35
+ def __init__(self, dict_name: str, prop_name: str) -> None:
36
+ """Initialize the property."""
37
+ self.dict_name = dict_name
38
+ self.prop_name = prop_name
39
+
40
+ def __get__(self, instance: Any, owner: type | None = None, /) -> Any:
41
+ """Getter."""
42
+ return getattr(instance, self.dict_name, {}).get(self.prop_name, "")
43
+
44
+ def __set__(self, instance: Any, value: Any, /) -> None:
45
+ """Setter."""
46
+ if value == "":
47
+ getattr(instance, self.dict_name, {}).pop(self.prop_name, None)
48
+ else:
49
+ getattr(instance, self.dict_name, {}).__setitem__(self.prop_name, value)
50
+
51
+
52
+ # def dict_property(dict_name: str, prop_name: str) -> property:
53
+ # """Create properties for a dictionary."""
54
+ # return property(
55
+ # fget=lambda self: str(getattr(self, dict_name).get(prop_name, "")),
56
+ # fset=lambda self, v: getattr(self, dict_name).set(prop_name, v),
57
+ # fdel=lambda self: getattr(self, dict_name).pop(prop_name, None),
58
+ # doc=f'self.{dict_name}["{prop_name}"]',
59
+ # )
60
+
61
+
34
62
  def retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
35
63
  """Retry if tenacity is installed."""
36
64
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aiohttp-msal
3
- Version: 1.0.3
3
+ Version: 1.0.5
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
@@ -137,4 +137,6 @@ uv sync --all-extras
137
137
  uv tool install ruff
138
138
  uv tool install codespell
139
139
  uv tool install pyproject-fmt
140
+ uv tool install prek
141
+ prek install # add pre-commit hooks
140
142
  ```
@@ -0,0 +1,11 @@
1
+ aiohttp_msal/__init__.py,sha256=nrjPAIy0kNbBvHZsy9qUSKs5r_-Kiea-KJM17bsBIkw,4375
2
+ aiohttp_msal/helpers.py,sha256=L0DFIZD9OfCepckfk64ZtxrVX0-y0aAqRFowBYipepE,2474
3
+ aiohttp_msal/msal_async.py,sha256=VG5L6PGBHzRBNg_XfIqvfU1V-Snznv_Srd_kXzBgTts,11702
4
+ aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
5
+ aiohttp_msal/routes.py,sha256=Cc6FHqs8dyHSTCC6AaT-yPttSDyBlsngoiz7j9QCKRU,5143
6
+ aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
7
+ aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
8
+ aiohttp_msal/utils.py,sha256=ll303J58nwCEB9QCAm13urUOa6cPqsAE7z_iP9OlRJw,2390
9
+ aiohttp_msal-1.0.5.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
10
+ aiohttp_msal-1.0.5.dist-info/METADATA,sha256=OR8PATJQ8sdTo2vifYRzTmj4iYt9Vcra-CxoADp39Ac,4572
11
+ aiohttp_msal-1.0.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.13
2
+ Generator: uv 0.8.15
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
aiohttp_msal/user_info.py DELETED
@@ -1,32 +0,0 @@
1
- """Graph User Info."""
2
-
3
- from aiohttp_msal.msal_async import AsyncMSAL
4
- from aiohttp_msal.utils import retry
5
-
6
-
7
- @retry
8
- async def get_user_info(aiomsal: AsyncMSAL) -> None:
9
- """Load user info from MS graph API. Requires User.Read permissions."""
10
- async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
11
- body = await res.json()
12
- try:
13
- aiomsal.session["mail"] = body["mail"]
14
- aiomsal.session["name"] = body["displayName"]
15
- except KeyError as err:
16
- raise KeyError(
17
- f"Unexpected return from Graph endpoint: {body}: {err}"
18
- ) from err
19
-
20
-
21
- @retry
22
- async def get_manager_info(aiomsal: AsyncMSAL) -> None:
23
- """Load manager info from MS graph API. Requires User.Read.All permissions."""
24
- async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
25
- body = await res.json()
26
- try:
27
- aiomsal.session["m_mail"] = body["mail"]
28
- aiomsal.session["m_name"] = body["displayName"]
29
- except KeyError as err:
30
- raise KeyError(
31
- f"Unexpected return from Graph endpoint: {body}: {err}"
32
- ) from err
@@ -1,11 +0,0 @@
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,,