aiohttp-msal 0.6.6__py3-none-any.whl → 0.6.8__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,8 +1,9 @@
1
1
  """aiohttp_msal."""
2
+
2
3
  import logging
4
+ import typing
3
5
  from functools import wraps
4
6
  from inspect import getfullargspec, iscoroutinefunction
5
- from typing import Any, Awaitable, Callable, Union
6
7
 
7
8
  from aiohttp import ClientSession, web
8
9
  from aiohttp_session import get_session
@@ -13,26 +14,29 @@ from aiohttp_msal.settings import ENV
13
14
 
14
15
  _LOGGER = logging.getLogger(__name__)
15
16
 
16
- VERSION = "0.6.6"
17
+ VERSION = "0.6.8"
17
18
 
18
19
 
19
- def msal_session(*args: Callable[[AsyncMSAL], Union[Any, Awaitable[Any]]]) -> Callable:
20
+ def msal_session(
21
+ *callbacks: typing.Callable[[AsyncMSAL], bool | typing.Awaitable[bool]],
22
+ at_least_one: bool | None = False,
23
+ ) -> typing.Callable:
20
24
  """Session decorator.
21
25
 
22
26
  Arguments can include a list of function to perform login tests etc.
23
27
  """
24
28
 
25
- def _session(func: Callable) -> Callable:
29
+ def _session(func: typing.Callable) -> typing.Callable:
26
30
  @wraps(func)
27
- async def __session(request: web.Request) -> Callable:
28
- _ses = AsyncMSAL(session=await get_session(request))
29
- for arg in args:
30
- if iscoroutinefunction(arg):
31
- if not await arg(_ses):
32
- raise web.HTTPForbidden
33
- elif not arg(_ses):
31
+ async def __session(request: web.Request) -> typing.Callable:
32
+ ses = AsyncMSAL(session=await get_session(request))
33
+ for c_b in callbacks:
34
+ _ok = await c_b(ses) if iscoroutinefunction(c_b) else c_b(ses)
35
+ if at_least_one and _ok:
36
+ break
37
+ if not at_least_one and not _ok:
34
38
  raise web.HTTPForbidden
35
- return await func(request=request, ses=_ses)
39
+ return await func(request=request, ses=ses)
36
40
 
37
41
  assert iscoroutinefunction(func), f"Function needs to be a coroutine: {func}"
38
42
  spec = getfullargspec(func)
@@ -4,10 +4,11 @@ The AsyncMSAL class contains more info to perform OAuth & get the required token
4
4
  Once you have the OAuth tokens store in the session, you are free to make requests
