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.
Files changed (45) hide show
  1. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/PKG-INFO +11 -13
  2. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/README.md +10 -10
  3. fastapi_cachex-0.4.0/fastapi_cachex/__init__.py +149 -0
  4. fastapi_cachex-0.4.0/fastapi_cachex/_deprecation.py +68 -0
  5. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/base.py +84 -20
  6. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/codec.py +4 -2
  7. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/config.py +0 -8
  8. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memcached.py +58 -11
  9. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memory.py +32 -29
  10. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/redis.py +106 -141
  11. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/cache.py +105 -113
  12. fastapi_cachex-0.4.0/fastapi_cachex/cache_key.py +224 -0
  13. fastapi_cachex-0.4.0/fastapi_cachex/exceptions.py +26 -0
  14. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/headers.py +9 -4
  15. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/manager.py +8 -35
  16. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/proxy.py +0 -37
  17. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/routes.py +30 -78
  18. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/__init__.py +12 -3
  19. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/config.py +43 -26
  20. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/dependencies.py +105 -56
  21. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/manager.py +132 -24
  22. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/middleware.py +51 -241
  23. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/models.py +51 -1
  24. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/token_serializers.py +14 -17
  25. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/__init__.py +10 -1
  26. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/types.py +51 -4
  27. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/pyproject.toml +3 -2
  28. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/pyproject.toml.orig +6 -3
  29. fastapi_cachex-0.3.9/fastapi_cachex/__init__.py +0 -123
  30. fastapi_cachex-0.3.9/fastapi_cachex/exceptions.py +0 -58
  31. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/LICENSE +0 -0
  32. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/__init__.py +0 -0
  33. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/dependencies.py +0 -0
  34. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/directives.py +0 -0
  35. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/lock.py +0 -0
  36. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/manager_proxy.py +0 -0
  37. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/py.typed +0 -0
  38. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/exceptions.py +0 -0
  39. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/proxy.py +0 -0
  40. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/session/security.py +0 -0
  41. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/dependencies.py +0 -0
  42. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/exceptions.py +0 -0
  43. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/manager.py +0 -0
  44. {fastapi_cachex-0.3.9 → fastapi_cachex-0.4.0}/fastapi_cachex/state/models.py +0 -0
  45. {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.9
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, and optional session management.
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 (optional)** — HMAC-signed or JWT session tokens over headers,
73
- bearer tokens or cookies, with sliding expiration and IP/User-Agent binding.
74
- - **OAuth state** — one-time state tokens for CSRF protection in OAuth/OIDC flows.
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` (the older `memcache` name still works until 0.4.0) |
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. lock=True runs build_report once
118
- # for concurrent misses (the default from 0.4.0).
119
- return await cache.get_or_set("report", build_report, ttl=300, lock=True)
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 act on the 0.3.9 warnings now
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, and optional session management.
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 (optional)** — HMAC-signed or JWT session tokens over headers,
30
- bearer tokens or cookies, with sliding expiration and IP/User-Agent binding.
31
- - **OAuth state** — one-time state tokens for CSRF protection in OAuth/OIDC flows.
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` (the older `memcache` name still works until 0.4.0) |
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. lock=True runs build_report once
75
- # for concurrent misses (the default from 0.4.0).
76
- return await cache.get_or_set("report", build_report, ttl=300, lock=True)
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 act on the 0.3.9 warnings now
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{CACHE_KEY_SEPARATOR}host"
36
- f"{CACHE_KEY_SEPARATOR}path{CACHE_KEY_SEPARATOR}query', so a bare "
37
- "path matches no HTTP cache entry. Use clear_path(path, "
38
- "include_params=True) to clear by path, or write the whole key out "
39
- f"as 'GET{CACHE_KEY_SEPARATOR}*{CACHE_KEY_SEPARATOR}{pattern}'.",
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) -> None:
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 reports how
148
- many were attempted, since ``delete`` does not say whether the key
149
- existed. The built-in backends override it and count what was
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.delete(key)
155
- count += 1
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 None:
174
- await self.delete(key)
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.delete(key)
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|||host|||path|||query``, so matching a
315
- path means writing the other components out::
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|||*|||/users/*")
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
  )