fastapi-cachex 0.3.9__tar.gz → 0.4.0__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.9 → fastapi_cachex-0.4.0}/PKG-INFO +11 -13
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/README.md +10 -10
- fastapi_cachex-0.4.0/fastapi_cachex/__init__.py +149 -0
- fastapi_cachex-0.4.0/fastapi_cachex/_deprecation.py +68 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/base.py +84 -20
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/codec.py +4 -2
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/config.py +0 -8
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memcached.py +58 -11
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memory.py +32 -29
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/redis.py +106 -141
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/cache.py +105 -113
- fastapi_cachex-0.4.0/fastapi_cachex/cache_key.py +224 -0
- fastapi_cachex-0.4.0/fastapi_cachex/exceptions.py +26 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/headers.py +9 -4
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/manager.py +8 -35
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/proxy.py +0 -37
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/routes.py +30 -78
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/__init__.py +12 -3
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/config.py +43 -26
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/dependencies.py +105 -56
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/manager.py +132 -24
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/middleware.py +51 -241
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/models.py +51 -1
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/token_serializers.py +14 -17
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/__init__.py +10 -1
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/types.py +51 -4
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/pyproject.toml +3 -2
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/pyproject.toml.orig +6 -3
- fastapi_cachex-0.3.9/fastapi_cachex/__init__.py +0 -123
- fastapi_cachex-0.3.9/fastapi_cachex/exceptions.py +0 -58
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/LICENSE +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/lock.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/manager_proxy.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/proxy.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/dependencies.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/manager.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/proxy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-cachex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -26,7 +26,6 @@ Requires-Dist: itsdangerous>=1.1.0
|
|
|
26
26
|
Requires-Dist: pydantic>=2.7.0
|
|
27
27
|
Requires-Dist: starlette>=1.0.0
|
|
28
28
|
Requires-Dist: pyjwt>=2.9.0 ; extra == 'jwt'
|
|
29
|
-
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcache'
|
|
30
29
|
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcached'
|
|
31
30
|
Requires-Dist: redis[hiredis]>=5.3.0 ; extra == 'redis'
|
|
32
31
|
Requires-Dist: orjson>=3.4.7 ; extra == 'redis'
|
|
@@ -36,7 +35,6 @@ Project-URL: Repository, https://github.com/allen0099/FastAPI-CacheX.git
|
|
|
36
35
|
Project-URL: Issues, https://github.com/allen0099/FastAPI-CacheX/issues
|
|
37
36
|
Project-URL: Documentation, https://fastapi-cachex.readthedocs.io/
|
|
38
37
|
Provides-Extra: jwt
|
|
39
|
-
Provides-Extra: memcache
|
|
40
38
|
Provides-Extra: memcached
|
|
41
39
|
Provides-Extra: redis
|
|
42
40
|
Description-Content-Type: text/markdown
|
|
@@ -57,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
|
57
55
|
|
|
58
56
|
[English](https://fastapi-cachex.readthedocs.io/en/latest/) | [繁體中文](https://fastapi-cachex.readthedocs.io/zh-tw/latest/)
|
|
59
57
|
|
|
60
|
-
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, application-level caching
|
|
58
|
+
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, and application-level caching.
|
|
61
59
|
|
|
62
60
|
**Documentation:** <https://fastapi-cachex.readthedocs.io/en/latest/> — guides and the full API reference.
|
|
63
61
|
|
|
@@ -69,9 +67,9 @@ A high-performance caching extension for FastAPI: a server-side response cache w
|
|
|
69
67
|
your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`.
|
|
70
68
|
- **Backends** — in-memory, Redis and Memcached, with atomic counters,
|
|
71
69
|
one-shot values and locks.
|
|
72
|
-
- **Sessions (
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
- **Sessions and OAuth state (deprecated)**: signed session tokens and one-time
|
|
71
|
+
OAuth state tokens. Both are deprecated in 0.4.0 and removed in 0.5.0; see
|
|
72
|
+
[where to move](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/#session-state-deprecated).
|
|
75
73
|
|
|
76
74
|
## Installation
|
|
77
75
|
|
|
@@ -85,7 +83,7 @@ backends and the optional session transports ship as extras:
|
|
|
85
83
|
| Extra | Install | Pulls in | Needed for |
|
|
86
84
|
|-------|---------|----------|------------|
|
|
87
85
|
| `redis` | `uv add "fastapi-cachex[redis]"` | `redis[hiredis]`, `orjson` | `AsyncRedisCacheBackend` |
|
|
88
|
-
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend`
|
|
86
|
+
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend` |
|
|
89
87
|
| `jwt` | `uv add "fastapi-cachex[jwt]"` | `PyJWT` | `SessionConfig(token_format="jwt")` |
|
|
90
88
|
|
|
91
89
|
Extras combine: `uv add "fastapi-cachex[redis,jwt]"`.
|
|
@@ -114,9 +112,9 @@ def build_report() -> dict:
|
|
|
114
112
|
|
|
115
113
|
@app.get("/report")
|
|
116
114
|
async def report(cache: AppCache):
|
|
117
|
-
# Cache any JSON value in your own code.
|
|
118
|
-
#
|
|
119
|
-
return await cache.get_or_set("report", build_report, ttl=300
|
|
115
|
+
# Cache any JSON value in your own code. Concurrent misses run
|
|
116
|
+
# build_report once: get_or_set() locks by default.
|
|
117
|
+
return await cache.get_or_set("report", build_report, ttl=300)
|
|
120
118
|
```
|
|
121
119
|
|
|
122
120
|
> [!IMPORTANT]
|
|
@@ -143,12 +141,12 @@ async def report(cache: AppCache):
|
|
|
143
141
|
|
|
144
142
|
## Documentation
|
|
145
143
|
|
|
146
|
-
- [Migrating to 0.4.0](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/) — what 0.4.0 changes and how to
|
|
144
|
+
- [Migrating to 0.4.0](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/) — what 0.4.0 changes and how to upgrade from 0.3.x
|
|
147
145
|
- [HTTP caching](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/) — the `@cache` decorator, Cache-Control directives, cache keys, invalidation and monitoring routes
|
|
148
146
|
- [Application cache](https://fastapi-cachex.readthedocs.io/en/latest/APP_CACHE/) — `CacheManager`
|
|
149
147
|
- [Backends](https://fastapi-cachex.readthedocs.io/en/latest/BACKENDS/) — choosing and configuring a backend, atomic primitives
|
|
150
148
|
- [Distributed lock](https://fastapi-cachex.readthedocs.io/en/latest/LOCK/) — `CacheLock` for multi-process mutual exclusion
|
|
151
|
-
- [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
149
|
+
- Deprecated, removed in 0.5.0: [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
152
150
|
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
153
151
|
- [Runnable examples](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples) — one complete app per feature, each covered by the test suite
|
|
154
152
|
- [API reference](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
[English](https://fastapi-cachex.readthedocs.io/en/latest/) | [繁體中文](https://fastapi-cachex.readthedocs.io/zh-tw/latest/)
|
|
16
16
|
|
|
17
|
-
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, application-level caching
|
|
17
|
+
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, and application-level caching.
|
|
18
18
|
|
|
19
19
|
**Documentation:** <https://fastapi-cachex.readthedocs.io/en/latest/> — guides and the full API reference.
|
|
20
20
|
|
|
@@ -26,9 +26,9 @@ A high-performance caching extension for FastAPI: a server-side response cache w
|
|
|
26
26
|
your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`.
|
|
27
27
|
- **Backends** — in-memory, Redis and Memcached, with atomic counters,
|
|
28
28
|
one-shot values and locks.
|
|
29
|
-
- **Sessions (
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
- **Sessions and OAuth state (deprecated)**: signed session tokens and one-time
|
|
30
|
+
OAuth state tokens. Both are deprecated in 0.4.0 and removed in 0.5.0; see
|
|
31
|
+
[where to move](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/#session-state-deprecated).
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
34
34
|
|
|
@@ -42,7 +42,7 @@ backends and the optional session transports ship as extras:
|
|
|
42
42
|
| Extra | Install | Pulls in | Needed for |
|
|
43
43
|
|-------|---------|----------|------------|
|
|
44
44
|
| `redis` | `uv add "fastapi-cachex[redis]"` | `redis[hiredis]`, `orjson` | `AsyncRedisCacheBackend` |
|
|
45
|
-
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend`
|
|
45
|
+
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend` |
|
|
46
46
|
| `jwt` | `uv add "fastapi-cachex[jwt]"` | `PyJWT` | `SessionConfig(token_format="jwt")` |
|
|
47
47
|
|
|
48
48
|
Extras combine: `uv add "fastapi-cachex[redis,jwt]"`.
|
|
@@ -71,9 +71,9 @@ def build_report() -> dict:
|
|
|
71
71
|
|
|
72
72
|
@app.get("/report")
|
|
73
73
|
async def report(cache: AppCache):
|
|
74
|
-
# Cache any JSON value in your own code.
|
|
75
|
-
#
|
|
76
|
-
return await cache.get_or_set("report", build_report, ttl=300
|
|
74
|
+
# Cache any JSON value in your own code. Concurrent misses run
|
|
75
|
+
# build_report once: get_or_set() locks by default.
|
|
76
|
+
return await cache.get_or_set("report", build_report, ttl=300)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
> [!IMPORTANT]
|
|
@@ -100,12 +100,12 @@ async def report(cache: AppCache):
|
|
|
100
100
|
|
|
101
101
|
## Documentation
|
|
102
102
|
|
|
103
|
-
- [Migrating to 0.4.0](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/) — what 0.4.0 changes and how to
|
|
103
|
+
- [Migrating to 0.4.0](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/) — what 0.4.0 changes and how to upgrade from 0.3.x
|
|
104
104
|
- [HTTP caching](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/) — the `@cache` decorator, Cache-Control directives, cache keys, invalidation and monitoring routes
|
|
105
105
|
- [Application cache](https://fastapi-cachex.readthedocs.io/en/latest/APP_CACHE/) — `CacheManager`
|
|
106
106
|
- [Backends](https://fastapi-cachex.readthedocs.io/en/latest/BACKENDS/) — choosing and configuring a backend, atomic primitives
|
|
107
107
|
- [Distributed lock](https://fastapi-cachex.readthedocs.io/en/latest/LOCK/) — `CacheLock` for multi-process mutual exclusion
|
|
108
|
-
- [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
108
|
+
- Deprecated, removed in 0.5.0: [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
109
109
|
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
110
110
|
- [Runnable examples](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples) — one complete app per feature, each covered by the test suite
|
|
111
111
|
- [API reference](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""FastAPI-CacheX: A powerful and flexible caching extension for FastAPI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from importlib.metadata import PackageNotFoundError
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .cache import build_cache_key as build_cache_key
|
|
10
|
+
from .cache import cache as cache
|
|
11
|
+
from .cache import default_key_builder as default_key_builder
|
|
12
|
+
from .cache import invalidate as invalidate
|
|
13
|
+
from .cache_key import CacheKey as CacheKey
|
|
14
|
+
from .dependencies import AppCache as AppCache
|
|
15
|
+
from .dependencies import CacheBackend as CacheBackend
|
|
16
|
+
from .dependencies import get_app_cache as get_app_cache
|
|
17
|
+
from .dependencies import get_cache_backend as get_cache_backend
|
|
18
|
+
from .exceptions import BackendNotFoundError as BackendNotFoundError
|
|
19
|
+
from .exceptions import CacheXError as CacheXError
|
|
20
|
+
from .exceptions import LockTimeoutError as LockTimeoutError
|
|
21
|
+
from .exceptions import ProxyNotSetError as ProxyNotSetError
|
|
22
|
+
from .exceptions import RequestNotFoundError as RequestNotFoundError
|
|
23
|
+
from .lock import CacheLock as CacheLock
|
|
24
|
+
from .manager import CacheManager as CacheManager
|
|
25
|
+
from .manager_proxy import CacheManagerProxy as CacheManagerProxy
|
|
26
|
+
from .proxy import BackendProxy as BackendProxy
|
|
27
|
+
from .routes import add_routes as add_routes
|
|
28
|
+
from .types import CacheKeyBuilder as CacheKeyBuilder
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
# Type checkers see the real types; at runtime `__getattr__` loads them.
|
|
32
|
+
from .session import (
|
|
33
|
+
FastAPICacheXSessionMiddleware as FastAPICacheXSessionMiddleware,
|
|
34
|
+
)
|
|
35
|
+
from .session import Session as Session
|
|
36
|
+
from .session import SessionConfig as SessionConfig
|
|
37
|
+
from .session import SessionManager as SessionManager
|
|
38
|
+
from .session import SessionManagerProxy as SessionManagerProxy
|
|
39
|
+
from .session import SessionUser as SessionUser
|
|
40
|
+
from .session import get_optional_session as get_optional_session
|
|
41
|
+
from .session import get_session as get_session
|
|
42
|
+
from .session import get_session_manager as get_session_manager
|
|
43
|
+
from .session import require_session as require_session
|
|
44
|
+
from .session import require_user_session as require_user_session
|
|
45
|
+
from .session.exceptions import SessionError as SessionError
|
|
46
|
+
from .session.exceptions import SessionExpiredError as SessionExpiredError
|
|
47
|
+
from .session.exceptions import SessionInvalidError as SessionInvalidError
|
|
48
|
+
from .session.exceptions import SessionNotFoundError as SessionNotFoundError
|
|
49
|
+
from .session.exceptions import SessionSecurityError as SessionSecurityError
|
|
50
|
+
from .session.exceptions import SessionTokenError as SessionTokenError
|
|
51
|
+
from .state import InvalidStateError as InvalidStateError
|
|
52
|
+
from .state import StateData as StateData
|
|
53
|
+
from .state import StateDataError as StateDataError
|
|
54
|
+
from .state import StateError as StateError
|
|
55
|
+
from .state import StateExpiredError as StateExpiredError
|
|
56
|
+
from .state import StateManager as StateManager
|
|
57
|
+
from .state import StateManagerDep as StateManagerDep
|
|
58
|
+
from .state import StateManagerProxy as StateManagerProxy
|
|
59
|
+
from .state import get_state_manager as get_state_manager
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_version() -> str:
|
|
63
|
+
"""Return the installed distribution's version.
|
|
64
|
+
|
|
65
|
+
Importing from a source tree that was never installed leaves no metadata to
|
|
66
|
+
read; reporting a development version there is part of the contract, so this
|
|
67
|
+
lives in a function the tests can drive rather than behind a coverage pragma.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
return version("fastapi-cachex")
|
|
71
|
+
except PackageNotFoundError:
|
|
72
|
+
return "0.0.0.dev0"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__version__ = _read_version()
|
|
76
|
+
|
|
77
|
+
_package_logger = logging.getLogger("fastapi_cachex")
|
|
78
|
+
_package_logger.addHandler(
|
|
79
|
+
logging.NullHandler()
|
|
80
|
+
) # Attach a NullHandler to avoid "No handler found" warnings in user applications.
|
|
81
|
+
|
|
82
|
+
# Session and OAuth state names, deprecated in 0.4.0 and removed in 0.5.0
|
|
83
|
+
# (#420). They load on first use, so `import fastapi_cachex` does not import
|
|
84
|
+
# either package; importing one emits its FutureWarning. They are left out of
|
|
85
|
+
# `__all__`, so `from fastapi_cachex import *` does not load them either.
|
|
86
|
+
_DEPRECATED_NAMES = {
|
|
87
|
+
"FastAPICacheXSessionMiddleware": "fastapi_cachex.session",
|
|
88
|
+
"InvalidStateError": "fastapi_cachex.state",
|
|
89
|
+
"Session": "fastapi_cachex.session",
|
|
90
|
+
"SessionConfig": "fastapi_cachex.session",
|
|
91
|
+
"SessionError": "fastapi_cachex.session.exceptions",
|
|
92
|
+
"SessionExpiredError": "fastapi_cachex.session.exceptions",
|
|
93
|
+
"SessionInvalidError": "fastapi_cachex.session.exceptions",
|
|
94
|
+
"SessionManager": "fastapi_cachex.session",
|
|
95
|
+
"SessionManagerProxy": "fastapi_cachex.session",
|
|
96
|
+
"SessionNotFoundError": "fastapi_cachex.session.exceptions",
|
|
97
|
+
"SessionSecurityError": "fastapi_cachex.session.exceptions",
|
|
98
|
+
"SessionTokenError": "fastapi_cachex.session.exceptions",
|
|
99
|
+
"SessionUser": "fastapi_cachex.session",
|
|
100
|
+
"StateData": "fastapi_cachex.state",
|
|
101
|
+
"StateDataError": "fastapi_cachex.state",
|
|
102
|
+
"StateError": "fastapi_cachex.state",
|
|
103
|
+
"StateExpiredError": "fastapi_cachex.state",
|
|
104
|
+
"StateManager": "fastapi_cachex.state",
|
|
105
|
+
"StateManagerDep": "fastapi_cachex.state",
|
|
106
|
+
"StateManagerProxy": "fastapi_cachex.state",
|
|
107
|
+
"get_optional_session": "fastapi_cachex.session",
|
|
108
|
+
"get_session": "fastapi_cachex.session",
|
|
109
|
+
"get_session_manager": "fastapi_cachex.session",
|
|
110
|
+
"get_state_manager": "fastapi_cachex.state",
|
|
111
|
+
"require_session": "fastapi_cachex.session",
|
|
112
|
+
"require_user_session": "fastapi_cachex.session",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def __getattr__(name: str) -> object:
|
|
117
|
+
"""Resolve a deprecated session or state name on first use."""
|
|
118
|
+
module_name = _DEPRECATED_NAMES.get(name)
|
|
119
|
+
if module_name is None:
|
|
120
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
121
|
+
raise AttributeError(msg)
|
|
122
|
+
value = getattr(import_module(module_name), name)
|
|
123
|
+
globals()[name] = value
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"AppCache",
|
|
129
|
+
"BackendNotFoundError",
|
|
130
|
+
"BackendProxy",
|
|
131
|
+
"CacheBackend",
|
|
132
|
+
"CacheKey",
|
|
133
|
+
"CacheKeyBuilder",
|
|
134
|
+
"CacheLock",
|
|
135
|
+
"CacheManager",
|
|
136
|
+
"CacheManagerProxy",
|
|
137
|
+
"CacheXError",
|
|
138
|
+
"LockTimeoutError",
|
|
139
|
+
"ProxyNotSetError",
|
|
140
|
+
"RequestNotFoundError",
|
|
141
|
+
"__version__",
|
|
142
|
+
"add_routes",
|
|
143
|
+
"build_cache_key",
|
|
144
|
+
"cache",
|
|
145
|
+
"default_key_builder",
|
|
146
|
+
"get_app_cache",
|
|
147
|
+
"get_cache_backend",
|
|
148
|
+
"invalidate",
|
|
149
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Deprecation of the session and OAuth state subsystems (#420).
|
|
2
|
+
|
|
3
|
+
Both packages warn once, when they are first imported, and are removed in
|
|
4
|
+
0.5.0 (#421).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import warnings
|
|
9
|
+
from types import FrameType
|
|
10
|
+
|
|
11
|
+
_MIGRATION_URL = (
|
|
12
|
+
"https://fastapi-cachex.readthedocs.io/en/latest/"
|
|
13
|
+
"MIGRATING_0_4/#session-state-deprecated"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
SESSION_DEPRECATION = (
|
|
17
|
+
"fastapi_cachex.session is deprecated and will be removed in "
|
|
18
|
+
"fastapi-cachex 0.5.0. Use Starlette's SessionMiddleware for signed-cookie "
|
|
19
|
+
f"sessions, or a dedicated session library for server-side sessions; see {_MIGRATION_URL}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
STATE_DEPRECATION = (
|
|
23
|
+
"fastapi_cachex.state is deprecated and will be removed in "
|
|
24
|
+
"fastapi-cachex 0.5.0. Use the state handling of your OAuth client library "
|
|
25
|
+
f"(Authlib, for example); see {_MIGRATION_URL}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_import_machinery(frame: FrameType) -> bool:
|
|
30
|
+
"""Whether ``warnings.warn()`` skips ``frame`` when it counts stacklevel."""
|
|
31
|
+
filename = frame.f_code.co_filename
|
|
32
|
+
return "importlib" in filename and "_bootstrap" in filename
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _importer_stacklevel() -> int:
|
|
36
|
+
"""Return the ``stacklevel`` of the first frame outside this package.
|
|
37
|
+
|
|
38
|
+
Frames of fastapi_cachex and of the import machinery are skipped, so the
|
|
39
|
+
warning names the application's ``import`` line, or the line that read a
|
|
40
|
+
deprecated name from the ``fastapi_cachex`` package.
|
|
41
|
+
"""
|
|
42
|
+
frame = inspect.currentframe()
|
|
43
|
+
if frame is None or frame.f_back is None: # no frame support
|
|
44
|
+
return 2
|
|
45
|
+
# Level 1 is warn_deprecated(); start at its caller.
|
|
46
|
+
level, frame = 2, frame.f_back.f_back
|
|
47
|
+
while frame is not None:
|
|
48
|
+
module = frame.f_globals.get("__name__", "")
|
|
49
|
+
if not module.startswith(("fastapi_cachex.", "importlib.")) and module not in {
|
|
50
|
+
"fastapi_cachex",
|
|
51
|
+
"importlib",
|
|
52
|
+
}:
|
|
53
|
+
break
|
|
54
|
+
# warnings.warn() does not count the frozen import machinery's frames
|
|
55
|
+
# towards stacklevel, so neither may this.
|
|
56
|
+
if not _is_import_machinery(frame):
|
|
57
|
+
level += 1
|
|
58
|
+
frame = frame.f_back
|
|
59
|
+
return level
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def warn_deprecated(message: str) -> None:
|
|
63
|
+
"""Emit the ``FutureWarning`` for a deprecated subsystem.
|
|
64
|
+
|
|
65
|
+
``FutureWarning`` rather than ``DeprecationWarning``: it is shown by
|
|
66
|
+
default, and the removal affects the application, not only its tests.
|
|
67
|
+
"""
|
|
68
|
+
warnings.warn(message, FutureWarning, stacklevel=_importer_stacklevel())
|
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
12
|
+
from fastapi_cachex.types import HTTP_KEY_FORMAT_TAG
|
|
12
13
|
from fastapi_cachex.types import CacheEntry
|
|
13
14
|
from fastapi_cachex.types import counter_entry
|
|
14
15
|
from fastapi_cachex.types import counter_value
|
|
@@ -30,13 +31,14 @@ def warn_if_path_shaped(pattern: str, cleared: int) -> None:
|
|
|
30
31
|
paths (stored directly through ``set``) stay silent when they work.
|
|
31
32
|
"""
|
|
32
33
|
if cleared == 0 and pattern.startswith("/") and CACHE_KEY_SEPARATOR not in pattern:
|
|
34
|
+
sep = CACHE_KEY_SEPARATOR
|
|
33
35
|
warnings.warn(
|
|
34
36
|
f"clear_pattern({pattern!r}) cleared nothing. Patterns match whole "
|
|
35
|
-
f"cache keys, which look like 'method{
|
|
36
|
-
f"{
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
f"
|
|
37
|
+
f"cache keys, which look like '{HTTP_KEY_FORMAT_TAG}{sep}method{sep}"
|
|
38
|
+
f"host{sep}path{sep}query', so a bare path matches no HTTP cache "
|
|
39
|
+
"entry. Use clear_path(path, include_params=True) to clear by "
|
|
40
|
+
"path, or write the whole key out as "
|
|
41
|
+
f"'{HTTP_KEY_FORMAT_TAG}{sep}GET{sep}*{sep}{pattern}{sep}*'.",
|
|
40
42
|
RuntimeWarning,
|
|
41
43
|
stacklevel=3,
|
|
42
44
|
)
|
|
@@ -138,23 +140,50 @@ class BaseCacheBackend(ABC):
|
|
|
138
140
|
"""
|
|
139
141
|
|
|
140
142
|
@abstractmethod
|
|
141
|
-
async def delete(self, key: str) ->
|
|
142
|
-
"""Remove a response from the cache.
|
|
143
|
+
async def delete(self, key: str) -> bool:
|
|
144
|
+
"""Remove a response from the cache.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Whether ``key`` held an entry that had not expired yet
|
|
148
|
+
"""
|
|
143
149
|
|
|
144
150
|
async def delete_many(self, keys: Iterable[str]) -> int:
|
|
145
151
|
"""Remove every key in ``keys``; returns how many were removed.
|
|
146
152
|
|
|
147
|
-
The base implementation deletes one key at a time and
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
actually removed.
|
|
153
|
+
The base implementation deletes one key at a time and counts the
|
|
154
|
+
deletes that found an entry. The built-in backends override it with
|
|
155
|
+
batched deletes.
|
|
151
156
|
"""
|
|
152
157
|
count = 0
|
|
153
158
|
for key in keys:
|
|
154
|
-
await self.
|
|
155
|
-
|
|
159
|
+
if await self._delete_reporting(key):
|
|
160
|
+
count += 1
|
|
156
161
|
return count
|
|
157
162
|
|
|
163
|
+
async def _delete_reporting(self, key: str) -> bool:
|
|
164
|
+
"""Call ``delete`` for the fallbacks, accepting a 0.3.x ``None`` result.
|
|
165
|
+
|
|
166
|
+
``delete`` returned ``None`` before 0.4.0, and a third-party backend
|
|
167
|
+
written then may still do so. ``None`` counts as removed, as every
|
|
168
|
+
fallback assumed in 0.3.x, and warns: 0.5.0 will treat it as ``False``.
|
|
169
|
+
``FutureWarning`` rather than ``DeprecationWarning``: the warning is
|
|
170
|
+
raised inside the package, where a ``DeprecationWarning`` is hidden by
|
|
171
|
+
default, and the change affects the application at runtime.
|
|
172
|
+
"""
|
|
173
|
+
result: object = await self.delete(key)
|
|
174
|
+
if result is None:
|
|
175
|
+
warnings.warn(
|
|
176
|
+
f"{type(self).__name__}.delete() returned None. Since "
|
|
177
|
+
"fastapi-cachex 0.4.0 it must return whether the key was "
|
|
178
|
+
"removed; None is counted as removed until 0.5.0, which treats "
|
|
179
|
+
"it as False. See https://fastapi-cachex.readthedocs.io/en/"
|
|
180
|
+
"stable/MIGRATING_0_4/#backend-delete",
|
|
181
|
+
FutureWarning,
|
|
182
|
+
stacklevel=3,
|
|
183
|
+
)
|
|
184
|
+
return True
|
|
185
|
+
return bool(result)
|
|
186
|
+
|
|
158
187
|
async def get_and_delete(self, key: str) -> CacheEntry | None:
|
|
159
188
|
"""Atomically retrieve and remove a cached entry.
|
|
160
189
|
|
|
@@ -170,8 +199,10 @@ class BaseCacheBackend(ABC):
|
|
|
170
199
|
The entry that was stored under ``key``, or ``None`` if there was none
|
|
171
200
|
"""
|
|
172
201
|
value = await self.get(key)
|
|
173
|
-
if value is not
|
|
174
|
-
|
|
202
|
+
if value is None or not await self._delete_reporting(key):
|
|
203
|
+
# Absent, or another caller removed it between the get and the
|
|
204
|
+
# delete: that caller got the entry.
|
|
205
|
+
return None
|
|
175
206
|
return value
|
|
176
207
|
|
|
177
208
|
async def set_if_absent(
|
|
@@ -224,8 +255,7 @@ class BaseCacheBackend(ABC):
|
|
|
224
255
|
"""
|
|
225
256
|
if await self.get(key) != expected:
|
|
226
257
|
return False
|
|
227
|
-
await self.
|
|
228
|
-
return True
|
|
258
|
+
return await self._delete_reporting(key)
|
|
229
259
|
|
|
230
260
|
async def expire_if_equals(self, key: str, expected: CacheEntry, ttl: int) -> bool:
|
|
231
261
|
"""Update expiry on ``key`` to ``ttl`` seconds only while it still holds ``expected``.
|
|
@@ -253,6 +283,39 @@ class BaseCacheBackend(ABC):
|
|
|
253
283
|
await self.set(key, expected, ttl=ttl)
|
|
254
284
|
return True
|
|
255
285
|
|
|
286
|
+
async def set_if_equals(
|
|
287
|
+
self,
|
|
288
|
+
key: str,
|
|
289
|
+
expected: CacheEntry,
|
|
290
|
+
value: CacheEntry,
|
|
291
|
+
ttl: int | None = None,
|
|
292
|
+
) -> bool:
|
|
293
|
+
"""Store ``value`` only while ``key`` still holds ``expected``.
|
|
294
|
+
|
|
295
|
+
A compare-and-set: a caller that read ``expected`` earlier overwrites
|
|
296
|
+
it only if nothing changed, deleted or expired the key since. Sessions
|
|
297
|
+
save through it, so a request that loaded a session cannot bring it
|
|
298
|
+
back after another request deleted or invalidated it.
|
|
299
|
+
|
|
300
|
+
The base implementation is a best-effort, NON-atomic get-compare-set
|
|
301
|
+
fallback for third-party backends; the built-in backends override it
|
|
302
|
+
with an atomic implementation.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
key: Cache key to overwrite
|
|
306
|
+
expected: The entry the caller last read or wrote (compared with ``==``)
|
|
307
|
+
value: Entry to store in its place
|
|
308
|
+
ttl: Time to live in seconds (``None`` = never expires)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Whether ``value`` was stored
|
|
312
|
+
"""
|
|
313
|
+
validate_ttl(ttl)
|
|
314
|
+
if await self.get(key) != expected:
|
|
315
|
+
return False
|
|
316
|
+
await self.set(key, value, ttl=ttl)
|
|
317
|
+
return True
|
|
318
|
+
|
|
256
319
|
async def increment(self, key: str, delta: int = 1, ttl: int | None = None) -> int:
|
|
257
320
|
"""Atomically add ``delta`` to the integer counter stored at ``key``.
|
|
258
321
|
|
|
@@ -311,10 +374,11 @@ class BaseCacheBackend(ABC):
|
|
|
311
374
|
|
|
312
375
|
The pattern is matched against the whole logical key — the key as the
|
|
313
376
|
caller sees it, without whatever prefix the backend adds internally.
|
|
314
|
-
HTTP cache keys are ``method
|
|
315
|
-
path means writing the other components
|
|
377
|
+
HTTP cache keys are ``http:v2|method|host|path|query`` (see
|
|
378
|
+
``CacheKey``), so matching a path means writing the other components
|
|
379
|
+
out::
|
|
316
380
|
|
|
317
|
-
await backend.clear_pattern("GET
|
|
381
|
+
await backend.clear_pattern("http:v2|GET|*|/users/*")
|
|
318
382
|
await backend.clear_pattern("cache:user:*") # a CacheManager key
|
|
319
383
|
|
|
320
384
|
To clear by path, prefer ``clear_path(path, include_params=...)``: it
|
|
@@ -46,7 +46,7 @@ def encode_entry(entry: CacheEntry) -> bytes:
|
|
|
46
46
|
"content": entry.content.decode("latin-1"),
|
|
47
47
|
"media_type": entry.media_type,
|
|
48
48
|
"status_code": entry.status_code,
|
|
49
|
-
"headers": entry.headers,
|
|
49
|
+
"headers": [[name, value] for name, value in entry.headers],
|
|
50
50
|
"stored_at": entry.stored_at,
|
|
51
51
|
},
|
|
52
52
|
)
|
|
@@ -72,7 +72,9 @@ def decode_entry(raw: str | bytes | None) -> CacheEntry | None:
|
|
|
72
72
|
|
|
73
73
|
Documents written before entries carried a status code and headers simply
|
|
74
74
|
lack those keys and decode to a plain ``200`` with no extra headers; those
|
|
75
|
-
written before entries carried ``stored_at`` decode with ``None``.
|
|
75
|
+
written before entries carried ``stored_at`` decode with ``None``. Headers
|
|
76
|
+
are a list of ``[name, value]`` lines; the object 0.3.x wrote (one value
|
|
77
|
+
per name) is still read.
|
|
76
78
|
"""
|
|
77
79
|
if raw is None:
|
|
78
80
|
return None
|
|
@@ -16,14 +16,6 @@ class RedisConfig(BaseModel):
|
|
|
16
16
|
default=None, description="Redis server password"
|
|
17
17
|
)
|
|
18
18
|
db: int = Field(default=0, ge=0, description="Redis database number")
|
|
19
|
-
encoding: str = Field(
|
|
20
|
-
default="utf-8",
|
|
21
|
-
description=(
|
|
22
|
-
"Deprecated, removed in 0.4.0: leave it unset. Character encoding "
|
|
23
|
-
"the client decodes replies with; setting it emits a "
|
|
24
|
-
"DeprecationWarning in load_from_config()."
|
|
25
|
-
),
|
|
26
|
-
)
|
|
27
19
|
socket_timeout: float = Field(
|
|
28
20
|
default=1.0, description="Timeout for socket operations in seconds"
|
|
29
21
|
)
|