5
5
  (typically from an aiohttp server's inside a request)
6
6
  """
7
+
7
8
  import asyncio
8
9
  import json
9
10
  from functools import partial, wraps
10
- from typing import Any, Callable, Optional, Union
11
+ from typing import Any, Callable, Literal
11
12
 
12
13
  from aiohttp import web
13
14
  from aiohttp.client import ClientResponse, ClientSession, _RequestContextManager
@@ -32,7 +33,7 @@ def async_wrap(func: Callable) -> Callable:
32
33
  @wraps(func)
33
34
  async def run(
34
35
  *args: Any,
35
- loop: Optional[asyncio.AbstractEventLoop] = None,
36
+ loop: asyncio.AbstractEventLoop | None = None,
36
37
  executor: Any = None,
37
38
  **kwargs: dict[str, Any],
38
39
  ) -> Callable:
@@ -53,44 +54,12 @@ USER_EMAIL = "mail"
53
54
  class AsyncMSAL:
54
55
  """AsycMSAL class.
55
56
 
56
- AsyncIO based OAuth using the Microsoft Authentication Library (MSAL) for Python.
57
- Blocking MSAL functions are executed in the executor thread.
58
- Use until such time as MSAL Python gets a true async version...
59
-
60
- Tested with MSAL Python 1.13.0
61
- https://github.com/AzureAD/microsoft-authentication-library-for-python
62
-
63
- AsyncMSAL is based on the following example app
64
- https://github.com/Azure-Samples/ms-identity-python-webapp/blob/master/app.py#L76
65
-
66
- Use as follows:
67
-
68
- Get the tokens via oauth
69
-
70
- 1. initiate_auth_code_flow
71
- https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.initiate_auth_code_flow
72
-
73
- The caller is expected to:
74
- 1. somehow store this content, typically inside the current session of the
75
- server,
76
- 2. guide the end user (i.e. resource owner) to visit that auth_uri,
77
- typically with a redirect
78
- 3. and then relay this dict and subsequent auth response to
79
- acquire_token_by_auth_code_flow().
80
-
81
- [1. and part of 3.] is stored by this class in the aiohttp_session
82
-
83
- 2. acquire_token_by_auth_code_flow
84
- https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.acquire_token_by_auth_code_flow
85
-
86
-
87
- Now you are free to make requests (typically from an aiohttp server)
88
-
89
- session = await get_session(request)
90
- aiomsal = AsyncMSAL(session)
91
- async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
92
- res = await res.json()
57
+ Authorization Code Flow Helper. Learn more about auth-code-flow at
58
+ https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
93
59
 
60
+ Async based OAuth using the Microsoft Authentication Library (MSAL) for Python.
61
+ Blocking MSAL functions are executed in the executor thread.
62
+ Use until such time as MSAL Python gets a true async version.
94
63
  """
95
64
 
96
65
  _token_cache: SerializableTokenCache = None
@@ -99,8 +68,8 @@ class AsyncMSAL:
99
68
 
100
69
  def __init__(
101
70
  self,
102
- session: Union[Session, dict[str, str]],
103
- save_cache: Optional[Callable[[Union[Session, dict[str, str]]], None]] = None,
71
+ session: Session | dict[str, str],
72
+ save_cache: Callable[[Session | dict[str, str]], None] | None = None,
104
73
  ):
105
74
  """Init the class.
106
75
 
@@ -149,7 +118,11 @@ class AsyncMSAL:
149
118
  self.save_token_cache(self.token_cache)
150
119
 
151
120
  def build_auth_code_flow(
152
- self, redirect_uri: str, scopes: Optional[list[str]] = None
121
+ self,
122
+ redirect_uri: str,
123
+ scopes: list[str] | None = None,
124
+ prompt: Literal["login", "consent", "select_account", "none"] | None = None,
125
+ **kwargs: Any,
153
126
  ) -> str:
154
127
  """First step - Start the flow."""
155
128
  self.session[TOKEN_CACHE] = None # type: ignore
@@ -157,7 +130,9 @@ class AsyncMSAL:
157
130
  self.session[FLOW_CACHE] = res = self.app.initiate_auth_code_flow(
158
131
  scopes or DEFAULT_SCOPES,
159
132
  redirect_uri=redirect_uri,
160
- response_mode="form_post"
133
+ response_mode="form_post",
134
+ prompt=prompt,
135
+ **kwargs,
161
136
  # max_age=1209600,
162
137
  # max allowed 86400 - 1 day
163
138
  )
@@ -185,7 +160,7 @@ class AsyncMSAL:
185
160
  None, self.acquire_token_by_auth_code_flow, auth_response
186
161
  )
187
162
 
188
- def get_token(self, scopes: Optional[list[str]] = None) -> Optional[dict[str, Any]]:
163
+ def get_token(self, scopes: list[str] | None = None) -> dict[str, Any] | None:
189
164
  """Acquire a token based on username."""
190
165
  accounts = self.app.get_accounts()
191
166
  if accounts:
@@ -196,7 +171,7 @@ class AsyncMSAL:
196
171
  return result
197
172
  return None
198
173
 
199
- async def async_get_token(self) -> Optional[dict[str, Any]]:
174
+ async def async_get_token(self) -> dict[str, Any] | None:
200
175
  """Acquire a token based on username."""
201
176
  return await asyncio.get_event_loop().run_in_executor(None, self.get_token)
