aiohttp-msal 1.0.4__py3-none-any.whl → 1.0.6__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
@@ -15,7 +15,7 @@ from aiohttp_msal.msal_async import AsyncMSAL
15
15
  from aiohttp_msal.settings import ENV
16
16
  from aiohttp_msal.utils import retry
17
17
 
18
- _LOGGER = logging.getLogger(__name__)
18
+ _LOG = logging.getLogger(__name__)
19
19
 
20
20
  _T = TypeVar("_T")
21
21
  Ts = TypeVarTuple("Ts")
@@ -107,7 +107,7 @@ async def app_init_redis_session(
107
107
  else:
108
108
  await check_proxy()
109
109
 
110
- _LOGGER.info("Connect to Redis %s", ENV.REDIS)
110
+ _LOG.info("Connect to Redis %s", ENV.REDIS)
111
111
  try:
112
112
  ENV.database = from_url(ENV.REDIS)
113
113
  # , encoding="utf-8", decode_responses=True
aiohttp_msal/helpers.py CHANGED
@@ -1,18 +1,19 @@
1
1
  """Graph User Info."""
2
2
 
3
3
  from collections.abc import Mapping, Sequence
4
- from typing import Any, Literal, TypeVar
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from aiohttp import web
7
- from aiohttp_session import get_session
8
7
 
9
- from aiohttp_msal import ENV
10
- from aiohttp_msal.msal_async import AsyncMSAL
8
+ from aiohttp_msal.settings import ENV
11
9
  from aiohttp_msal.utils import retry
12
10
 
11
+ if TYPE_CHECKING:
12
+ from aiohttp_msal.msal_async import AsyncMSAL
13
+
13
14
 
14
15
  @retry
15
- async def get_user_info(aiomsal: AsyncMSAL) -> None:
16
+ async def get_user_info(aiomsal: "AsyncMSAL") -> None:
16
17
  """Load user info from MS graph API. Requires User.Read permissions."""
17
18
  async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
18
19
  body = await res.json()
@@ -26,7 +27,7 @@ async def get_user_info(aiomsal: AsyncMSAL) -> None:
26
27
 
27
28
 
28
29
  @retry
29
- async def get_manager_info(aiomsal: AsyncMSAL) -> None:
30
+ async def get_manager_info(aiomsal: "AsyncMSAL") -> None:
30
31
  """Load manager info from MS graph API. Requires User.Read.All permissions."""
31
32
  async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
32
33
  body = await res.json()
@@ -69,68 +70,7 @@ def html_wrap(msgs: Sequence[str]) -> str:
69
70
  """
70
71
 
71
72
 
72
- TA = TypeVar("TA", bound=AsyncMSAL)
73
-
74
-
75
- async def check_auth_response(
76
- request: web.Request,
77
- asyncmsal_class: type[TA] = AsyncMSAL, # type:ignore[assignment]
78
- get_info: Literal["user", "manager", ""] = "manager",
79
- ) -> tuple[TA | None, list[str]]:
80
- """Parse the MS auth response."""
81
- # Expecting response_mode="form_post"
82
- auth_response = dict(await request.post())
83
-
84
- msg = list[str]()
85
-
86
- # Ensure all expected variables were returned...
87
- if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
88
- msg.append("Expected 'code', 'session_state', 'state' in auth_response")
89
- msg.append(f"Received auth_response: {list(auth_response)}")
90
- return None, msg
91
-
92
- if not request.cookies.get(ENV.COOKIE_NAME):
93
- cookies = dict(request.cookies.items())
94
- msg.append(f"<b>Expected '{ENV.COOKIE_NAME}' in cookies</b>")
95
- msg.append(html_table(cookies))
96
- msg.append("Cookie should be set with Samesite:None")
97
-
98
- session = await get_session(request)
99
- if session.new:
100
- msg.append(
101
- "Warning: This is a new session and may not have all expected values."
102
- )
103
-
104
- if not session.get(asyncmsal_class.flow_cache_key):
105
- msg.append(f"<b>Expected '{asyncmsal_class.flow_cache_key}' in session</b>")
106
- msg.append(html_table(session))
107
-
108
- aiomsal = asyncmsal_class(session)
109
- aiomsal.redirect = "/" + aiomsal.redirect.lstrip("/")
110
-
111
- if msg:
112
- return aiomsal, msg
113
-
114
- try:
115
- await aiomsal.async_acquire_token_by_auth_code_flow(auth_response)
116
- except Exception as err:
117
- msg.append("<b>Could not get token</b> - async_acquire_token_by_auth_code_flow")
118
- msg.append(str(err))
119
-
120
- if not msg:
121
- try:
122
- if get_info in ("user", "manager"):
123
- await get_user_info(aiomsal)
124
- if get_info == "manager":
125
- await get_manager_info(aiomsal)
126
- except Exception as err:
127
- msg.append("Could not get org info from MS graph")
128
- msg.append(str(err))
129
- aiomsal.mail = ""
130
- aiomsal.name = ""
131
-
132
- if session.get("mail"):
133
- for lcb in ENV.login_callback:
134
- await lcb(aiomsal)
135
-
136
- return aiomsal, msg
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, cast
13
+ from typing import Any, ClassVar, Literal, Self, TypeVar, Unpack, cast
13
14
 
14
15
  import attrs
15
16
  from aiohttp import web
@@ -20,12 +21,15 @@ 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
27
29
  from aiohttp_msal.utils import dict_property
28
30
 
31
+ _LOG = logging.getLogger(__name__)
32
+
29
33
  HttpMethods = Literal["get", "post", "put", "patch", "delete"]
30
34
  HTTP_GET = "get"
31
35
  HTTP_POST = "post"
@@ -34,6 +38,8 @@ HTTP_PATCH = "patch"
34
38
  HTTP_DELETE = "delete"
35
39
  HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
36
40
 
41
+ T = TypeVar("T")
42
+
37
43
 
38
44
  @attrs.define(slots=False)
39
45
  class AsyncMSAL:
@@ -54,14 +60,46 @@ class AsyncMSAL:
54
60
  """
55
61
  app_kwargs: dict[str, Any] | None = None
56
62
  """ConfidentialClientApplication kwargs."""
57
- client_session: ClassVar[ClientSession | None] = None
58
63
 
64
+ client_session: ClassVar[ClientSession | None] = None
59
65
  token_cache_key: ClassVar[str] = "token_cache"
60
66
  user_email_key: ClassVar[str] = "mail"
61
67
  flow_cache_key: ClassVar[str] = "flow_cache"
62
- redirect_key = "redirect"
68
+ redirect_key: ClassVar[str] = "redirect"
63
69
  default_scopes: ClassVar[list[str]] = ["User.Read", "User.Read.All"]
64
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'"
96
+
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
102
+
65
103
  @cached_property
66
104
  def app(self) -> ConfidentialClientApplication:
67
105
  """Get the app."""
@@ -118,7 +156,7 @@ class AsyncMSAL:
118
156
  ) -> None:
