fastapi-cachex 0.2.1__tar.gz → 0.2.3__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.2.1 → fastapi_cachex-0.2.3}/PKG-INFO +23 -5
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/README.md +19 -2
- fastapi_cachex-0.2.3/fastapi_cachex/__init__.py +27 -0
- fastapi_cachex-0.2.3/fastapi_cachex/backends/__init__.py +13 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/backends/memcached.py +21 -2
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/backends/memory.py +33 -5
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/backends/redis.py +29 -6
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/cache.py +59 -19
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/dependencies.py +2 -2
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/proxy.py +9 -2
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/routes.py +6 -5
- fastapi_cachex-0.2.3/fastapi_cachex/session/__init__.py +21 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/config.py +70 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/dependencies.py +65 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/exceptions.py +25 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/manager.py +389 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/middleware.py +149 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/models.py +185 -0
- fastapi_cachex-0.2.3/fastapi_cachex/session/security.py +111 -0
- fastapi_cachex-0.2.3/fastapi_cachex/state/__init__.py +8 -0
- fastapi_cachex-0.2.3/fastapi_cachex/state/exceptions.py +19 -0
- fastapi_cachex-0.2.3/fastapi_cachex/state/manager.py +258 -0
- fastapi_cachex-0.2.3/fastapi_cachex/state/models.py +31 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/types.py +9 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/pyproject.toml +3 -2
- fastapi_cachex-0.2.1/fastapi_cachex/__init__.py +0 -7
- fastapi_cachex-0.2.1/fastapi_cachex/backends/__init__.py +0 -13
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/backends/base.py +0 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.2.1 → fastapi_cachex-0.2.3}/fastapi_cachex/py.typed +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-cachex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
-
Author:
|
|
7
|
-
Author-email:
|
|
6
|
+
Author: allen0099
|
|
7
|
+
Author-email: allen0099 <s96016641@gmail.com>
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
Classifier: Development Status :: 3 - Alpha
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -20,6 +20,7 @@ Classifier: Framework :: FastAPI
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
22
22
|
Requires-Dist: fastapi
|
|
23
|
+
Requires-Dist: pydantic
|
|
23
24
|
Requires-Dist: pymemcache ; extra == 'memcache'
|
|
24
25
|
Requires-Dist: redis[hiredis] ; extra == 'redis'
|
|
25
26
|
Requires-Dist: orjson ; extra == 'redis'
|
|
@@ -47,10 +48,11 @@ Description-Content-Type: text/markdown
|
|
|
47
48
|
|
|
48
49
|
[English](README.md) | [繁體中文](docs/README.zh-TW.md)
|
|
49
50
|
|
|
50
|
-
A high-performance caching extension for FastAPI, providing comprehensive HTTP caching support.
|
|
51
|
+
A high-performance caching extension for FastAPI, providing comprehensive HTTP caching support and optional session management.
|
|
51
52
|
|
|
52
53
|
## Features
|
|
53
54
|
|
|
55
|
+
### HTTP Caching
|
|
54
56
|
- Support for HTTP caching headers
|
|
55
57
|
- `Cache-Control`
|
|
56
58
|
- `ETag`
|
|
@@ -62,6 +64,15 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
62
64
|
- Complete Cache-Control directive implementation
|
|
63
65
|
- Easy-to-use `@cache` decorator
|
|
64
66
|
|
|
67
|
+
### Session Management (Optional Extension)
|
|
68
|
+
- Secure session management with HMAC-SHA256 token signing
|
|
69
|
+
- IP address and User-Agent binding (optional security features)
|
|
70
|
+
- Header and bearer token support (API-first architecture)
|
|
71
|
+
- Automatic session renewal (sliding expiration)
|
|
72
|
+
- Flash messages for cross-request communication
|
|
73
|
+
- Multiple backend support (Redis, Memcached, In-Memory)
|
|
74
|
+
- Complete session lifecycle management (create, validate, refresh, invalidate)
|
|
75
|
+
|
|
65
76
|
### Cache-Control Directives
|
|
66
77
|
|
|
67
78
|
| Directive | Supported | Description |
|
|
@@ -86,6 +97,12 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
86
97
|
uv add fastapi-cachex
|
|
87
98
|
```
|
|
88
99
|
|
|
100
|
+
### Development Installation
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv add git+https://github.com/allen0099/FastAPI-CacheX.git
|
|
104
|
+
```
|
|
105
|
+
|
|
89
106
|
## Quick Start
|
|
90
107
|
|
|
91
108
|
```python
|
|
@@ -129,7 +146,7 @@ FastAPI-CacheX supports multiple caching backends. You can easily switch between
|
|
|
129
146
|
Cache keys are generated in the following format to avoid collisions:
|
|
130
147
|
|
|
131
148
|
```
|
|
132
|
-
{method}
|
|
149
|
+
{method}|||{host}|||{path}|||{query_params}
|
|
133
150
|
```
|
|
134
151
|
|
|
135
152
|
This ensures that:
|
|
@@ -236,6 +253,7 @@ async def expensive_operation():
|
|
|
236
253
|
- [Cache Flow Explanation](docs/CACHE_FLOW.md)
|
|
237
254
|
- [Development Guide](docs/DEVELOPMENT.md)
|
|
238
255
|
- [Contributing Guidelines](docs/CONTRIBUTING.md)
|
|
256
|
+
- [Session Management Guide](docs/SESSION.md) - Complete guide for session features
|
|
239
257
|
|
|
240
258
|
## License
|
|
241
259
|
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
[English](README.md) | [繁體中文](docs/README.zh-TW.md)
|
|
16
16
|
|
|
17
|
-
A high-performance caching extension for FastAPI, providing comprehensive HTTP caching support.
|
|
17
|
+
A high-performance caching extension for FastAPI, providing comprehensive HTTP caching support and optional session management.
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
+
### HTTP Caching
|
|
21
22
|
- Support for HTTP caching headers
|
|
22
23
|
- `Cache-Control`
|
|
23
24
|
- `ETag`
|
|
@@ -29,6 +30,15 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
29
30
|
- Complete Cache-Control directive implementation
|
|
30
31
|
- Easy-to-use `@cache` decorator
|
|
31
32
|
|
|
33
|
+
### Session Management (Optional Extension)
|
|
34
|
+
- Secure session management with HMAC-SHA256 token signing
|
|
35
|
+
- IP address and User-Agent binding (optional security features)
|
|
36
|
+
- Header and bearer token support (API-first architecture)
|
|
37
|
+
- Automatic session renewal (sliding expiration)
|
|
38
|
+
- Flash messages for cross-request communication
|
|
39
|
+
- Multiple backend support (Redis, Memcached, In-Memory)
|
|
40
|
+
- Complete session lifecycle management (create, validate, refresh, invalidate)
|
|
41
|
+
|
|
32
42
|
### Cache-Control Directives
|
|
33
43
|
|
|
34
44
|
| Directive | Supported | Description |
|
|
@@ -53,6 +63,12 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
53
63
|
uv add fastapi-cachex
|
|
54
64
|
```
|
|
55
65
|
|
|
66
|
+
### Development Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv add git+https://github.com/allen0099/FastAPI-CacheX.git
|
|
70
|
+
```
|
|
71
|
+
|
|
56
72
|
## Quick Start
|
|
57
73
|
|
|
58
74
|
```python
|
|
@@ -96,7 +112,7 @@ FastAPI-CacheX supports multiple caching backends. You can easily switch between
|
|
|
96
112
|
Cache keys are generated in the following format to avoid collisions:
|
|
97
113
|
|
|
98
114
|
```
|
|
99
|
-
{method}
|
|
115
|
+
{method}|||{host}|||{path}|||{query_params}
|
|
100
116
|
```
|
|
101
117
|
|
|
102
118
|
This ensures that:
|
|
@@ -203,6 +219,7 @@ async def expensive_operation():
|
|
|
203
219
|
- [Cache Flow Explanation](docs/CACHE_FLOW.md)
|
|
204
220
|
- [Development Guide](docs/DEVELOPMENT.md)
|
|
205
221
|
- [Contributing Guidelines](docs/CONTRIBUTING.md)
|
|
222
|
+
- [Session Management Guide](docs/SESSION.md) - Complete guide for session features
|
|
206
223
|
|
|
207
224
|
## License
|
|
208
225
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""FastAPI-CacheX: A powerful and flexible caching extension for FastAPI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .cache import cache as cache
|
|
6
|
+
from .cache import default_key_builder as default_key_builder
|
|
7
|
+
from .dependencies import CacheBackend as CacheBackend
|
|
8
|
+
from .dependencies import get_cache_backend as get_cache_backend
|
|
9
|
+
from .proxy import BackendProxy as BackendProxy
|
|
10
|
+
from .routes import add_routes as add_routes
|
|
11
|
+
from .types import CacheKeyBuilder as CacheKeyBuilder
|
|
12
|
+
|
|
13
|
+
_package_logger = logging.getLogger("fastapi_cachex")
|
|
14
|
+
_package_logger.addHandler(
|
|
15
|
+
logging.NullHandler()
|
|
16
|
+
) # Attach a NullHandler to avoid "No handler found" warnings in user applications.
|
|
17
|
+
|
|
18
|
+
# Session management (optional feature)
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BackendProxy",
|
|
21
|
+
"CacheBackend",
|
|
22
|
+
"CacheKeyBuilder",
|
|
23
|
+
"add_routes",
|
|
24
|
+
"cache",
|
|
25
|
+
"default_key_builder",
|
|
26
|
+
"get_cache_backend",
|
|
27
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Cache backend implementations for FastAPI-CacheX."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseCacheBackend
|
|
4
|
+
from .memcached import MemcachedBackend
|
|
5
|
+
from .memory import MemoryBackend
|
|
6
|
+
from .redis import AsyncRedisCacheBackend
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AsyncRedisCacheBackend",
|
|
10
|
+
"BaseCacheBackend",
|
|
11
|
+
"MemcachedBackend",
|
|
12
|
+
"MemoryBackend",
|
|
13
|
+
]
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
"""Memcached cache backend implementation."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import warnings
|
|
4
5
|
|
|
5
|
-
from fastapi_cachex.backends.base import BaseCacheBackend
|
|
6
6
|
from fastapi_cachex.exceptions import CacheXError
|
|
7
7
|
from fastapi_cachex.types import ETagContent
|
|
8
8
|
|
|
9
|
+
from .base import BaseCacheBackend
|
|
10
|
+
|
|
9
11
|
try:
|
|
10
12
|
import orjson as json
|
|
11
13
|
|
|
12
14
|
except ImportError: # pragma: no cover
|
|
13
15
|
import json # type: ignore[no-redef] # pragma: no cover
|
|
14
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
15
19
|
# Default Memcached key prefix for fastapi-cachex
|
|
16
20
|
DEFAULT_MEMCACHE_PREFIX = "fastapi_cachex:"
|
|
17
21
|
|
|
@@ -70,11 +74,13 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
70
74
|
prefixed_key = self._make_key(key)
|
|
71
75
|
value = self.client.get(prefixed_key)
|
|
72
76
|
if value is None:
|
|
77
|
+
logger.debug("Memcached MISS; key=%s", key)
|
|
73
78
|
return None
|
|
74
79
|
|
|
75
80
|
# Memcached stores data as bytes; deserialize from JSON
|
|
76
81
|
try:
|
|
77
82
|
data = json.loads(value.decode("utf-8"))
|
|
83
|
+
logger.debug("Memcached HIT; key=%s", key)
|
|
78
84
|
return ETagContent(
|
|
79
85
|
etag=data["etag"],
|
|
80
86
|
content=data["content"].encode()
|
|
@@ -82,6 +88,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
82
88
|
else data["content"],
|
|
83
89
|
)
|
|
84
90
|
except (json.JSONDecodeError, KeyError, ValueError):
|
|
91
|
+
logger.debug("Memcached DESERIALIZE ERROR; key=%s", key)
|
|
85
92
|
return None
|
|
86
93
|
|
|
87
94
|
async def set(self, key: str, value: ETagContent, ttl: int | None = None) -> None:
|
|
@@ -119,6 +126,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
119
126
|
serialized_bytes,
|
|
120
127
|
expire=ttl if ttl is not None else 0,
|
|
121
128
|
)
|
|
129
|
+
logger.debug("Memcached SET; key=%s ttl=%s", key, ttl)
|
|
122
130
|
|
|
123
131
|
async def delete(self, key: str) -> None:
|
|
124
132
|
"""Delete value from cache.
|
|
@@ -127,6 +135,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
127
135
|
key: Cache key to delete
|
|
128
136
|
"""
|
|
129
137
|
self.client.delete(self._make_key(key))
|
|
138
|
+
logger.debug("Memcached DELETE; key=%s", key)
|
|
130
139
|
|
|
131
140
|
async def clear(self) -> None:
|
|
132
141
|
"""Clear all values from cache.
|
|
@@ -142,6 +151,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
142
151
|
stacklevel=2,
|
|
143
152
|
)
|
|
144
153
|
self.client.flush_all()
|
|
154
|
+
logger.debug("Memcached CLEAR; flush_all issued")
|
|
145
155
|
|
|
146
156
|
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
147
157
|
"""Clear cached responses for a specific path.
|
|
@@ -175,9 +185,15 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
175
185
|
except Exception: # noqa: BLE001
|
|
176
186
|
return 0
|
|
177
187
|
else:
|
|
188
|
+
logger.debug(
|
|
189
|
+
"Memcached CLEAR_PATH; path=%s include_params=%s removed=%s",
|
|
190
|
+
path,
|
|
191
|
+
include_params,
|
|
192
|
+
1 if result else 0,
|
|
193
|
+
)
|
|
178
194
|
return 1 if result else 0
|
|
179
195
|
|
|
180
|
-
async def clear_pattern(self, pattern: str) -> int:
|
|
196
|
+
async def clear_pattern(self, pattern: str) -> int:
|
|
181
197
|
"""Clear cached responses matching a pattern.
|
|
182
198
|
|
|
183
199
|
Memcached does not support pattern matching or key scanning.
|
|
@@ -197,6 +213,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
197
213
|
RuntimeWarning,
|
|
198
214
|
stacklevel=2,
|
|
199
215
|
)
|
|
216
|
+
logger.debug("Memcached CLEAR_PATTERN unsupported; pattern=%s", pattern)
|
|
200
217
|
return 0
|
|
201
218
|
|
|
202
219
|
async def get_all_keys(self) -> list[str]:
|
|
@@ -218,6 +235,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
218
235
|
RuntimeWarning,
|
|
219
236
|
stacklevel=2,
|
|
220
237
|
)
|
|
238
|
+
logger.debug("Memcached GET_ALL_KEYS unsupported; returning empty list")
|
|
221
239
|
return []
|
|
222
240
|
|
|
223
241
|
async def get_cache_data(self) -> dict[str, tuple[ETagContent, float | None]]:
|
|
@@ -236,4 +254,5 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
236
254
|
RuntimeWarning,
|
|
237
255
|
stacklevel=2,
|
|
238
256
|
)
|
|
257
|
+
logger.debug("Memcached GET_CACHE_DATA unsupported; returning empty dict")
|
|
239
258
|
return {}
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
5
|
import fnmatch
|
|
6
|
+
import logging
|
|
6
7
|
import time
|
|
7
8
|
|
|
9
|
+
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
8
10
|
from fastapi_cachex.types import CacheItem
|
|
9
11
|
from fastapi_cachex.types import ETagContent
|
|
10
12
|
|
|
11
13
|
from .base import BaseCacheBackend
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Cache keys are formatted as: method|||host|||path|||query_params
|
|
14
18
|
# Minimum parts required to extract path component
|
|
15
19
|
_MIN_KEY_PARTS = 3
|
|
16
20
|
# Maximum parts to split (method, host, path, query_params)
|
|
@@ -43,6 +47,10 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
43
47
|
with contextlib.suppress(RuntimeError):
|
|
44
48
|
# No event loop yet; will be created on first async operation
|
|
45
49
|
self._cleanup_task = asyncio.create_task(self._cleanup_task_impl())
|
|
50
|
+
logger.debug(
|
|
51
|
+
"Started memory backend cleanup task (interval=%s)",
|
|
52
|
+
self.cleanup_interval,
|
|
53
|
+
)
|
|
46
54
|
|
|
47
55
|
def start_cleanup(self) -> None:
|
|
48
56
|
"""Start the cleanup task if it's not already running.
|
|
@@ -56,6 +64,7 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
56
64
|
if self._cleanup_task is not None:
|
|
57
65
|
self._cleanup_task.cancel()
|
|
58
66
|
self._cleanup_task = None
|
|
67
|
+
logger.debug("Stopped memory backend cleanup task")
|
|
59
68
|
|
|
60
69
|
async def get(self, key: str) -> ETagContent | None:
|
|
61
70
|
"""Retrieve a cached response.
|
|
@@ -69,10 +78,13 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
69
78
|
cached_item = self.cache.get(key)
|
|
70
79
|
if cached_item:
|
|
71
80
|
if cached_item.expiry is None or cached_item.expiry > time.time():
|
|
81
|
+
logger.debug("Memory cache HIT; key=%s", key)
|
|
72
82
|
return cached_item.value
|
|
73
83
|
# Entry has expired; clean it up
|
|
74
84
|
del self.cache[key]
|
|
85
|
+
logger.debug("Memory cache EXPIRED; key=%s removed", key)
|
|
75
86
|
return None
|
|
87
|
+
logger.debug("Memory cache MISS; key=%s", key)
|
|
76
88
|
return None
|
|
77
89
|
|
|
78
90
|
async def set(self, key: str, value: ETagContent, ttl: int | None = None) -> None:
|
|
@@ -86,16 +98,19 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
86
98
|
async with self.lock:
|
|
87
99
|
expiry = time.time() + ttl if ttl is not None else None
|
|
88
100
|
self.cache[key] = CacheItem(value=value, expiry=expiry)
|
|
101
|
+
logger.debug("Memory cache SET; key=%s ttl=%s", key, ttl)
|
|
89
102
|
|
|
90
103
|
async def delete(self, key: str) -> None:
|
|
91
104
|
"""Remove a response from the cache."""
|
|
92
105
|
async with self.lock:
|
|
93
106
|
self.cache.pop(key, None)
|
|
107
|
+
logger.debug("Memory cache DELETE; key=%s", key)
|
|
94
108
|
|
|
95
109
|
async def clear(self) -> None:
|
|
96
110
|
"""Clear all cached responses."""
|
|
97
111
|
async with self.lock:
|
|
98
112
|
self.cache.clear()
|
|
113
|
+
logger.debug("Memory cache CLEAR; all entries removed")
|
|
99
114
|
|
|
100
115
|
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
101
116
|
"""Clear cached responses for a specific path.
|
|
@@ -115,8 +130,8 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
115
130
|
async with self.lock:
|
|
116
131
|
keys_to_delete = []
|
|
117
132
|
for key in self.cache:
|
|
118
|
-
# Keys are formatted as: method
|
|
119
|
-
parts = key.split(
|
|
133
|
+
# Keys are formatted as: method|||host|||path|||query_params
|
|
134
|
+
parts = key.split(CACHE_KEY_SEPARATOR, _MAX_KEY_PARTS)
|
|
120
135
|
if len(parts) >= _MIN_KEY_PARTS:
|
|
121
136
|
cache_path = parts[2]
|
|
122
137
|
has_params = len(parts) > _MIN_KEY_PARTS
|
|
@@ -127,6 +142,12 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
127
142
|
for key in keys_to_delete:
|
|
128
143
|
del self.cache[key]
|
|
129
144
|
|
|
145
|
+
logger.debug(
|
|
146
|
+
"Memory cache CLEAR_PATH; path=%s include_params=%s removed=%s",
|
|
147
|
+
path,
|
|
148
|
+
include_params,
|
|
149
|
+
cleared_count,
|
|
150
|
+
)
|
|
130
151
|
return cleared_count
|
|
131
152
|
|
|
132
153
|
async def clear_pattern(self, pattern: str) -> int:
|
|
@@ -145,8 +166,8 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
145
166
|
async with self.lock:
|
|
146
167
|
keys_to_delete = []
|
|
147
168
|
for key in self.cache:
|
|
148
|
-
# Extract path component (method
|
|
149
|
-
parts = key.split(
|
|
169
|
+
# Extract path component (method|||host|||path|||query_params)
|
|
170
|
+
parts = key.split(CACHE_KEY_SEPARATOR, _MAX_KEY_PARTS)
|
|
150
171
|
if len(parts) >= _MIN_KEY_PARTS:
|
|
151
172
|
cache_path = parts[2]
|
|
152
173
|
if fnmatch.fnmatch(cache_path, pattern):
|
|
@@ -156,6 +177,9 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
156
177
|
for key in keys_to_delete:
|
|
157
178
|
del self.cache[key]
|
|
158
179
|
|
|
180
|
+
logger.debug(
|
|
181
|
+
"Memory cache CLEAR_PATTERN; pattern=%s removed=%s", pattern, cleared_count
|
|
182
|
+
)
|
|
159
183
|
return cleared_count
|
|
160
184
|
|
|
161
185
|
async def get_all_keys(self) -> list[str]:
|
|
@@ -196,3 +220,7 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
196
220
|
]
|
|
197
221
|
for key in expired_keys:
|
|
198
222
|
self.cache.pop(key, None)
|
|
223
|
+
if expired_keys:
|
|
224
|
+
logger.debug(
|
|
225
|
+
"Memory cache CLEANUP; expired removed=%s", len(expired_keys)
|
|
226
|
+
)
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Redis cache backend implementation."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Any
|
|
5
6
|
from typing import Literal
|
|
6
7
|
|
|
7
|
-
from fastapi_cachex.backends.base import BaseCacheBackend
|
|
8
8
|
from fastapi_cachex.exceptions import CacheXError
|
|
9
|
+
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
9
10
|
from fastapi_cachex.types import ETagContent
|
|
10
11
|
|
|
12
|
+
from .base import BaseCacheBackend
|
|
13
|
+
|
|
11
14
|
if TYPE_CHECKING:
|
|
12
15
|
from redis.asyncio import Redis as AsyncRedis
|
|
13
16
|
|
|
@@ -17,6 +20,8 @@ try:
|
|
|
17
20
|
except ImportError: # pragma: no cover
|
|
18
21
|
import json # type: ignore[no-redef] # pragma: no cover
|
|
19
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
20
25
|
# Default Redis key prefix for fastapi-cachex
|
|
21
26
|
DEFAULT_REDIS_PREFIX = "fastapi_cachex:"
|
|
22
27
|
|
|
@@ -116,7 +121,9 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
116
121
|
async def get(self, key: str) -> ETagContent | None:
|
|
117
122
|
"""Retrieve a cached response."""
|
|
118
123
|
result = await self.client.get(self._make_key(key))
|
|
119
|
-
|
|
124
|
+
value = self._deserialize(result)
|
|
125
|
+
logger.debug("Redis %s; key=%s", "HIT" if value else "MISS", key)
|
|
126
|
+
return value
|
|
120
127
|
|
|
121
128
|
async def set(self, key: str, value: ETagContent, ttl: int | None = None) -> None:
|
|
122
129
|
"""Store a response in the cache."""
|
|
@@ -126,10 +133,12 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
126
133
|
await self.client.setex(prefixed_key, ttl, serialized)
|
|
127
134
|
else:
|
|
128
135
|
await self.client.set(prefixed_key, serialized)
|
|
136
|
+
logger.debug("Redis SET; key=%s ttl=%s", key, ttl)
|
|
129
137
|
|
|
130
138
|
async def delete(self, key: str) -> None:
|
|
131
139
|
"""Remove a response from the cache."""
|
|
132
140
|
await self.client.delete(self._make_key(key))
|
|
141
|
+
logger.debug("Redis DELETE; key=%s", key)
|
|
133
142
|
|
|
134
143
|
async def clear(self) -> None:
|
|
135
144
|
"""Clear all cached responses for this namespace.
|
|
@@ -160,6 +169,7 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
160
169
|
batch = keys_to_delete[i : i + batch_size]
|
|
161
170
|
if batch:
|
|
162
171
|
await self.client.delete(*batch)
|
|
172
|
+
logger.debug("Redis CLEAR; removed=%s", len(keys_to_delete))
|
|
163
173
|
|
|
164
174
|
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
165
175
|
"""Clear cached responses for a specific path.
|
|
@@ -175,11 +185,13 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
175
185
|
"""
|
|
176
186
|
# Pattern includes the HTTP method, host, and path components
|
|
177
187
|
if include_params:
|
|
178
|
-
# Clear all variations:
|
|
179
|
-
pattern =
|
|
188
|
+
# Clear all variations: *|||path|||*
|
|
189
|
+
pattern = (
|
|
190
|
+
f"{self.key_prefix}*{CACHE_KEY_SEPARATOR}{path}{CACHE_KEY_SEPARATOR}*"
|
|
191
|
+
)
|
|
180
192
|
else:
|
|
181
|
-
# Clear only exact path (no query params):
|
|
182
|
-
pattern = f"{self.key_prefix}
|
|
193
|
+
# Clear only exact path (no query params): *|||path
|
|
194
|
+
pattern = f"{self.key_prefix}*{CACHE_KEY_SEPARATOR}{path}"
|
|
183
195
|
|
|
184
196
|
cursor = 0
|
|
185
197
|
batch_size = 100
|
|
@@ -206,6 +218,12 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
206
218
|
deleted = await self.client.delete(*batch)
|
|
207
219
|
cleared_count += deleted
|
|
208
220
|
|
|
221
|
+
logger.debug(
|
|
222
|
+
"Redis CLEAR_PATH; path=%s include_params=%s removed=%s",
|
|
223
|
+
path,
|
|
224
|
+
include_params,
|
|
225
|
+
cleared_count,
|
|
226
|
+
)
|
|
209
227
|
return cleared_count
|
|
210
228
|
|
|
211
229
|
async def clear_pattern(self, pattern: str) -> int:
|
|
@@ -250,6 +268,9 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
250
268
|
deleted = await self.client.delete(*batch)
|
|
251
269
|
cleared_count += deleted
|
|
252
270
|
|
|
271
|
+
logger.debug(
|
|
272
|
+
"Redis CLEAR_PATTERN; pattern=%s removed=%s", full_pattern, cleared_count
|
|
273
|
+
)
|
|
253
274
|
return cleared_count
|
|
254
275
|
|
|
255
276
|
async def get_all_keys(self) -> list[str]:
|
|
@@ -275,6 +296,7 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
275
296
|
if cursor == 0:
|
|
276
297
|
break
|
|
277
298
|
|
|
299
|
+
logger.debug("Redis GET_ALL_KEYS; count=%s", len(all_keys))
|
|
278
300
|
return all_keys
|
|
279
301
|
|
|
280
302
|
async def get_cache_data(self) -> dict[str, tuple[ETagContent, float | None]]:
|
|
@@ -297,4 +319,5 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
297
319
|
if value is not None:
|
|
298
320
|
cache_data[original_key] = (value, None)
|
|
299
321
|
|
|
322
|
+
logger.debug("Redis GET_CACHE_DATA; keys=%s", len(cache_data))
|
|
300
323
|
return cache_data
|