202
177
 
@@ -1,4 +1,5 @@
1
1
  """Redis tools for sessions."""
2
+
2
3
  import asyncio
3
4
  import json
4
5
  import logging
@@ -9,7 +10,7 @@ from typing import Any, AsyncGenerator, Optional
9
10
  from redis.asyncio import Redis, from_url
10
11
 
11
12
  from aiohttp_msal.msal_async import AsyncMSAL
12
- from aiohttp_msal.settings import ENV
13
+ from aiohttp_msal.settings import ENV as MENV
13
14
 
14
15
  _LOGGER = logging.getLogger(__name__)
15
16
 
@@ -19,15 +20,17 @@ SES_KEYS = ("mail", "name", "m_mail", "m_name")
19
20
  @asynccontextmanager
20
21
  async def get_redis() -> AsyncGenerator[Redis, None]:
21
22
  """Get a Redis connection."""
22
- if ENV.database:
23
+ if MENV.database:
23
24
  _LOGGER.debug("Using redis from environment")
24
- yield ENV.database
25
+ yield MENV.database
25
26
  return
26
- _LOGGER.info("Connect to Redis %s", ENV.REDIS)
27
- redis = from_url(ENV.REDIS)
27
+ _LOGGER.info("Connect to Redis %s", MENV.REDIS)
28
+ redis = from_url(MENV.REDIS) # decode_responses=True not allowed aiohttp_session
29
+ MENV.database = redis
28
30
  try:
29
31
  yield redis
30
32
  finally:
33
+ MENV.database = None # type:ignore
31
34
  await redis.close()
32
35
 
33
36
 
