fastapi-cachex 0.3.0__tar.gz → 0.3.2__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.
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/PKG-INFO +3 -1
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/__init__.py +10 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/memcached.py +3 -1
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/memory.py +5 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/cache.py +55 -7
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/manager.py +64 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/routes.py +6 -2
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/__init__.py +2 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/config.py +35 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/manager.py +6 -4
- fastapi_cachex-0.3.2/fastapi_cachex/session/middleware.py +485 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/state/__init__.py +3 -0
- fastapi_cachex-0.3.2/fastapi_cachex/state/dependencies.py +28 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/state/manager.py +3 -1
- fastapi_cachex-0.3.2/fastapi_cachex/state/proxy.py +9 -0
- fastapi_cachex-0.3.2/pyproject.toml +189 -0
- fastapi_cachex-0.3.0/pyproject.toml → fastapi_cachex-0.3.2/pyproject.toml.orig +6 -2
- fastapi_cachex-0.3.0/fastapi_cachex/session/middleware.py +0 -164
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/README.md +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/base.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/config.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/backends/redis.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/manager_proxy.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/proxy.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/dependencies.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/models.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/proxy.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/session/token_serializers.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.3.0 → fastapi_cachex-0.3.2}/fastapi_cachex/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-cachex
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
|
|
5
5
|
Keywords: fastapi,cache,etag,cache-control,redis,memcached,in-memory
|
|
6
6
|
Author: allen0099
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pyjwt>=2.9.0 ; extra == 'jwt'
|
|
|
25
25
|
Requires-Dist: pymemcache ; extra == 'memcache'
|
|
26
26
|
Requires-Dist: redis[hiredis]>=5.3.0 ; extra == 'redis'
|
|
27
27
|
Requires-Dist: orjson ; extra == 'redis'
|
|
28
|
+
Requires-Dist: itsdangerous ; extra == 'starlette'
|
|
28
29
|
Requires-Python: >=3.10
|
|
29
30
|
Project-URL: Homepage, https://github.com/allen0099/FastAPI-CacheX
|
|
30
31
|
Project-URL: Repository, https://github.com/allen0099/FastAPI-CacheX.git
|
|
@@ -32,6 +33,7 @@ Project-URL: Issues, https://github.com/allen0099/FastAPI-CacheX/issues
|
|
|
32
33
|
Provides-Extra: jwt
|
|
33
34
|
Provides-Extra: memcache
|
|
34
35
|
Provides-Extra: redis
|
|
36
|
+
Provides-Extra: starlette
|
|
35
37
|
Description-Content-Type: text/markdown
|
|
36
38
|
|
|
37
39
|
# FastAPI-Cache X
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
|
|
5
5
|
from .cache import cache as cache
|
|
6
6
|
from .cache import default_key_builder as default_key_builder
|
|
7
|
+
from .cache import invalidate as invalidate
|
|
7
8
|
from .dependencies import AppCache as AppCache
|
|
8
9
|
from .dependencies import CacheBackend as CacheBackend
|
|
9
10
|
from .dependencies import get_app_cache as get_app_cache
|
|
@@ -12,6 +13,7 @@ from .manager import CacheManager as CacheManager
|
|
|
12
13
|
from .manager_proxy import CacheManagerProxy as CacheManagerProxy
|
|
13
14
|
from .proxy import BackendProxy as BackendProxy
|
|
14
15
|
from .routes import add_routes as add_routes
|
|
16
|
+
from .session import FastAPICacheXSessionMiddleware as FastAPICacheXSessionMiddleware
|
|
15
17
|
from .session import Session as Session
|
|
16
18
|
from .session import SessionConfig as SessionConfig
|
|
17
19
|
from .session import SessionManager as SessionManager
|
|
@@ -34,6 +36,9 @@ from .state import StateDataError as StateDataError
|
|
|
34
36
|
from .state import StateError as StateError
|
|
35
37
|
from .state import StateExpiredError as StateExpiredError
|
|
36
38
|
from .state import StateManager as StateManager
|
|
39
|
+
from .state import StateManagerDep as StateManagerDep
|
|
40
|
+
from .state import StateManagerProxy as StateManagerProxy
|
|
41
|
+
from .state import get_state_manager as get_state_manager
|
|
37
42
|
from .types import CacheKeyBuilder as CacheKeyBuilder
|
|
38
43
|
|
|
39
44
|
_package_logger = logging.getLogger("fastapi_cachex")
|
|
@@ -48,6 +53,7 @@ __all__ = [
|
|
|
48
53
|
"CacheKeyBuilder",
|
|
49
54
|
"CacheManager",
|
|
50
55
|
"CacheManagerProxy",
|
|
56
|
+
"FastAPICacheXSessionMiddleware",
|
|
51
57
|
"InvalidStateError",
|
|
52
58
|
"Session",
|
|
53
59
|
"SessionConfig",
|
|
@@ -66,6 +72,8 @@ __all__ = [
|
|
|
66
72
|
"StateError",
|
|
67
73
|
"StateExpiredError",
|
|
68
74
|
"StateManager",
|
|
75
|
+
"StateManagerDep",
|
|
76
|
+
"StateManagerProxy",
|
|
69
77
|
"add_routes",
|
|
70
78
|
"cache",
|
|
71
79
|
"default_key_builder",
|
|
@@ -74,5 +82,7 @@ __all__ = [
|
|
|
74
82
|
"get_optional_session",
|
|
75
83
|
"get_session",
|
|
76
84
|
"get_session_manager",
|
|
85
|
+
"get_state_manager",
|
|
86
|
+
"invalidate",
|
|
77
87
|
"require_session",
|
|
78
88
|
]
|
|
@@ -177,7 +177,9 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
177
177
|
# Try to delete the prefixed key (exact match only)
|
|
178
178
|
prefixed_key = self._make_key(path)
|
|
179
179
|
try:
|
|
180
|
-
result = await asyncio.to_thread(
|
|
180
|
+
result = await asyncio.to_thread(
|
|
181
|
+
self.client.delete, prefixed_key, noreply=False
|
|
182
|
+
)
|
|
181
183
|
except Exception: # noqa: BLE001
|
|
182
184
|
return 0
|
|
183
185
|
else:
|
|
@@ -183,6 +183,11 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
183
183
|
if fnmatch.fnmatch(cache_path, pattern):
|
|
184
184
|
keys_to_delete.append(key)
|
|
185
185
|
cleared_count += 1
|
|
186
|
+
elif fnmatch.fnmatch(key, pattern):
|
|
187
|
+
# Non-HTTP-cache key (no separators, e.g. CacheManager/
|
|
188
|
+
# StateManager keys) - match against the raw key.
|
|
189
|
+
keys_to_delete.append(key)
|
|
190
|
+
cleared_count += 1
|
|
186
191
|
|
|
187
192
|
for key in keys_to_delete:
|
|
188
193
|
del self.cache[key]
|
|
@@ -61,6 +61,44 @@ def default_key_builder(request: Request) -> str:
|
|
|
61
61
|
return key
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
async def invalidate(
|
|
65
|
+
request: Request,
|
|
66
|
+
key_builder: CacheKeyBuilder | None = None,
|
|
67
|
+
) -> bool:
|
|
68
|
+
"""Invalidate the cache entry a ``@cache``-decorated route would use.
|
|
69
|
+
|
|
70
|
+
Builds the same cache key the ``@cache`` decorator would build for
|
|
71
|
+
``request`` (via ``key_builder`` or ``default_key_builder``) and deletes
|
|
72
|
+
it from the configured backend. Use this after a mutation to bust the
|
|
73
|
+
cache for a specific cached route response.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
request: The request whose cache key should be invalidated. Typically
|
|
77
|
+
a request to the same route/method as the cached one (e.g. build
|
|
78
|
+
it via ``request.app.url_path_for(...)`` for a GET route).
|
|
79
|
+
key_builder: Custom key builder used by the target route's ``@cache``
|
|
80
|
+
decorator, if any. If None, uses ``default_key_builder``.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if a cache entry existed and was deleted, False otherwise.
|
|
84
|
+
"""
|
|
85
|
+
builder = key_builder or default_key_builder
|
|
86
|
+
cache_key = builder(request)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
cache_backend = BackendProxy.get()
|
|
90
|
+
except BackendNotFoundError:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
existing = await cache_backend.get(cache_key)
|
|
94
|
+
if existing is None:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
await cache_backend.delete(cache_key)
|
|
98
|
+
logger.debug("Cache INVALIDATE; key=%s", cache_key)
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
|
|
64
102
|
class CacheControl:
|
|
65
103
|
"""Manages Cache-Control header directives."""
|
|
66
104
|
|
|
@@ -113,11 +151,21 @@ async def get_response(
|
|
|
113
151
|
msg = "Route not found in request scope"
|
|
114
152
|
raise CacheXError(msg)
|
|
115
153
|
|
|
116
|
-
#
|
|
117
|
-
#
|
|
154
|
+
# A placeholder means this route uses the application default. Unwrap the
|
|
155
|
+
# application value instead of calling the route's DefaultPlaceholder.
|
|
156
|
+
route_response_class = route.response_class
|
|
157
|
+
application_default_response_class = __request.app.router.default_response_class
|
|
118
158
|
response_class: type[Response] = cast(
|
|
119
159
|
"type[Response]",
|
|
120
|
-
|
|
160
|
+
(
|
|
161
|
+
getattr(
|
|
162
|
+
application_default_response_class,
|
|
163
|
+
"value",
|
|
164
|
+
application_default_response_class,
|
|
165
|
+
)
|
|
166
|
+
if hasattr(route_response_class, "value")
|
|
167
|
+
else route_response_class
|
|
168
|
+
),
|
|
121
169
|
)
|
|
122
170
|
|
|
123
171
|
# Convert non-Response result to Response using appropriate response_class
|
|
@@ -282,9 +330,7 @@ def cache(
|
|
|
282
330
|
# StreamingResponse/FileResponse — cannot compute ETag; serve as-is
|
|
283
331
|
current_response.headers["Cache-Control"] = cache_control
|
|
284
332
|
return current_response
|
|
285
|
-
current_etag = (
|
|
286
|
-
f'W/"{hashlib.md5(current_body).hexdigest()}"' # noqa: S324
|
|
287
|
-
)
|
|
333
|
+
current_etag = f'W/"{hashlib.md5(current_body).hexdigest()}"' # noqa: S324
|
|
288
334
|
|
|
289
335
|
if client_etag == current_etag:
|
|
290
336
|
# For no-cache, compare fresh data with client's ETag
|
|
@@ -343,7 +389,9 @@ def cache(
|
|
|
343
389
|
|
|
344
390
|
# Update cache if needed
|
|
345
391
|
if not cached_data or cached_data.fingerprint != current_etag:
|
|
346
|
-
assert
|
|
392
|
+
assert (
|
|
393
|
+
current_body is not None
|
|
394
|
+
) # guaranteed by early-return guards above
|
|
347
395
|
# Store in cache if data changed
|
|
348
396
|
await cache_backend.set(
|
|
349
397
|
cache_key,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Generic application-level cache manager for FastAPI-CacheX."""
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import inspect
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
7
|
+
from collections.abc import Awaitable
|
|
8
|
+
from collections.abc import Callable
|
|
6
9
|
from typing import Any
|
|
7
10
|
|
|
8
11
|
from .backends.base import BaseCacheBackend
|
|
@@ -117,6 +120,67 @@ class CacheManager:
|
|
|
117
120
|
"""
|
|
118
121
|
return await self.backend.get(self._cache_key(key)) is not None
|
|
119
122
|
|
|
123
|
+
async def get_or_set(
|
|
124
|
+
self,
|
|
125
|
+
key: str,
|
|
126
|
+
factory: Callable[[], Any] | Callable[[], Awaitable[Any]],
|
|
127
|
+
ttl: int | None = None,
|
|
128
|
+
) -> Any:
|
|
129
|
+
"""Get a cached value, computing and storing it via ``factory`` on a miss.
|
|
130
|
+
|
|
131
|
+
``factory`` is only invoked when ``key`` is missing, expired, or its
|
|
132
|
+
stored content cannot be decoded; on a hit the cached value is
|
|
133
|
+
returned directly. This method does not provide stampede protection:
|
|
134
|
+
concurrent misses for the same key may each invoke ``factory``.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
key: Logical cache key (without the manager's prefix).
|
|
138
|
+
factory: Zero-argument callable (sync or async) that produces the
|
|
139
|
+
JSON-serializable value to cache on a miss.
|
|
140
|
+
ttl: Time-to-live in seconds for a newly created value. If None,
|
|
141
|
+
uses ``self.default_ttl``.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The cached value (existing or newly created).
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
TypeError: If the value produced by ``factory`` is not JSON-serializable.
|
|
148
|
+
"""
|
|
149
|
+
sentinel = object()
|
|
150
|
+
cached = await self.get(key, default=sentinel)
|
|
151
|
+
if cached is not sentinel:
|
|
152
|
+
return cached
|
|
153
|
+
|
|
154
|
+
if inspect.iscoroutinefunction(factory):
|
|
155
|
+
value = await factory()
|
|
156
|
+
else:
|
|
157
|
+
value = factory()
|
|
158
|
+
|
|
159
|
+
await self.set(key, value, ttl=ttl)
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
async def clear_pattern(self, pattern: str) -> int:
|
|
163
|
+
"""Clear all keys under this manager's namespace matching a glob pattern.
|
|
164
|
+
|
|
165
|
+
Delegates to the backend's native ``clear_pattern`` (e.g. Redis ``SCAN``),
|
|
166
|
+
which can be more efficient than ``clear_prefix``'s full key-space scan.
|
|
167
|
+
Note that backends without key-enumeration support (e.g. Memcached)
|
|
168
|
+
cannot honor this and will return 0 with a ``RuntimeWarning``.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
pattern: Glob pattern (relative to ``self.key_prefix``) to match
|
|
172
|
+
against, e.g. ``"user:*"``.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Number of cache entries cleared.
|
|
176
|
+
"""
|
|
177
|
+
match_pattern = self._cache_key(pattern)
|
|
178
|
+
cleared = await self.backend.clear_pattern(match_pattern)
|
|
179
|
+
logger.debug(
|
|
180
|
+
"Cache CLEAR_PATTERN; pattern=%s removed=%s", match_pattern, cleared
|
|
181
|
+
)
|
|
182
|
+
return cleared
|
|
183
|
+
|
|
120
184
|
async def clear_prefix(self, prefix: str | None = None) -> int:
|
|
121
185
|
"""Clear all keys under this manager's namespace matching a sub-prefix.
|
|
122
186
|
|
|
@@ -129,7 +129,9 @@ async def _get_cached_hits_handler(backend: BaseCacheBackend) -> CacheHitsRespon
|
|
|
129
129
|
if method: # Valid cache key
|
|
130
130
|
# Check if cache entry is expired
|
|
131
131
|
is_expired = expiry is not None and expiry <= now
|
|
132
|
-
ttl_remaining =
|
|
132
|
+
ttl_remaining = (
|
|
133
|
+
max(0.0, round(expiry - now, 2)) if expiry is not None else None
|
|
134
|
+
)
|
|
133
135
|
|
|
134
136
|
cached_hits.append(
|
|
135
137
|
CacheHitRecord(
|
|
@@ -188,7 +190,9 @@ async def _get_cached_records_handler(
|
|
|
188
190
|
content = entry.content
|
|
189
191
|
content_size = len(content) if isinstance(content, (bytes, str)) else 0
|
|
190
192
|
|
|
191
|
-
ttl_remaining =
|
|
193
|
+
ttl_remaining = (
|
|
194
|
+
max(0.0, round(expiry - now, 2)) if expiry is not None else None
|
|
195
|
+
)
|
|
192
196
|
|
|
193
197
|
content_preview = (
|
|
194
198
|
content[:100].decode("utf-8", errors="ignore")
|
|
@@ -6,12 +6,14 @@ from .dependencies import get_session
|
|
|
6
6
|
from .dependencies import get_session_manager
|
|
7
7
|
from .dependencies import require_session
|
|
8
8
|
from .manager import SessionManager
|
|
9
|
+
from .middleware import FastAPICacheXSessionMiddleware
|
|
9
10
|
from .middleware import SessionMiddleware
|
|
10
11
|
from .models import Session
|
|
11
12
|
from .models import SessionUser
|
|
12
13
|
from .proxy import SessionManagerProxy
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
16
|
+
"FastAPICacheXSessionMiddleware",
|
|
15
17
|
"Session",
|
|
16
18
|
"SessionConfig",
|
|
17
19
|
"SessionManager",
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
|
+
from pydantic import ConfigDict
|
|
6
7
|
from pydantic import Field
|
|
7
8
|
from pydantic import SecretStr
|
|
8
9
|
|
|
10
|
+
SameSitePolicy = Literal["lax", "strict", "none"]
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class SessionConfig(BaseModel):
|
|
11
14
|
"""Session configuration settings."""
|
|
12
15
|
|
|
16
|
+
model_config = ConfigDict(extra="forbid")
|
|
17
|
+
|
|
13
18
|
# Session lifetime
|
|
14
19
|
session_ttl: int = Field(
|
|
15
20
|
default=3600,
|
|
@@ -87,3 +92,33 @@ class SessionConfig(BaseModel):
|
|
|
87
92
|
default="session:",
|
|
88
93
|
description="Prefix for session keys in backend storage",
|
|
89
94
|
)
|
|
95
|
+
|
|
96
|
+
# Cookie settings (FastAPICacheXSessionMiddleware only)
|
|
97
|
+
cookie_name: str = Field(
|
|
98
|
+
default="session",
|
|
99
|
+
description="Name of the cookie used to store the session token "
|
|
100
|
+
"(FastAPICacheXSessionMiddleware only)",
|
|
101
|
+
)
|
|
102
|
+
cookie_max_age: int | None = Field(
|
|
103
|
+
default=14 * 24 * 60 * 60,
|
|
104
|
+
description="Max-Age (seconds) for the session cookie; None disables "
|
|
105
|
+
"Max-Age/Expires (session cookie deleted when browser closes)",
|
|
106
|
+
)
|
|
107
|
+
cookie_path: str = Field(
|
|
108
|
+
default="/",
|
|
109
|
+
description="Path attribute for the session cookie",
|
|
110
|
+
)
|
|
111
|
+
cookie_same_site: SameSitePolicy = Field(
|
|
112
|
+
default="lax",
|
|
113
|
+
description="SameSite attribute for the session cookie",
|
|
114
|
+
)
|
|
115
|
+
cookie_https_only: bool = Field(
|
|
116
|
+
default=False,
|
|
117
|
+
description="Whether to set the Secure flag on the session cookie "
|
|
118
|
+
"(cookie only sent over HTTPS)",
|
|
119
|
+
)
|
|
120
|
+
cookie_domain: str | None = Field(
|
|
121
|
+
default=None,
|
|
122
|
+
description="Domain attribute for the session cookie; None omits the "
|
|
123
|
+
"Domain attribute",
|
|
124
|
+
)
|
|
@@ -76,7 +76,7 @@ class SessionManager:
|
|
|
76
76
|
user: SessionUser,
|
|
77
77
|
ip_address: str | None = None,
|
|
78
78
|
user_agent: str | None = None,
|
|
79
|
-
**extra_data:
|
|
79
|
+
**extra_data: object,
|
|
80
80
|
) -> tuple[Session, str]:
|
|
81
81
|
"""Create a new session for an authenticated user.
|
|
82
82
|
|
|
@@ -100,7 +100,7 @@ class SessionManager:
|
|
|
100
100
|
self,
|
|
101
101
|
ip_address: str | None = None,
|
|
102
102
|
user_agent: str | None = None,
|
|
103
|
-
**extra_data:
|
|
103
|
+
**extra_data: object,
|
|
104
104
|
) -> tuple[Session, str]:
|
|
105
105
|
"""Create a new session without user information."""
|
|
106
106
|
return await self._create_session(
|
|
@@ -115,7 +115,7 @@ class SessionManager:
|
|
|
115
115
|
user: SessionUser | None,
|
|
116
116
|
ip_address: str | None = None,
|
|
117
117
|
user_agent: str | None = None,
|
|
118
|
-
**extra_data:
|
|
118
|
+
**extra_data: object,
|
|
119
119
|
) -> tuple[Session, str]:
|
|
120
120
|
"""Internal helper to create and persist a session."""
|
|
121
121
|
session = Session(
|
|
@@ -449,7 +449,9 @@ class SessionManager:
|
|
|
449
449
|
ttl = max(ttl, 1) # Ensure at least 1 second
|
|
450
450
|
|
|
451
451
|
fingerprint = self.security.hash_data(value.decode("utf-8"))
|
|
452
|
-
await self.backend.set(
|
|
452
|
+
await self.backend.set(
|
|
453
|
+
key, CacheEntry(fingerprint=fingerprint, content=value), ttl=ttl
|
|
454
|
+
)
|
|
453
455
|
logger.debug("Session saved; id=%s ttl=%s", session.session_id, ttl)
|
|
454
456
|
|
|
455
457
|
async def _load_session(self, session_id: str) -> Session | None:
|