119
157
  """Second step - Acquire token."""
120
158
  # Assume we have it in the cache (added by /login)
121
- # will raise keryerror if no cache
159
+ # will raise KeyError if not in cache
122
160
  auth_code_flow = self.session.pop(self.flow_cache_key)
123
161
  result = self.app.acquire_token_by_auth_code_flow(
124
162
  auth_code_flow, auth_response, scopes=scopes
@@ -208,3 +246,67 @@ class AsyncMSAL:
208
246
  manager_name = dict_property("session", "m_name")
209
247
  manager_mail = dict_property("session", "m_mail")
210
248
  redirect = dict_property("session", redirect_key)
249
+
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.
256
+
257
+ Parse the auth response from the request, checks for valid keys,
258
+ acquire the token and get_info.
259
+
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
@@ -6,14 +6,14 @@ import logging
6
6
  import time
7
7
  from collections.abc import AsyncGenerator
8
8
  from contextlib import AsyncExitStack, asynccontextmanager
9
- from typing import Any
9
+ from typing import Any, TypeVar
10
10
 
11
11
  from redis.asyncio import Redis, from_url
12
12
 
13
13
  from aiohttp_msal.msal_async import AsyncMSAL
14
14
  from aiohttp_msal.settings import ENV as MENV
15
15
 
16
- _LOGGER = logging.getLogger(__name__)
16
+ _LOG = logging.getLogger(__name__)
17
17
 
18
18
  SES_KEYS = ("mail", "name", "m_mail", "m_name")
19
19
 
@@ -22,10 +22,10 @@ SES_KEYS = ("mail", "name", "m_mail", "m_name")
22
22
  async def get_redis() -> AsyncGenerator[Redis, None]:
23
23
  """Get a Redis connection."""
24
24
  if MENV.database:
25
- _LOGGER.debug("Using redis from environment")
25
+ _LOG.debug("Using redis from environment")
26
26
  yield MENV.database
27
27
  return
28
- _LOGGER.info("Connect to Redis %s", MENV.REDIS)
28
+ _LOG.info("Connect to Redis %s", MENV.REDIS)
29
29
  redis = from_url(MENV.REDIS) # decode_responses=True not allowed aiohttp_session
30
30
  MENV.database = redis
31
31
  try:
@@ -37,6 +37,7 @@ async def get_redis() -> AsyncGenerator[Redis, None]:
37
37
 
38
38
  async def session_iter(
39
39
  redis: Redis,
40
+ /,
40
41
  *,
41
42
  match: dict[str, str] | None = None,
42
43
  key_match: str | None = None,
@@ -74,7 +75,7 @@ async def session_iter(
74
75
 
75
76
 
76
77
  async def session_clean(
77
- redis: Redis, *, max_age: int = 90, expected_keys: dict[str, Any] | None = None
78
+ redis: Redis, /, *, max_age: int = 90, expected_keys: dict[str, Any] | None = None
78
79
  ) -> None:
79
80
  """Clear session entries older than max_age days."""
80
81
  rem, keep = 0, 0
@@ -89,12 +90,12 @@ async def session_clean(
89
90
  keep += 1
90
91
  finally:
91
92
  if rem:
92
- _LOGGER.info("Sessions removed: %s (%s total)", rem, keep)
93
+ _LOG.info("Sessions removed: %s (%s total)", rem, keep)
93
94
  else:
94
- _LOGGER.debug("No sessions removed (%s total)", keep)
95
+ _LOG.debug("No sessions removed (%s total)", keep)
95
96
 
96
97
 
97
- async def invalid_sessions(redis: Redis) -> None:
98
+ async def invalid_sessions(redis: Redis, /) -> None:
98
99
  """Find & clean invalid sessions."""
99
100
  async for key in redis.scan_iter(count=100, match=f"{MENV.COOKIE_NAME}*"):
100
101
  if not isinstance(key, str):
@@ -107,12 +108,17 @@ async def invalid_sessions(redis: Redis) -> None:
107
108
  assert isinstance(val["created"], int)
108
109
  assert isinstance(val["session"], dict)
109
110
  except Exception as err:
110
- _LOGGER.warning("Removing session %s: %s", key, err)
111
+ _LOG.warning("Removing session %s: %s", key, err)
111
112
  await redis.delete(key)
112
113
 
113
114
 
114
- def _session_factory(key: str, created: int, session: dict) -> AsyncMSAL:
115
- """Create a AsyncMSAL session.
115
+ T = TypeVar("T", bound=AsyncMSAL)
116
+
117
+
118
+ def async_msal_factory(
119
+ cls: type[T], key: str, created: int, session: dict[str, Any], /
120
+ ) -> T:
121
+ """Create a AsyncMSAL session with a save_callback.
116
122
 
117
123
  When get_token refreshes the token retrieved from Redis, the save_cache callback
118
124
  will be responsible to update the cache in Redis.
@@ -130,12 +136,17 @@ def _session_factory(key: str, created: int, session: dict) -> AsyncMSAL:
130
136
  except RuntimeError:
131
137
  asyncio.run(async_save_cache(*args))
132
138
 
133
- return AsyncMSAL(session, save_callback=save_cache)
139
+ return cls(session, save_callback=save_cache)
134
140
 
135
141
 
136
142
  async def get_session(
137
- email: str, *, redis: Redis | None = None, scope: str = ""
138
- ) -> AsyncMSAL:
143
+ cls: type[T],
144
+ email: str,
145
+ /,
146
+ *,
147
+ redis: Redis | None = None,
148
+ scope: str = "",
149
+ ) -> T:
139
150
  """Get a session from Redis."""
140
151
  cnt = 0
141
152
  async with AsyncExitStack() as stack:
@@ -143,22 +154,22 @@ async def get_session(
143
154
  redis = await stack.enter_async_context(get_redis())
144
155
  async for key, created, session in session_iter(redis, match={"mail": email}):
145
156
  cnt += 1
146
- if scope and scope not in str(session.get("token_cache")).lower():
157
+ if scope and scope not in str(session.get(cls.token_cache_key)).lower():
147
158
  continue
148
- return _session_factory(key, created, session)
159
+ return async_msal_factory(cls, key, created, session)
149
160
  msg = f"Session for {email}"
150
161
  if not scope:
151
162
  raise ValueError(f"{msg} not found")
152
163
  raise ValueError(f"{msg} with scope {scope} not found ({cnt} checked)")
153
164
 
154
165
 
155
- async def redis_get_json(key: str) -> list | dict | None:
166
+ async def redis_get_json(key: str) -> list[str] | dict[str, Any] | None:
156
167
  """Get a key from redis."""
157
168
  res = await MENV.database.get(key)
158
169
  if isinstance(res, str | bytes | bytearray):
159
170
  return json.loads(res)
160
171
  if res is not None:
161
- _LOGGER.warning("Unexpected type for %s: %s", key, type(res))
172
+ _LOG.warning("Unexpected type for %s: %s", key, type(res))
162
173
  return None
163
174
 
164
175
 
@@ -170,7 +181,7 @@ async def redis_get(key: str) -> str | None:
170
181
  if isinstance(res, bytes | bytearray):
171
182
  return res.decode()
172
183
  if res is not None:
173
- _LOGGER.warning("Unexpected type for %s: %s", key, type(res))
184
+ _LOG.warning("Unexpected type for %s: %s", key, type(res))
174
185
  return None
175
186
 
176
187
 
@@ -182,16 +193,16 @@ async def redis_set_set(key: str, new_set: set[str]) -> None:
182
193
  )
183
194
  dif = list(cur_set - new_set)
184
195
  if dif:
185
- _LOGGER.warning("%s: removing %s", key, dif)
196
+ _LOG.warning("%s: removing %s", key, dif)
186
197
  await MENV.database.srem(key, *dif)
187
198
 
188
199
  dif = list(new_set - cur_set)
189
200
  if dif:
190
- _LOGGER.info("%s: adding %s", key, dif)
201
+ _LOG.info("%s: adding %s", key, dif)
191
202
  await MENV.database.sadd(key, *dif)
192
203
 
193
204
 
194
- async def redis_scan(match_str: str) -> list[str]:
205
+ async def redis_scan_keys(match_str: str) -> list[str]:
195
206
  """Return a list of matching keys."""
196
207
  return [
197
208
  s if isinstance(s, str) else s.decode()
aiohttp_msal/routes.py CHANGED
@@ -9,12 +9,7 @@ from aiohttp import web
9
9
  from aiohttp_session import get_session, new_session
10
10
 
11
11
  from aiohttp_msal import ENV, auth_ok, msal_session
12
- from aiohttp_msal.helpers import (
13
- check_auth_response,
14
- get_manager_info,
15
- get_user_info,
16
- html_wrap,
17
- )
12
+ from aiohttp_msal.helpers import get_manager_info, get_url, get_user_info, html_wrap
18
13
  from aiohttp_msal.msal_async import AsyncMSAL
19
14
 
20
15
  ROUTES = web.RouteTableDef()
@@ -24,17 +19,6 @@ URI_USER_AUTHORIZED = "/user/authorized"
24
19
  SESSION_REDIRECT = "redirect"
25
20
 
26
21
 
27
- def get_route(request: web.Request, url: str) -> str:
28
- """Retrieve server route from request.
29
-
30
- localhost and production on http:// with nginx proxy that adds TLS/SSL.
31
- """
32
- url = str(request.url.origin() / url)
33
- if "localhost" not in url:
34
- url = url.replace("http:", "https:", 1)
35
- return url
36
-
37
-
38
22
  @ROUTES.get(URI_USER_LOGIN)
39
23
  @ROUTES.get(f"{URI_USER_LOGIN}/{{to:.+$}}")
40
24
  async def user_login(request: web.Request) -> web.Response:
@@ -47,7 +31,7 @@ async def user_login(request: web.Request) -> web.Response:
47
31
  _to = "/"
48
32
  session[SESSION_REDIRECT] = urljoin(_to, request.match_info.get("to", ""))
49
33
 
50
- msredirect = get_route(request, URI_USER_AUTHORIZED.lstrip("/"))
34
+ msredirect = get_url(request, URI_USER_AUTHORIZED.lstrip("/"))
51
35
  redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
52
36
  raise web.HTTPFound(redir)
53
37
 
@@ -55,9 +39,10 @@ async def user_login(request: web.Request) -> web.Response:
55
39
  @ROUTES.post(URI_USER_AUTHORIZED)
56
40
  async def user_authorized(request: web.Request) -> web.Response:
57
41
  """Complete the auth code flow."""
58
- aiomsal, msg = await check_auth_response(request, AsyncMSAL)
42
+ aiomsal = await AsyncMSAL.from_request(request)
43
+ ok, msg = await aiomsal.async_acquire_token_by_auth_code_flow_plus(request)
59
44
 
60
- if aiomsal and not msg:
45
+ if ok and not msg:
61
46
  try:
62
47
  raise web.HTTPFound(aiomsal.redirect)
63
48
  finally:
@@ -149,7 +134,7 @@ async def user_logout(request: web.Request, ses: AsyncMSAL) -> web.Response:
149
134
  if ref:
150
135
  _to = urljoin(ref, _to)
151
136
  else:
152
- _to = get_route(request, _to)
137
+ _to = get_url(request, _to)
153
138
 
154
139
  return web.HTTPFound(
155
140
  f"https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri={_to}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aiohttp-msal
3
- Version: 1.0.4
3
+ Version: 1.0.6
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=bCBQhbjM5IHKnHKzSDkHQ8Lk_YgW3flO11dgaDn8-Yg,4369
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=vAA3pvMT8jXd1sDL7wUZ8Z1FWO0xfHTWzi3_pnA_f8E,6672
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.6.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
10
+ aiohttp_msal-1.0.6.dist-info/METADATA,sha256=YvbKSl1STeQhHmyb9n9vKpsoHfalctOKStQIA_NZHNk,4572
11
+ aiohttp_msal-1.0.6.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
@@ -1,11 +0,0 @@
1
- aiohttp_msal/__init__.py,sha256=nrjPAIy0kNbBvHZsy9qUSKs5r_-Kiea-KJM17bsBIkw,4375
2
- aiohttp_msal/helpers.py,sha256=8sW-7FW4t1Ztazql11ng67SqdKBlbOWuOMjOBRfBc_c,4495
3
- aiohttp_msal/msal_async.py,sha256=RUV_XlhNNCOZslT1sEkEPyaYcHDOmsIyiGmRpZuA3I0,8097
4
- aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
5
- aiohttp_msal/routes.py,sha256=3rAejWcHfL5czVA91TY3wZGT5EkRxmfyvc8vr6rhyxU,5438
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.4.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
10
- aiohttp_msal-1.0.4.dist-info/METADATA,sha256=OBl_U9OTy602OfSL9-d5myNJPUYuaGjtBF7voW5lu2Y,4514
11
- aiohttp_msal-1.0.4.dist-info/RECORD,,