@@ -45,10 +48,11 @@ async def session_iter(
45
48
  if match and not all(isinstance(v, str) for v in match.values()):
46
49
  raise ValueError("match values must be strings")
47
50
  async for key in redis.scan_iter(
48
- count=100, match=key_match or f"{ENV.COOKIE_NAME}*"
51
+ count=100, match=key_match or f"{MENV.COOKIE_NAME}*"
49
52
  ):
53
+ if not isinstance(key, str):
54
+ key = key.decode()
50
55
  sval = await redis.get(key)
51
- _LOGGER.debug("Session: %s = %s", key, sval)
52
56
  created, ses = 0, {}
53
57
  try:
54
58
  val = json.loads(sval) # type: ignore
@@ -110,11 +114,67 @@ def _session_factory(key: str, created: str, session: dict) -> AsyncMSAL:
110
114
  return AsyncMSAL(session, save_cache=save_cache)
111
115
 
112
116
 
113
- async def get_session(email: str, *, redis: Optional[Redis] = None) -> AsyncMSAL:
117
+ async def get_session(
118
+ email: str, *, redis: Optional[Redis] = None, scope: str = ""
119
+ ) -> AsyncMSAL:
114
120
  """Get a session from Redis."""
121
+ cnt = 0
115
122
  async with AsyncExitStack() as stack:
116
123
  if redis is None:
117
124
  redis = await stack.enter_async_context(get_redis())
118
125
  async for key, created, session in session_iter(redis, match={"mail": email}):
126
+ cnt += 1
127
+ if scope and scope not in str(session.get("token_cache")).lower():
128
+ continue
119
129
  return _session_factory(key, str(created), session)
120
- raise ValueError(f"Session for {email} not found")
130
+ msg = f"Session for {email}"
131
+ if not scope:
132
+ raise ValueError(f"{msg} not found")
133
+ raise ValueError(f"{msg} with scope {scope} not found ({cnt} checked)")
134
+
135
+
136
+ async def redis_get_json(key: str) -> list | dict | None:
137
+ """Get a key from redis."""
138
+ res = await MENV.database.get(key)
139
+ if isinstance(res, (str, bytes, bytearray)):
140
+ return json.loads(res)
141
+ if res is not None:
142
+ _LOGGER.warning("Unexpected type for %s: %s", key, type(res))
143
+ return None
144
+
145
+
146
+ async def redis_get(key: str) -> str | None:
147
+ """Get a key from redis."""
148
+ res = await MENV.database.get(key)
149
+ if isinstance(res, str):
150
+ return res
151
+ if isinstance(res, (bytes, bytearray)):
152
+ return res.decode()
153
+ if res is not None:
154
+ _LOGGER.warning("Unexpected type for %s: %s", key, type(res))
155
+ return None
156
+
157
+
158
+ async def redis_set_set(key: str, new_set: set[str]) -> None:
159
+ """Set the value of a set in redis."""
160
+ cur_set = set(
161
+ s if isinstance(s, str) else s.decode()
162
+ for s in await MENV.database.smembers(key)
163
+ )
164
+ dif = list(cur_set - new_set)
165
+ if dif:
166
+ _LOGGER.warning("%s: removing %s", key, dif)
167
+ await MENV.database.srem(key, *dif)
168
+
169
+ dif = list(new_set - cur_set)
170
+ if dif:
171
+ _LOGGER.info("%s: adding %s", key, dif)
172
+ await MENV.database.sadd(key, *dif)
173
+
174
+
175
+ async def redis_scan(match_str: str) -> list[str]:
176
+ """Return a list of matching keys."""
177
+ return [
178
+ s if isinstance(s, str) else s.decode()
179
+ async for s in MENV.database.scan_iter(match=match_str)
180
+ ]
aiohttp_msal/routes.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """The user blueprint."""
2
+
2
3
  import time
3
4
  from inspect import iscoroutinefunction
4
5
  from typing import Any, Mapping, Sequence
aiohttp_msal/settings.py CHANGED
@@ -1,8 +1,14 @@
1
1
  """Settings."""
2
- from typing import Any, Awaitable, Callable, Union
2
+
3
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable
3
4
 
4
5
  from aiohttp_msal.settings_base import SettingsBase, Var
5
6
 
7
+ if TYPE_CHECKING:
8
+ from redis.asyncio import Redis
9
+ else:
10
+ Redis = Any
11
+
6
12
 
7
13
  class MSALSettings(SettingsBase):
8
14
  """Settings."""
@@ -26,12 +32,12 @@ class MSALSettings(SettingsBase):
26
32
 
27
33
  login_callback: list[Callable[[Any], Awaitable[Any]]] = []
28
34
  """A list of callbacks to execute on successful login."""
29
- info: dict[str, Callable[[Any], Union[Any, Awaitable[Any]]]] = {}
35
+ info: dict[str, Callable[[Any], Any | Awaitable[Any]]] = {}
30
36
  """List of attributes to return in /user/info."""
31
37
 
32
38
  REDIS = "redis://redis1:6379"
33
39
  """OPTIONAL: Redis database connection used by app_init_redis_session()."""
34
- database: Any = None
40
+ database: Redis
35
41
  """Store the Redis connection when using app_init_redis_session()."""
36
42
 
37
43
 
@@ -1,4 +1,6 @@
1
1
  """Settings Base."""
2
+
3
+ from __future__ import annotations
2
4
  import logging
3
5
  import os
4
6
  from pathlib import Path
@@ -9,7 +11,7 @@ class Var: # pylint: disable=too-few-public-methods
9
11
  """Variable settings."""
10
12
 
11
13
  @staticmethod
12
- def from_value(val: Any): # type: ignore
14
+ def from_value(val: Any) -> Var:
13
15
  """Ensure the return is an instance of Var."""
14
16
  return val if isinstance(val, Var) else Var(type(val))
15
17
 
aiohttp_msal/user_info.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Graph User Info."""
2
+
2
3
  import asyncio
3
4
  from functools import wraps
4
5
  from typing import Any, Callable
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2022-2023 kellerza
3
+ Copyright (c) 2022-2024 kellerza
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: aiohttp-msal
3
- Version: 0.6.6
2
+ Name: aiohttp_msal
3
+ Version: 0.6.8
4
4
  Summary: Helper Library to use the Microsoft Authentication Library (MSAL) with aiohttp
5
5
  Home-page: https://github.com/kellerza/aiohttp_msal
6
6
  Author: Johann Kellerman
@@ -12,20 +12,20 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
- Requires-Python: >=3.9
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: msal >=1.24.1
21
+ Requires-Dist: msal >=1.30.0
22
22
  Requires-Dist: aiohttp-session >=2.12
23
23
  Requires-Dist: aiohttp >=3.8
24
24
  Provides-Extra: redis
25
25
  Requires-Dist: aiohttp-session[aioredis] >=2.12 ; extra == 'redis'
26
26
  Provides-Extra: tests
27
- Requires-Dist: black ==23.9.1 ; extra == 'tests'
28
- Requires-Dist: pylint ; extra == 'tests'
27
+ Requires-Dist: black ==24.8.0 ; extra == 'tests'
28
+ Requires-Dist: pylint ==3.2.6 ; extra == 'tests'
29
29
  Requires-Dist: flake8 ; extra == 'tests'
30
30
  Requires-Dist: pytest-aiohttp ; extra == 'tests'
31
31
  Requires-Dist: pytest ; extra == 'tests'
@@ -35,9 +35,13 @@ Requires-Dist: pytest-env ; extra == 'tests'
35
35
 
36
36
  # aiohttp_msal Python library
37
37
 
38
- AsyncIO based OAuth using the Microsoft Authentication Library (MSAL) for Python.
38
+ Authorization Code Flow Helper. Learn more about auth-code-flow at
39
+ <https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow>
39
40
 
40
- Blocking MSAL functions are executed in the executor thread. Should be useful until such time as MSAL Python gets a true async version...
41
+ Async based OAuth using the Microsoft Authentication Library (MSAL) for Python.
42
+
43
+ Blocking MSAL functions are executed in the executor thread.
44
+ Should be useful until such time as MSAL Python gets a true async version.
41
45
 
42
46
  Tested with MSAL Python 1.21.0 onward - [MSAL Python docs](https://github.com/AzureAD/microsoft-authentication-library-for-python)
43
47
 
@@ -0,0 +1,18 @@
1
+ aiohttp_msal/__init__.py,sha256=vvc6_F-5jkV_Fy2il81x-kFCrwhZvBMfZ9rNPhkx4ts,3103
2
+ aiohttp_msal/msal_async.py,sha256=afvfh7gZrXk5KO7Umb9jAnQu4jdg_iVlgaTnxS3JgNM,8899
3
+ aiohttp_msal/redis_tools.py,sha256=zgRACVxm2wPkbEHtA6VmArsd-QQlKn-crlq1XlFbjEY,5919
4
+ aiohttp_msal/routes.py,sha256=JNz0tn-avHUiUmI-YeprY7XyVQNnHRdb2kQFynCTBy0,8228
5
+ aiohttp_msal/settings.py,sha256=AriMUJnTzNkxbwtg8tjxAuM4uPxtJFx26RA3-YvYp1U,1318
6
+ aiohttp_msal/settings_base.py,sha256=m4tmurnq8xipVNAa-Dh4ii9Rsu6gg39F4aDJNHPLwiI,2919
7
+ aiohttp_msal/user_info.py,sha256=fijBUbl5g1AVgrpOl-2ZY-eQCCWcu4YqcA0QaMQrcWw,1766
8
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ tests/test_init.py,sha256=olI80lEHoG9Bf2owq3lyfJHp6OPYdsoN_jzlrNY0GYk,76
10
+ tests/test_msal_async.py,sha256=7-G6dO3_qWb8zxqC6_SqMMubCBbEERZy513P7UM4vmw,365
11
+ tests/test_redis_tools.py,sha256=uFpPSe6atbDVAuh1_OUtFgeZwyuLDspp42_EECJDSPg,1869
12
+ tests/test_settings.py,sha256=z-qtUs1zl5Q9NEux051eebyPnArLZ_OfZu65FKz0N4Y,333
13
+ aiohttp_msal-0.6.8.dist-info/LICENSE,sha256=BwqFEcF0Ij49hDZx4A_5CzsKnfU_twRjrm87JFwydFc,1080
14
+ aiohttp_msal-0.6.8.dist-info/METADATA,sha256=brsm0akO5VVzEW7FDe-GxKCw8Bl1WiQadjxq5cVpYXA,4724
15
+ aiohttp_msal-0.6.8.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
16
+ aiohttp_msal-0.6.8.dist-info/top_level.txt,sha256=QPWOi5JtacVEdbaU5bJExc9o-cCT2Lufx0QhUpsv5_E,19
17
+ aiohttp_msal-0.6.8.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
18
+ aiohttp_msal-0.6.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/test_init.py CHANGED
@@ -1,3 +1,4 @@
1
1
  """Init."""
2
+
2
3
  import aiohttp_msal # noqa
3
4
  import aiohttp_msal.routes # noqa
tests/test_msal_async.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Test the AsyncMSAL class."""
2
+
2
3
  from aiohttp_msal.msal_async import AsyncMSAL, Session
3
4
 
4
5
 
tests/test_redis_tools.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Test redis tools."""
2
+
2
3
  from json import dumps
3
4
  from typing import AsyncGenerator
4
5
  from unittest.mock import AsyncMock, MagicMock, Mock, call
@@ -1,18 +0,0 @@
1
- aiohttp_msal/__init__.py,sha256=R-OEq-FJnT_uYiiSYeltrUqTSnPTcFqR8SKSbVszues,3025
2
- aiohttp_msal/msal_async.py,sha256=HjLJ5gOengQNjuN1Nod1U7J4sqChTN_B4QuXjVJkheA,9964
3
- aiohttp_msal/redis_tools.py,sha256=3Zyl3-sdT_SgxACdWeeBtWGywBzwyF9Ekm9CC2oCb90,4082
4
- aiohttp_msal/routes.py,sha256=UmUQkFFubFK2N1IAryDPwN7UBMCgow87H6zhNbKiE1I,8227
5
- aiohttp_msal/settings.py,sha256=Nz0GO62IMtQdfdZqJ8o8w331i1sxfGuaIo4j533u4NM,1243
6
- aiohttp_msal/settings_base.py,sha256=pmVmzTtaGEgRh-AMGy0HdhF1JvoZhZp42G3PL_ILHLw,2892
7
- aiohttp_msal/user_info.py,sha256=02DYVbP8lnLNCm1HQEGZY7tktWRHvEAuHf6fLj2Kcp8,1765
8
- tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- tests/test_init.py,sha256=sHmt7yNDlcu5JHrpM_gim0NeLce0NwUAMM3HAdGoo58,75
10
- tests/test_msal_async.py,sha256=Vi2Jx8_GG-iMfKPY4UCvmX-0U1Bl2xswrAyWGIRP-Ac,364
11
- tests/test_redis_tools.py,sha256=x9Yd9WOGd2ZW5fJwyiAta5wMRkSM3prsCPhkVe6CS3Q,1868
12
- tests/test_settings.py,sha256=z-qtUs1zl5Q9NEux051eebyPnArLZ_OfZu65FKz0N4Y,333
13
- aiohttp_msal-0.6.6.dist-info/LICENSE,sha256=H1aGfkSfZFwK3q4INn9mUldOJGZy-ZXu5-65K9Glunw,1080
14
- aiohttp_msal-0.6.6.dist-info/METADATA,sha256=XDKJEJXQkOyLveVhob8NukGwprF14ZRLX_geNu8rJm4,4565
15
- aiohttp_msal-0.6.6.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
16
- aiohttp_msal-0.6.6.dist-info/top_level.txt,sha256=QPWOi5JtacVEdbaU5bJExc9o-cCT2Lufx0QhUpsv5_E,19
17
- aiohttp_msal-0.6.6.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
18
- aiohttp_msal-0.6.6.dist-info/RECORD,,