aiohttp-msal 1.0.7__tar.gz → 1.0.8__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.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022-2025 kellerza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,24 +1,44 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aiohttp-msal
3
- Version: 1.0.7
3
+ Version: 1.0.8
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
7
7
  Author-email: Johann Kellerman <kellerza@gmail.com>
8
- License: MIT
8
+ License: The MIT License (MIT)
9
+
10
+ Copyright (c) 2022-2025 kellerza
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
9
29
  Classifier: Development Status :: 4 - Beta
10
30
  Classifier: Intended Audience :: Developers
11
31
  Classifier: Natural Language :: English
12
32
  Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.11
14
33
  Classifier: Programming Language :: Python :: 3.12
15
34
  Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Programming Language :: Python :: 3.14
16
36
  Requires-Dist: aiohttp>=3.11.18,<3.13
17
- Requires-Dist: aiohttp-session>=2.12.1,<3
37
+ Requires-Dist: aiohttp-session[aioredis]>=2.12.1,<3
18
38
  Requires-Dist: attrs>=25.3,<26
19
39
  Requires-Dist: msal>=1.32.3,<2
20
40
  Requires-Dist: aiohttp-session[aioredis]>=2.12.1,<3 ; extra == 'aioredis'
21
- Requires-Python: >=3.11
41
+ Requires-Python: >=3.12
22
42
  Project-URL: Homepage, https://github.com/kellerza/aiohttp_msal
23
43
  Provides-Extra: aioredis
24
44
  Description-Content-Type: text/markdown
@@ -135,8 +155,6 @@ def main()
135
155
  ```bash
136
156
  uv sync --all-extras
137
157
  uv tool install ruff
138
- uv tool install codespell
139
- uv tool install pyproject-fmt
140
158
  uv tool install prek
141
159
  prek install # add pre-commit hooks
142
160
  ```
@@ -110,8 +110,6 @@ def main()
110
110
  ```bash
111
111
  uv sync --all-extras
112
112
  uv tool install ruff
113
- uv tool install codespell
114
- uv tool install pyproject-fmt
115
113
  uv tool install prek
116
114
  prek install # add pre-commit hooks
117
115
  ```
@@ -1,28 +1,28 @@
1
1
  [build-system]
2
2
  build-backend = "uv_build"
3
- requires = [ "uv-build" ] # >=0.5.15,<0.6
3
+ requires = [ "uv-build>=0.8.20,<0.9" ]
4
4
 
5
5
  [project]
6
6
  name = "aiohttp-msal"
7
- version = "1.0.7"
7
+ version = "1.0.8"
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" ]
11
- license = { text = "MIT" }
11
+ license = { file = "LICENSE" }
12
12
  authors = [ { name = "Johann Kellerman", email = "kellerza@gmail.com" } ]
13
- requires-python = ">=3.11"
13
+ requires-python = ">=3.12"
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "Natural Language :: English",
18
18
  "Programming Language :: Python :: 3 :: Only",
19
- "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
22
  ]
23
23
  dependencies = [
24
24
  "aiohttp>=3.11.18,<3.13",
25
- "aiohttp-session>=2.12.1,<3",
25
+ "aiohttp-session[aioredis]>=2.12.1,<3",
26
26
  "attrs>=25.3,<26",
27
27
  "msal>=1.32.3,<2",
28
28
  ]
@@ -31,7 +31,9 @@ urls.Homepage = "https://github.com/kellerza/aiohttp_msal"
31
31
 
32
32
  [dependency-groups]
