fastapi-cachex 0.3.9__tar.gz → 0.4.1__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.1}/PKG-INFO +11 -13
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/README.md +10 -10
- fastapi_cachex-0.4.1/fastapi_cachex/__init__.py +149 -0
- fastapi_cachex-0.4.1/fastapi_cachex/_deprecation.py +68 -0
- fastapi_cachex-0.4.1/fastapi_cachex/_warnings.py +25 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/base.py +103 -21
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/codec.py +4 -2
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/config.py +0 -8
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/memcached.py +125 -39
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/memory.py +46 -35
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/redis.py +113 -143
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/cache.py +503 -175
- fastapi_cachex-0.4.1/fastapi_cachex/cache_key.py +224 -0
- fastapi_cachex-0.4.1/fastapi_cachex/exceptions.py +26 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/headers.py +9 -4
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/lock.py +48 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/manager.py +8 -35
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/proxy.py +0 -37
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/routes.py +30 -78
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/__init__.py +12 -3
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/config.py +43 -26
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/dependencies.py +105 -56
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/manager.py +132 -24
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/middleware.py +51 -241
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/models.py +51 -1
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/token_serializers.py +14 -17
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/state/__init__.py +10 -1
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/types.py +51 -4
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/pyproject.toml +3 -2
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/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.1}/LICENSE +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/manager_proxy.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/proxy.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/state/dependencies.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/state/manager.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.1}/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.1
|
|
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())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Point a warning at the application's line rather than the library's (#333)."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def caller_stacklevel() -> int:
|
|
7
|
+
"""Return the ``stacklevel`` of the first frame outside fastapi_cachex.
|
|
8
|
+
|
|
9
|
+
For a ``warnings.warn`` in the function that calls this. The depth of a
|
|
10
|
+
library call varies (a backend method called directly, through
|
|
11
|
+
``CacheManager`` or through a base-class fallback), so a fixed
|
|
12
|
+
``stacklevel`` names a library file on some paths.
|
|
13
|
+
"""
|
|
14
|
+
frame = inspect.currentframe()
|
|
15
|
+
if frame is None or frame.f_back is None: # no frame support
|
|
16
|
+
return 2
|
|
17
|
+
# Level 1 is the function that warns; start at its caller.
|
|
18
|
+
level, frame = 2, frame.f_back.f_back
|
|
19
|
+
while frame is not None:
|
|
20
|
+
module = frame.f_globals.get("__name__", "")
|
|
21
|
+
if module != "fastapi_cachex" and not module.startswith("fastapi_cachex."):
|
|
22
|
+
break
|
|
23
|
+
frame = frame.f_back
|
|
24
|
+
level += 1
|
|
25
|
+
return level
|
|
@@ -8,7 +8,10 @@ from types import TracebackType
|
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from fastapi_cachex._warnings import caller_stacklevel
|
|
12
|
+
from fastapi_cachex.exceptions import CacheXError
|
|
11
13
|
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
14
|
+
from fastapi_cachex.types import HTTP_KEY_FORMAT_TAG
|
|
12
15
|
from fastapi_cachex.types import CacheEntry
|
|
13
16
|
from fastapi_cachex.types import counter_entry
|
|
14
17
|
from fastapi_cachex.types import counter_value
|
|
@@ -30,13 +33,14 @@ def warn_if_path_shaped(pattern: str, cleared: int) -> None:
|
|
|
30
33
|
paths (stored directly through ``set``) stay silent when they work.
|
|
31
34
|
"""
|
|
32
35
|
if cleared == 0 and pattern.startswith("/") and CACHE_KEY_SEPARATOR not in pattern:
|
|
36
|
+
sep = CACHE_KEY_SEPARATOR
|
|
33
37
|
warnings.warn(
|
|
34
38
|
f"clear_pattern({pattern!r}) cleared nothing. Patterns match whole "
|
|
35
|
-
f"cache keys, which look like 'method{
|
|
36
|
-
f"{
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
f"
|
|
39
|
+
f"cache keys, which look like '{HTTP_KEY_FORMAT_TAG}{sep}method{sep}"
|
|
40
|
+
f"host{sep}path{sep}query', so a bare path matches no HTTP cache "
|
|
41
|
+
"entry. Use clear_path(path, include_params=True) to clear by "
|
|
42
|
+
"path, or write the whole key out as "
|
|
43
|
+
f"'{HTTP_KEY_FORMAT_TAG}{sep}GET{sep}*{sep}{pattern}{sep}*'.",
|
|
40
44
|
RuntimeWarning,
|
|
41
45
|
stacklevel=3,
|
|
42
46
|
)
|
|
@@ -95,6 +99,18 @@ def validate_delta(delta: int) -> int:
|
|
|
95
99
|
return delta
|
|
96
100
|
|
|
97
101
|
|
|
102
|
+
def check_counter_range(value: int) -> int:
|
|
103
|
+
"""Return ``value`` if a signed 64-bit counter can hold it.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
CacheXError: If ``value`` is outside the signed 64-bit range
|
|
107
|
+
"""
|
|
108
|
+
if not -(2**63) <= value < 2**63:
|
|
109
|
+
msg = "Counter increment would overflow a signed 64-bit integer"
|
|
110
|
+
raise CacheXError(msg)
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
98
114
|
class BaseCacheBackend(ABC):
|
|
99
115
|
"""Base class for all cache backends.
|
|
100
116
|
|
|
@@ -138,23 +154,50 @@ class BaseCacheBackend(ABC):
|
|
|
138
154
|
"""
|
|
139
155
|
|
|
140
156
|
@abstractmethod
|
|
141
|
-
async def delete(self, key: str) ->
|
|
142
|
-
"""Remove a response from the cache.
|
|
157
|
+
async def delete(self, key: str) -> bool:
|
|
158
|
+
"""Remove a response from the cache.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Whether ``key`` held an entry that had not expired yet
|
|
162
|
+
"""
|
|
143
163
|
|
|
144
164
|
async def delete_many(self, keys: Iterable[str]) -> int:
|
|
145
165
|
"""Remove every key in ``keys``; returns how many were removed.
|
|
146
166
|
|
|
147
|
-
The base implementation deletes one key at a time and
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
actually removed.
|
|
167
|
+
The base implementation deletes one key at a time and counts the
|
|
168
|
+
deletes that found an entry. The built-in backends override it with
|
|
169
|
+
batched deletes.
|
|
151
170
|
"""
|
|
152
171
|
count = 0
|
|
153
172
|
for key in keys:
|
|
154
|
-
await self.
|
|
155
|
-
|
|
173
|
+
if await self._delete_reporting(key):
|
|
174
|
+
count += 1
|
|
156
175
|
return count
|
|
157
176
|
|
|
177
|
+
async def _delete_reporting(self, key: str) -> bool:
|
|
178
|
+
"""Call ``delete`` for the fallbacks, accepting a 0.3.x ``None`` result.
|
|
179
|
+
|
|
180
|
+
``delete`` returned ``None`` before 0.4.0, and a third-party backend
|
|
181
|
+
written then may still do so. ``None`` counts as removed, as every
|
|
182
|
+
fallback assumed in 0.3.x, and warns: 0.5.0 will treat it as ``False``.
|
|
183
|
+
``FutureWarning`` rather than ``DeprecationWarning``: the warning is
|
|
184
|
+
raised inside the package, where a ``DeprecationWarning`` is hidden by
|
|
185
|
+
default, and the change affects the application at runtime.
|
|
186
|
+
"""
|
|
187
|
+
result: object = await self.delete(key)
|
|
188
|
+
if result is None:
|
|
189
|
+
warnings.warn(
|
|
190
|
+
f"{type(self).__name__}.delete() returned None. Since "
|
|
191
|
+
"fastapi-cachex 0.4.0 it must return whether the key was "
|
|
192
|
+
"removed; None is counted as removed until 0.5.0, which treats "
|
|
193
|
+
"it as False. See https://fastapi-cachex.readthedocs.io/en/"
|
|
194
|
+
"stable/MIGRATING_0_4/#backend-delete",
|
|
195
|
+
FutureWarning,
|
|
196
|
+
stacklevel=caller_stacklevel(),
|
|
197
|
+
)
|
|
198
|
+
return True
|
|
199
|
+
return bool(result)
|
|
200
|
+
|
|
158
201
|
async def get_and_delete(self, key: str) -> CacheEntry | None:
|
|
159
202
|
"""Atomically retrieve and remove a cached entry.
|
|
160
203
|
|
|
@@ -170,8 +213,10 @@ class BaseCacheBackend(ABC):
|
|
|
170
213
|
The entry that was stored under ``key``, or ``None`` if there was none
|
|
171
214
|
"""
|
|
172
215
|
value = await self.get(key)
|
|
173
|
-
if value is not
|
|
174
|
-
|
|
216
|
+
if value is None or not await self._delete_reporting(key):
|
|
217
|
+
# Absent, or another caller removed it between the get and the
|
|
218
|
+
# delete: that caller got the entry.
|
|
219
|
+
return None
|
|
175
220
|
return value
|
|
176
221
|
|
|
177
222
|
async def set_if_absent(
|
|
@@ -224,8 +269,7 @@ class BaseCacheBackend(ABC):
|
|
|
224
269
|
"""
|
|
225
270
|
if await self.get(key) != expected:
|
|
226
271
|
return False
|
|
227
|
-
await self.
|
|
228
|
-
return True
|
|
272
|
+
return await self._delete_reporting(key)
|
|
229
273
|
|
|
230
274
|
async def expire_if_equals(self, key: str, expected: CacheEntry, ttl: int) -> bool:
|
|
231
275
|
"""Update expiry on ``key`` to ``ttl`` seconds only while it still holds ``expected``.
|
|
@@ -253,6 +297,39 @@ class BaseCacheBackend(ABC):
|
|
|
253
297
|
await self.set(key, expected, ttl=ttl)
|
|
254
298
|
return True
|
|
255
299
|
|
|
300
|
+
async def set_if_equals(
|
|
301
|
+
self,
|
|
302
|
+
key: str,
|
|
303
|
+
expected: CacheEntry,
|
|
304
|
+
value: CacheEntry,
|
|
305
|
+
ttl: int | None = None,
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""Store ``value`` only while ``key`` still holds ``expected``.
|
|
308
|
+
|
|
309
|
+
A compare-and-set: a caller that read ``expected`` earlier overwrites
|
|
310
|
+
it only if nothing changed, deleted or expired the key since. Sessions
|
|
311
|
+
save through it, so a request that loaded a session cannot bring it
|
|
312
|
+
back after another request deleted or invalidated it.
|
|
313
|
+
|
|
314
|
+
The base implementation is a best-effort, NON-atomic get-compare-set
|
|
315
|
+
fallback for third-party backends; the built-in backends override it
|
|
316
|
+
with an atomic implementation.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
key: Cache key to overwrite
|
|
320
|
+
expected: The entry the caller last read or wrote (compared with ``==``)
|
|
321
|
+
value: Entry to store in its place
|
|
322
|
+
ttl: Time to live in seconds (``None`` = never expires)
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Whether ``value`` was stored
|
|
326
|
+
"""
|
|
327
|
+
validate_ttl(ttl)
|
|
328
|
+
if await self.get(key) != expected:
|
|
329
|
+
return False
|
|
330
|
+
await self.set(key, value, ttl=ttl)
|
|
331
|
+
return True
|
|
332
|
+
|
|
256
333
|
async def increment(self, key: str, delta: int = 1, ttl: int | None = None) -> int:
|
|
257
334
|
"""Atomically add ``delta`` to the integer counter stored at ``key``.
|
|
258
335
|
|
|
@@ -277,7 +354,10 @@ class BaseCacheBackend(ABC):
|
|
|
277
354
|
The counter value after the increment
|
|
278
355
|
|
|
279
356
|
Raises:
|
|
280
|
-
CacheXError: If ``key`` holds a cached response instead of a
|
|
357
|
+
CacheXError: If ``key`` holds a cached response instead of a
|
|
358
|
+
counter, or the result would leave the counter's range: signed
|
|
359
|
+
64-bit, except on Memcached, whose counters are unsigned 64-bit.
|
|
360
|
+
The counter is left unchanged.
|
|
281
361
|
TypeError: If ``delta`` or ``ttl`` is not an ``int``
|
|
282
362
|
ValueError: If ``ttl`` is out of range, or ``delta`` does not fit
|
|
283
363
|
in a signed 64-bit integer
|
|
@@ -286,6 +366,7 @@ class BaseCacheBackend(ABC):
|
|
|
286
366
|
validate_ttl(ttl)
|
|
287
367
|
current = await self.get(key)
|
|
288
368
|
value = delta if current is None else counter_value(current) + delta
|
|
369
|
+
check_counter_range(value)
|
|
289
370
|
await self.set(key, counter_entry(value), ttl=ttl)
|
|
290
371
|
return value
|
|
291
372
|
|
|
@@ -311,10 +392,11 @@ class BaseCacheBackend(ABC):
|
|
|
311
392
|
|
|
312
393
|
The pattern is matched against the whole logical key — the key as the
|
|
313
394
|
caller sees it, without whatever prefix the backend adds internally.
|
|
314
|
-
HTTP cache keys are ``method
|
|
315
|
-
path means writing the other components
|
|
395
|
+
HTTP cache keys are ``http:v2|method|host|path|query`` (see
|
|
396
|
+
``CacheKey``), so matching a path means writing the other components
|
|
397
|
+
out::
|
|
316
398
|
|
|
317
|
-
await backend.clear_pattern("GET
|
|
399
|
+
await backend.clear_pattern("http:v2|GET|*|/users/*")
|
|
318
400
|
await backend.clear_pattern("cache:user:*") # a CacheManager key
|
|
319
401
|
|
|
320
402
|
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
|
)
|