aiohttp-msal 1.0.6__tar.gz → 1.0.7__tar.gz
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-1.0.6 → aiohttp_msal-1.0.7}/PKG-INFO +1 -1
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/pyproject.toml +1 -1
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/__init__.py +3 -6
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/msal_async.py +2 -3
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/redis_tools.py +20 -21
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/settings.py +5 -1
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/README.md +0 -0
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/helpers.py +0 -0
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/routes.py +0 -0
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/settings_base.py +0 -0
- {aiohttp_msal-1.0.6 → aiohttp_msal-1.0.7}/src/aiohttp_msal/utils.py +0 -0
|
@@ -4,7 +4,7 @@ requires = [ "uv-build" ] # >=0.5.15,<0.6
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aiohttp-msal"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.7"
|
|
8
8
|
description = "Helper Library to use the Microsoft Authentication Library (MSAL) with aiohttp"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "aiohttp", "asyncio", "msal", "oauth" ]
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""aiohttp_msal."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import logging
|
|
5
4
|
from collections.abc import Awaitable, Callable
|
|
6
5
|
from functools import wraps
|
|
7
6
|
from inspect import getfullargspec, iscoroutinefunction
|
|
8
|
-
from typing import
|
|
7
|
+
from typing import TypeVar, TypeVarTuple, cast
|
|
9
8
|
|
|
10
9
|
from aiohttp import ClientSession, web
|
|
11
10
|
from aiohttp_session import get_session
|
|
@@ -92,8 +91,6 @@ async def app_init_redis_session(
|
|
|
92
91
|
app: web.Application,
|
|
93
92
|
max_age: int = 3600 * 24 * 90,
|
|
94
93
|
check_proxy_cb: Callable[[], Awaitable[None]] | None = None,
|
|
95
|
-
encoder: Callable[[object], str] = json.dumps,
|
|
96
|
-
decoder: Callable[[str], Any] = json.loads,
|
|
97
94
|
) -> None:
|
|
98
95
|
"""Init an aiohttp_session with Redis storage helper.
|
|
99
96
|
|
|
@@ -123,8 +120,8 @@ async def app_init_redis_session(
|
|
|
123
120
|
secure=True,
|
|
124
121
|
domain=ENV.DOMAIN,
|
|
125
122
|
cookie_name=ENV.COOKIE_NAME,
|
|
126
|
-
encoder=
|
|
127
|
-
decoder=
|
|
123
|
+
encoder=ENV.dumps,
|
|
124
|
+
decoder=ENV.loads,
|
|
128
125
|
)
|
|
129
126
|
_setup(app, storage)
|
|
130
127
|
|
|
@@ -6,7 +6,6 @@ Once you have the OAuth tokens store in the session, you are free to make reques
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
import json
|
|
10
9
|
import logging
|
|
11
10
|
from collections.abc import Callable
|
|
12
11
|
from functools import cached_property, partialmethod
|
|
@@ -43,7 +42,7 @@ T = TypeVar("T")
|
|
|
43
42
|
|
|
44
43
|
@attrs.define(slots=False)
|
|
45
44
|
class AsyncMSAL:
|
|
46
|
-
"""
|
|
45
|
+
"""AsyncMSAL class.
|
|
47
46
|
|
|
48
47
|
Authorization Code Flow Helper. Learn more about auth-code-flow at
|
|
49
48
|
https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
|
@@ -220,7 +219,7 @@ class AsyncMSAL:
|
|
|
220
219
|
elif method in [HTTP_POST, HTTP_PUT, HTTP_PATCH]:
|
|
221
220
|
headers["Content-type"] = "application/json"
|
|
222
221
|
if "data" in kwargs:
|
|
223
|
-
kwargs["data"] =
|
|
222
|
+
kwargs["data"] = ENV.dumps(kwargs["data"]) # auto convert to json
|
|
224
223
|
|
|
225
224
|
if not AsyncMSAL.client_session:
|
|
226
225
|
AsyncMSAL.client_session = ClientSession(trust_env=True)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Redis tools for sessions."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
import time
|
|
7
6
|
from collections.abc import AsyncGenerator
|
|
@@ -11,7 +10,7 @@ from typing import Any, TypeVar
|
|
|
11
10
|
from redis.asyncio import Redis, from_url
|
|
12
11
|
|
|
13
12
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
14
|
-
from aiohttp_msal.settings import ENV
|
|
13
|
+
from aiohttp_msal.settings import ENV
|
|
15
14
|
|
|
16
15
|
_LOG = logging.getLogger(__name__)
|
|
17
16
|
|
|
@@ -21,17 +20,17 @@ SES_KEYS = ("mail", "name", "m_mail", "m_name")
|
|
|
21
20
|
@asynccontextmanager
|
|
22
21
|
async def get_redis() -> AsyncGenerator[Redis, None]:
|
|
23
22
|
"""Get a Redis connection."""
|
|
24
|
-
if
|
|
23
|
+
if ENV.database:
|
|
25
24
|
_LOG.debug("Using redis from environment")
|
|
26
|
-
yield
|
|
25
|
+
yield ENV.database
|
|
27
26
|
return
|
|
28
|
-
_LOG.info("Connect to Redis %s",
|
|
29
|
-
redis = from_url(
|
|
30
|
-
|
|
27
|
+
_LOG.info("Connect to Redis %s", ENV.REDIS)
|
|
28
|
+
redis = from_url(ENV.REDIS) # decode_responses=True not allowed aiohttp_session
|
|
29
|
+
ENV.database = redis
|
|
31
30
|
try:
|
|
32
31
|
yield redis
|
|
33
32
|
finally:
|
|
34
|
-
|
|
33
|
+
ENV.database = None # type:ignore[assignment]
|
|
35
34
|
await redis.close()
|
|
36
35
|
|
|
37
36
|
|
|
@@ -50,14 +49,14 @@ async def session_iter(
|
|
|
50
49
|
if match and not all(isinstance(v, str) for v in match.values()):
|
|
51
50
|
raise ValueError("match values must be strings")
|
|
52
51
|
async for key in redis.scan_iter(
|
|
53
|
-
count=100, match=key_match or f"{
|
|
52
|
+
count=100, match=key_match or f"{ENV.COOKIE_NAME}*"
|
|
54
53
|
):
|
|
55
54
|
if not isinstance(key, str):
|
|
56
55
|
key = key.decode()
|
|
57
56
|
sval = await redis.get(key)
|
|
58
57
|
created, ses = 0, {}
|
|
59
58
|
try:
|
|
60
|
-
val =
|
|
59
|
+
val = ENV.loads(sval) # type: ignore[arg-type]
|
|
61
60
|
created = int(val["created"])
|
|
62
61
|
ses = val["session"]
|
|
63
62
|
except Exception:
|
|
@@ -97,14 +96,14 @@ async def session_clean(
|
|
|
97
96
|
|
|
98
97
|
async def invalid_sessions(redis: Redis, /) -> None:
|
|
99
98
|
"""Find & clean invalid sessions."""
|
|
100
|
-
async for key in redis.scan_iter(count=100, match=f"{
|
|
99
|
+
async for key in redis.scan_iter(count=100, match=f"{ENV.COOKIE_NAME}*"):
|
|
101
100
|
if not isinstance(key, str):
|
|
102
101
|
key = key.decode()
|
|
103
102
|
sval = await redis.get(key)
|
|
104
103
|
if sval is None:
|
|
105
104
|
continue
|
|
106
105
|
try:
|
|
107
|
-
val: dict =
|
|
106
|
+
val: dict = ENV.loads(sval)
|
|
108
107
|
assert isinstance(val["created"], int)
|
|
109
108
|
assert isinstance(val["session"], dict)
|
|
110
109
|
except Exception as err:
|
|
@@ -127,7 +126,7 @@ def async_msal_factory(
|
|
|
127
126
|
async def async_save_cache(_: dict) -> None:
|
|
128
127
|
"""Save the token cache to Redis."""
|
|
129
128
|
async with get_redis() as rd2:
|
|
130
|
-
await rd2.set(key,
|
|
129
|
+
await rd2.set(key, ENV.dumps({"created": created, "session": session}))
|
|
131
130
|
|
|
132
131
|
def save_cache(*args: Any) -> None:
|
|
133
132
|
"""Save the token cache to Redis."""
|
|
@@ -163,11 +162,11 @@ async def get_session(
|
|
|
163
162
|
raise ValueError(f"{msg} with scope {scope} not found ({cnt} checked)")
|
|
164
163
|
|
|
165
164
|
|
|
166
|
-
async def redis_get_json(key: str) -> list[
|
|
165
|
+
async def redis_get_json(key: str) -> list[Any] | dict[str, Any] | None:
|
|
167
166
|
"""Get a key from redis."""
|
|
168
|
-
res = await
|
|
167
|
+
res = await ENV.database.get(key)
|
|
169
168
|
if isinstance(res, str | bytes | bytearray):
|
|
170
|
-
return
|
|
169
|
+
return ENV.loads(res)
|
|
171
170
|
if res is not None:
|
|
172
171
|
_LOG.warning("Unexpected type for %s: %s", key, type(res))
|
|
173
172
|
return None
|
|
@@ -175,7 +174,7 @@ async def redis_get_json(key: str) -> list[str] | dict[str, Any] | None:
|
|
|
175
174
|
|
|
176
175
|
async def redis_get(key: str) -> str | None:
|
|
177
176
|
"""Get a key from redis."""
|
|
178
|
-
res = await
|
|
177
|
+
res = await ENV.database.get(key)
|
|
179
178
|
if isinstance(res, str):
|
|
180
179
|
return res
|
|
181
180
|
if isinstance(res, bytes | bytearray):
|
|
@@ -189,22 +188,22 @@ async def redis_set_set(key: str, new_set: set[str]) -> None:
|
|
|
189
188
|
"""Set the value of a set in redis."""
|
|
190
189
|
cur_set = set(
|
|
191
190
|
s if isinstance(s, str) else s.decode()
|
|
192
|
-
for s in await
|
|
191
|
+
for s in await ENV.database.smembers(key)
|
|
193
192
|
)
|
|
194
193
|
dif = list(cur_set - new_set)
|
|
195
194
|
if dif:
|
|
196
195
|
_LOG.warning("%s: removing %s", key, dif)
|
|
197
|
-
await
|
|
196
|
+
await ENV.database.srem(key, *dif)
|
|
198
197
|
|
|
199
198
|
dif = list(new_set - cur_set)
|
|
200
199
|
if dif:
|
|
201
200
|
_LOG.info("%s: adding %s", key, dif)
|
|
202
|
-
await
|
|
201
|
+
await ENV.database.sadd(key, *dif)
|
|
203
202
|
|
|
204
203
|
|
|
205
204
|
async def redis_scan_keys(match_str: str) -> list[str]:
|
|
206
205
|
"""Return a list of matching keys."""
|
|
207
206
|
return [
|
|
208
207
|
s if isinstance(s, str) else s.decode()
|
|
209
|
-
async for s in
|
|
208
|
+
async for s in ENV.database.scan_iter(match=match_str)
|
|
210
209
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Settings."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from collections.abc import Awaitable, Callable
|
|
4
5
|
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
@@ -39,8 +40,11 @@ class MSALSettings(SettingsBase):
|
|
|
39
40
|
|
|
40
41
|
REDIS: str = "redis://redis1:6379"
|
|
41
42
|
"""OPTIONAL: Redis database connection used by app_init_redis_session()."""
|
|
42
|
-
database: "Redis" = attrs.field(init=False)
|
|
43
|
+
database: "Redis" = attrs.field(init=False, default=None)
|
|
43
44
|
"""Store the Redis connection when using app_init_redis_session()."""
|
|
44
45
|
|
|
46
|
+
dumps: Callable[[Any], str] = attrs.field(default=json.dumps)
|
|
47
|
+
loads: Callable[[str | bytes | bytearray], Any] = attrs.field(default=json.loads)
|
|
48
|
+
|
|
45
49
|
|
|
46
50
|
ENV = MSALSettings()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|