33
33
  dev = [
34
+ "codespell",
34
35
  "mypy",
36
+ "pyproject-fmt",
35
37
  "pytest",
36
38
  "pytest-aiohttp",
37
39
  "pytest-asyncio",
@@ -73,8 +75,7 @@ lint.ignore = [
73
75
  lint.isort.no-lines-before = [ "future", "standard-library" ]
74
76
 
75
77
  [tool.codespell]
76
- skip = [ "build/*", "*.json", "*.csv", "**/node_modules/*", "./s2-js/dist/*" ]
77
- #ignore-words-list = []
78
+ skip = [ "build/*" ]
78
79
 
79
80
  [tool.pytest.ini_options]
80
81
  pythonpath = [ ".", "src" ]
@@ -125,5 +126,4 @@ match = "main"
125
126
  [tool.semantic_release.commit_parser_options]
126
127
  major_tags = [ ":boom:" ]
127
128
  minor_tags = [ ":rocket:" ]
128
- # patch_tags = [ ":ambulance:", ":lock:", ":bug:", ":dolphin:" ]
129
- patch_tags = [ "" ] # always patch
129
+ patch_tags = [ "" ] # always patch
@@ -4,7 +4,7 @@ import logging
4
4
  from collections.abc import Awaitable, Callable
5
5
  from functools import wraps
6
6
  from inspect import getfullargspec, iscoroutinefunction
7
- from typing import TypeVar, TypeVarTuple, cast
7
+ from typing import cast
8
8
 
9
9
  from aiohttp import ClientSession, web
10
10
  from aiohttp_session import get_session
@@ -16,15 +16,12 @@ from aiohttp_msal.utils import retry
16
16
 
17
17
  _LOG = logging.getLogger(__name__)
18
18
 
19
- _T = TypeVar("_T")
20
- Ts = TypeVarTuple("Ts")
21
19
 
22
-
23
- def msal_session(
20
+ def msal_session[T, *Ts](
24
21
  *callbacks: Callable[[AsyncMSAL], bool | Awaitable[bool]],
25
22
  at_least_one: bool | None = False,
26
23
  ) -> Callable[
27
- [Callable[[*Ts, AsyncMSAL], Awaitable[_T]]], Callable[[*Ts], Awaitable[_T]]
24
+ [Callable[[*Ts, AsyncMSAL], Awaitable[T]]], Callable[[*Ts], Awaitable[T]]
28
25
  ]:
29
26
  """Session decorator.
30
27
 
@@ -32,10 +29,10 @@ def msal_session(
32
29
  """
33
30
 
34
31
  def check_session(
35
- func: Callable[[*Ts, AsyncMSAL], Awaitable[_T]],
36
- ) -> Callable[[*Ts], Awaitable[_T]]:
32
+ func: Callable[[*Ts, AsyncMSAL], Awaitable[T]],
33
+ ) -> Callable[[*Ts], Awaitable[T]]:
37
34
  @wraps(func)
38
- async def wrapper(*args: *Ts) -> _T:
35
+ async def wrapper(*args: *Ts) -> T:
39
36
  if len(args) < 1:
40
37
  raise AssertionError("Requires a Request as the first parameter")
41
38
  request = cast(web.Request, args[0])
@@ -104,12 +101,13 @@ async def app_init_redis_session(
104
101
  else:
105
102
  await check_proxy()
106
103
 
107
- _LOG.info("Connect to Redis %s", ENV.REDIS)
108
- try:
109
- ENV.database = from_url(ENV.REDIS)
110
- # , encoding="utf-8", decode_responses=True
111
- except ConnectionRefusedError as err:
112
- raise ConnectionError("Could not connect to REDIS server") from err
104
+ if ENV.database is None:
105
+ _LOG.info("Connect to Redis %s", ENV.REDIS)
106
+ try:
107
+ ENV.database = from_url(ENV.REDIS)
108
+ # , encoding="utf-8", decode_responses=True
109
+ except ConnectionRefusedError as err:
110
+ raise ConnectionError("Could not connect to REDIS server") from err
113
111
 
114
112
  storage = redis_storage.RedisStorage(
115
113
  ENV.database,
@@ -120,8 +118,8 @@ async def app_init_redis_session(
120
118
  secure=True,
121
119
  domain=ENV.DOMAIN,
122
120
  cookie_name=ENV.COOKIE_NAME,
123
- encoder=ENV.dumps,
124
- decoder=ENV.loads,
121
+ encoder=ENV.json_dumps,
122
+ decoder=ENV.json_loads,
125
123
  )
126
124
  _setup(app, storage)
127
125
 
@@ -129,13 +127,13 @@ async def app_init_redis_session(
129
127
  @retry
130
128
  async def check_proxy() -> None:
131
129
  """Test if we have Internet connectivity through proxies etc."""
132
- try:
133
- async with ClientSession(trust_env=True) as cses:
134
- async with cses.get("http://httpbin.org/get") as resp:
130
+ print("Check Internet connectivity for OAuth", end="", flush=True)
131
+ async with ClientSession(trust_env=True) as cses:
132
+ for url in ("https://www.google.com", "http://httpbin.org/get"):
133
+ async with cses.get(url) as resp:
135
134
  if resp.ok:
135
+ print(" ... ok", flush=True)
136
136
  return
137
- raise ConnectionError(await resp.text())
138
- except Exception as err:
139
- raise ConnectionError(
140
- "No connection to the Internet. Required for OAuth. Check your Proxy?"
141
- ) from err
137
+ raise ConnectionError(
138
+ "No connection to the Internet. Required for OAuth. Check your Proxy?"
139
+ )
@@ -9,7 +9,7 @@ import asyncio
9
9
  import logging
10
10
  from collections.abc import Callable
11
11
  from functools import cached_property, partialmethod
12
- from typing import Any, ClassVar, Literal, Self, TypeVar, Unpack, cast
12
+ from typing import Any, ClassVar, Literal, Self, Unpack, cast
13
13
 
14
14
  import attrs
15
15
  from aiohttp import web
@@ -37,8 +37,6 @@ HTTP_PATCH = "patch"
37
37
  HTTP_DELETE = "delete"
38
38
  HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
39
39
 
40
- T = TypeVar("T")
41
-
42
40
 
43
41
  @attrs.define(slots=False)
44
42
  class AsyncMSAL:
@@ -219,7 +217,7 @@ class AsyncMSAL:
219
217
  elif method in [HTTP_POST, HTTP_PUT, HTTP_PATCH]:
220
218
  headers["Content-type"] = "application/json"
221
219
  if "data" in kwargs:
222
- kwargs["data"] = ENV.dumps(kwargs["data"]) # auto convert to json
220
+ kwargs["data"] = ENV.json_dumps(kwargs["data"]) # auto convert to json
223
221
 
224
222
  if not AsyncMSAL.client_session:
225
223
  AsyncMSAL.client_session = ClientSession(trust_env=True)
@@ -5,7 +5,7 @@ import logging
5
5
  import time
6
6
  from collections.abc import AsyncGenerator
7
7
  from contextlib import AsyncExitStack, asynccontextmanager
8
- from typing import Any, TypeVar
8
+ from typing import Any
9
9
 
10
10
  from redis.asyncio import Redis, from_url
11
11
 
@@ -56,7 +56,7 @@ async def session_iter(
56
56
  sval = await redis.get(key)
57
57
  created, ses = 0, {}
58
58
  try:
59
- val = ENV.loads(sval) # type: ignore[arg-type]
59
+ val = ENV.json_loads(sval) # type: ignore[arg-type]
60
60
  created = int(val["created"])
61
61
  ses = val["session"]
62
62
  except Exception:
@@ -103,7 +103,7 @@ async def invalid_sessions(redis: Redis, /) -> None:
103
103
  if sval is None:
104
104
  continue
105
105
  try:
106
- val: dict = ENV.loads(sval)
106
+ val: dict = ENV.json_loads(sval)
107
107
  assert isinstance(val["created"], int)
108
108
  assert isinstance(val["session"], dict)
109
109
  except Exception as err:
@@ -111,10 +111,7 @@ async def invalid_sessions(redis: Redis, /) -> None:
111
111
  await redis.delete(key)
112
112
 
113
113
 
114
- T = TypeVar("T", bound=AsyncMSAL)
115
-
116
-
117
- def async_msal_factory(
114
+ def async_msal_factory[T: AsyncMSAL](
118
115
  cls: type[T], key: str, created: int, session: dict[str, Any], /
119
116
  ) -> T:
120
117
  """Create a AsyncMSAL session with a save_callback.
@@ -126,7 +123,7 @@ def async_msal_factory(
126
123
  async def async_save_cache(_: dict) -> None:
127
124
  """Save the token cache to Redis."""
128
125
  async with get_redis() as rd2:
129
- await rd2.set(key, ENV.dumps({"created": created, "session": session}))
126
+ await rd2.set(key, ENV.json_dumps({"created": created, "session": session}))
130
127
 
131
128
  def save_cache(*args: Any) -> None:
132
129
  """Save the token cache to Redis."""
@@ -138,7 +135,7 @@ def async_msal_factory(
138
135
  return cls(session, save_callback=save_cache)
139
136
 
140
137
 
141
- async def get_session(
138
+ async def get_session[T: AsyncMSAL](
142
139
  cls: type[T],
143
140
  email: str,
144
141
  /,
@@ -166,7 +163,7 @@ async def redis_get_json(key: str) -> list[Any] | dict[str, Any] | None:
166
163
  """Get a key from redis."""
167
164
  res = await ENV.database.get(key)
168
165
  if isinstance(res, str | bytes | bytearray):
169
- return ENV.loads(res)
166
+ return ENV.json_loads(res)
170
167
  if res is not None:
171
168
  _LOG.warning("Unexpected type for %s: %s", key, type(res))
172
169
  return None
@@ -43,8 +43,10 @@ class MSALSettings(SettingsBase):
43
43
  database: "Redis" = attrs.field(init=False, default=None)
44
44
  """Store the Redis connection when using app_init_redis_session()."""
45
45
 
46
- dumps: Callable[[Any], str] = attrs.field(default=json.dumps)
47
- loads: Callable[[str | bytes | bytearray], Any] = attrs.field(default=json.loads)
46
+ json_dumps: Callable[[Any], str] = attrs.field(default=json.dumps)
47
+ json_loads: Callable[[str | bytes | bytearray], Any] = attrs.field(
48
+ default=json.loads
49
+ )
48
50
 
49
51
 
50
52
  ENV = MSALSettings()
@@ -3,13 +3,10 @@
3
3
  import asyncio
4
4
  from collections.abc import Awaitable, Callable
5
5
  from functools import partial, wraps
6
- from typing import Any, ParamSpec, TypeVar
6
+ from typing import Any
7
7
 
8
- T = TypeVar("T")
9
- P = ParamSpec("P")
10
8
 
11
-
12
- def async_wrap(
9
+ def async_wrap[T](
13
10
  func: Callable[..., T],
14
11
  ) -> Callable[..., Awaitable[T]]:
15
12
  """Wrap a function doing I/O to run in an executor thread."""
@@ -49,17 +46,7 @@ class dict_property(property):
49
46
  getattr(instance, self.dict_name, {}).__setitem__(self.prop_name, value)
50
47
 
51
48
 
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
-
62
- def retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
49
+ def retry[T, **P](func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
63
50
  """Retry if tenacity is installed."""
64
51
 
65
52
  @wraps(func)