fastapi-cachex 0.3.2__tar.gz → 0.3.5__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.2 → fastapi_cachex-0.3.5}/PKG-INFO +253 -12
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/README.md +251 -9
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/__init__.py +19 -0
- fastapi_cachex-0.3.5/fastapi_cachex/backends/base.py +175 -0
- fastapi_cachex-0.3.5/fastapi_cachex/backends/codec.py +79 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/memcached.py +126 -47
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/memory.py +108 -65
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/redis.py +94 -165
- fastapi_cachex-0.3.5/fastapi_cachex/cache.py +632 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/dependencies.py +13 -1
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/manager.py +6 -12
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/proxy.py +2 -2
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/routes.py +82 -120
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/config.py +43 -1
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/manager.py +55 -55
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/middleware.py +48 -46
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/security.py +9 -2
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/token_serializers.py +4 -4
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/manager.py +47 -82
- fastapi_cachex-0.3.5/fastapi_cachex/types.py +73 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/pyproject.toml +10 -4
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/pyproject.toml.orig +7 -4
- fastapi_cachex-0.3.2/fastapi_cachex/backends/base.py +0 -70
- fastapi_cachex-0.3.2/fastapi_cachex/cache.py +0 -416
- fastapi_cachex-0.3.2/fastapi_cachex/types.py +0 -34
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/config.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/manager_proxy.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/__init__.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/dependencies.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/models.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/proxy.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/__init__.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/dependencies.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/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.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -20,12 +20,12 @@ 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: itsdangerous
|
|
23
24
|
Requires-Dist: pydantic
|
|
24
25
|
Requires-Dist: pyjwt>=2.9.0 ; extra == 'jwt'
|
|
25
26
|
Requires-Dist: pymemcache ; extra == 'memcache'
|
|
26
27
|
Requires-Dist: redis[hiredis]>=5.3.0 ; extra == 'redis'
|
|
27
28
|
Requires-Dist: orjson ; extra == 'redis'
|
|
28
|
-
Requires-Dist: itsdangerous ; extra == 'starlette'
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
30
|
Project-URL: Homepage, https://github.com/allen0099/FastAPI-CacheX
|
|
31
31
|
Project-URL: Repository, https://github.com/allen0099/FastAPI-CacheX.git
|
|
@@ -33,7 +33,6 @@ Project-URL: Issues, https://github.com/allen0099/FastAPI-CacheX/issues
|
|
|
33
33
|
Provides-Extra: jwt
|
|
34
34
|
Provides-Extra: memcache
|
|
35
35
|
Provides-Extra: redis
|
|
36
|
-
Provides-Extra: starlette
|
|
37
36
|
Description-Content-Type: text/markdown
|
|
38
37
|
|
|
39
38
|
# FastAPI-Cache X
|
|
@@ -72,7 +71,9 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
72
71
|
- Secure session management with HMAC-SHA256 token signing
|
|
73
72
|
- Optional JWT token format for interoperability (install extra `jwt`)
|
|
74
73
|
- IP address and User-Agent binding (optional security features)
|
|
75
|
-
- Header
|
|
74
|
+
- Header, bearer token and cookie transports (cookies via
|
|
75
|
+
`FastAPICacheXSessionMiddleware`; the older `SessionMiddleware` is deprecated
|
|
76
|
+
and removed in 0.4.0 — see [Session Management Guide](docs/SESSION.md))
|
|
76
77
|
- Automatic session renewal (sliding expiration)
|
|
77
78
|
- Flash messages for cross-request communication
|
|
78
79
|
- Multiple backend support (Redis, Memcached, In-Memory)
|
|
@@ -102,11 +103,19 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
|
|
|
102
103
|
uv add fastapi-cachex
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
Everything in the core package works with the in-memory backend. The other
|
|
107
|
+
backends and the optional session transports ship as extras:
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
| Extra | Install | Pulls in | Needed for |
|
|
110
|
+
|-------|---------|----------|------------|
|
|
111
|
+
| `redis` | `uv add "fastapi-cachex[redis]"` | `redis[hiredis]`, `orjson` | `AsyncRedisCacheBackend` |
|
|
112
|
+
| `memcache` | `uv add "fastapi-cachex[memcache]"` | `pymemcache` | `MemcachedBackend` (note: `memcache`, not `memcached`) |
|
|
113
|
+
| `jwt` | `uv add "fastapi-cachex[jwt]"` | `PyJWT` | `SessionConfig(token_format="jwt")` |
|
|
114
|
+
|
|
115
|
+
The `starlette` extra is gone: `itsdangerous` is a base dependency now, so
|
|
116
|
+
`FastAPICacheXSessionMiddleware` works on a plain install.
|
|
117
|
+
|
|
118
|
+
Extras combine: `uv add "fastapi-cachex[redis,jwt]"`.
|
|
110
119
|
|
|
111
120
|
### Development Installation
|
|
112
121
|
|
|
@@ -145,9 +154,88 @@ async def non_store_endpoint():
|
|
|
145
154
|
@app.get("/clear_cache")
|
|
146
155
|
async def remove_cache(cache: CacheBackend):
|
|
147
156
|
await cache.clear_path("/path/to/clear") # Clear cache for a specific path
|
|
148
|
-
|
|
157
|
+
# Patterns match the whole key, not just the path
|
|
158
|
+
await cache.clear_pattern("GET|||*|||/path/to/clear/*")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`clear_pattern` globs the whole logical key. HTTP cache keys look like
|
|
162
|
+
`method|||host|||path|||query`, so a pattern that is only a path matches
|
|
163
|
+
nothing — use `clear_path(path, include_params=True)` when the path is what you
|
|
164
|
+
mean, and keep `clear_pattern` for keys you built yourself.
|
|
165
|
+
|
|
166
|
+
### Invalidating a Single Cached Route
|
|
167
|
+
|
|
168
|
+
`clear_path`/`clear_pattern` work on ranges of keys. To drop exactly the entry a
|
|
169
|
+
`@cache`-decorated route would use — typically right after a mutation — call
|
|
170
|
+
`invalidate()`, which rebuilds that route's key with the same key builder and
|
|
171
|
+
deletes it:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from fastapi import Request
|
|
175
|
+
from starlette.requests import Request as StarletteRequest
|
|
176
|
+
|
|
177
|
+
from fastapi_cachex import cache, invalidate
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.get("/items/{item_id}")
|
|
181
|
+
@cache(ttl=300)
|
|
182
|
+
async def read_item(item_id: int):
|
|
183
|
+
return await load(item_id)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.post("/items/{item_id}")
|
|
187
|
+
async def update_item(item_id: int, request: Request):
|
|
188
|
+
await save(item_id)
|
|
189
|
+
# Build the key the cached GET would have used: same host and headers,
|
|
190
|
+
# GET method, the cached path, no query string.
|
|
191
|
+
scope = dict(request.scope)
|
|
192
|
+
scope["method"] = "GET"
|
|
193
|
+
scope["path"] = f"/items/{item_id}"
|
|
194
|
+
scope["query_string"] = b""
|
|
195
|
+
return {"invalidated": await invalidate(StarletteRequest(scope))}
|
|
149
196
|
```
|
|
150
197
|
|
|
198
|
+
`invalidate(request, key_builder=None)` returns `True` when an entry existed and
|
|
199
|
+
was removed, `False` otherwise (including when no backend is configured — it
|
|
200
|
+
never raises). The request you hand it must produce the cached route's key:
|
|
201
|
+
same method, host, path and query string. If the cached route uses a custom
|
|
202
|
+
`key_builder`, pass the same one here, or the key will not match.
|
|
203
|
+
|
|
204
|
+
### Cache Monitoring Routes
|
|
205
|
+
|
|
206
|
+
`add_routes()` mounts two read-only endpoints that report what is currently in
|
|
207
|
+
the backend:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from fastapi import Depends, FastAPI
|
|
211
|
+
from fastapi_cachex import add_routes
|
|
212
|
+
|
|
213
|
+
app = FastAPI()
|
|
214
|
+
add_routes(
|
|
215
|
+
app,
|
|
216
|
+
prefix="/admin/cache", # default "" -> /cached-hits, /cached-records
|
|
217
|
+
include_in_schema=False, # default: hidden from OpenAPI
|
|
218
|
+
dependencies=[Depends(verify_admin)],
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `GET {prefix}/cached-hits` — per-route hit counts and cache key information.
|
|
223
|
+
- `GET {prefix}/cached-records` — every cached record with its size, expiry and
|
|
224
|
+
a preview of the cached content.
|
|
225
|
+
|
|
226
|
+
> [!WARNING]
|
|
227
|
+
> **These routes have no authentication of their own.** `include_in_schema=False`
|
|
228
|
+
> only hides them from the OpenAPI document; anyone who guesses the path can read
|
|
229
|
+
> them. `/cached-records` includes a preview of the cached content and exposes
|
|
230
|
+
> your whole route structure. In production always pass
|
|
231
|
+
> `dependencies=[Depends(your_auth)]`, or mount them on an internal-only app.
|
|
232
|
+
|
|
233
|
+
> [!NOTE]
|
|
234
|
+
> The `ttl_remaining` field is not available on the Redis backend.
|
|
235
|
+
> `AsyncRedisCacheBackend.get_cache_data()` does not issue a per-key `TTL`
|
|
236
|
+
> lookup, so Redis-backed entries are reported as never expiring. Expiry itself
|
|
237
|
+
> still happens — only the monitoring view is blind to it.
|
|
238
|
+
|
|
151
239
|
### Application-Level Caching (Manual Get/Set)
|
|
152
240
|
|
|
153
241
|
Beyond HTTP response caching via `@cache`, you can cache arbitrary JSON-serializable
|
|
@@ -157,6 +245,7 @@ namespaced wrapper around whichever backend is configured via `BackendProxy`.
|
|
|
157
245
|
```python
|
|
158
246
|
from fastapi_cachex import AppCache, CacheManager
|
|
159
247
|
|
|
248
|
+
|
|
160
249
|
@app.get("/expensive")
|
|
161
250
|
async def expensive_operation(cache: AppCache):
|
|
162
251
|
result = await cache.get("expensive:result")
|
|
@@ -172,16 +261,25 @@ await manager.set("user:42", {"name": "Alice"})
|
|
|
172
261
|
user = await manager.get("user:42") # {"name": "Alice"}
|
|
173
262
|
await manager.delete("user:42")
|
|
174
263
|
await manager.clear_prefix() # clear everything under "myapp:"
|
|
264
|
+
|
|
265
|
+
# Compute-on-miss: `factory` runs only when the key is missing, expired, or
|
|
266
|
+
# undecodable. It may be sync or async.
|
|
267
|
+
profile = await manager.get_or_set("user:42", lambda: load_user(42), ttl=300)
|
|
268
|
+
|
|
269
|
+
# Glob over this manager's namespace, using the backend's native pattern
|
|
270
|
+
# support (Redis SCAN) rather than enumerating every key.
|
|
271
|
+
await manager.clear_pattern("user:*") # matches "myapp:user:*"
|
|
175
272
|
```
|
|
176
273
|
|
|
177
|
-
`
|
|
274
|
+
`get_or_set()` provides no stampede protection: concurrent misses for the same
|
|
275
|
+
key each run `factory`. `CacheManager.get()` returns `None` (or a supplied `default=`) on a cache miss —
|
|
178
276
|
it never raises for missing or corrupted entries. `CacheManager` keys live under
|
|
179
277
|
their own `cache:`-prefixed namespace by default, separate from the HTTP route
|
|
180
278
|
cache and OAuth state, so `clear()`/`clear_prefix()` never touch unrelated cache
|
|
181
279
|
entries.
|
|
182
280
|
|
|
183
281
|
**Note**: `clear()`/`clear_prefix()` are implemented via the backend's
|
|
184
|
-
`get_all_keys()
|
|
282
|
+
`get_all_keys()` and `delete_many()` (one batched `DEL` on Redis). Since Memcached doesn't support key enumeration (see
|
|
185
283
|
[Memcached limitations](#memcached)), these two methods are no-ops on a
|
|
186
284
|
Memcached backend — `get()`/`set()`/`delete()`/`has()` work normally. Use
|
|
187
285
|
Redis or the in-memory backend if you need bulk clearing.
|
|
@@ -204,19 +302,126 @@ This ensures that:
|
|
|
204
302
|
- Different query parameters get separate cache entries
|
|
205
303
|
- The same endpoint with different parameters can be cached independently
|
|
206
304
|
|
|
305
|
+
Query parameters are taken in the order the client sent them, without sorting, so
|
|
306
|
+
`?a=1&b=2` and `?b=2&a=1` are two distinct cache entries for the same logical request.
|
|
307
|
+
|
|
207
308
|
All backends automatically namespace keys with a prefix (e.g., `fastapi_cachex:`) to avoid conflicts with other applications.
|
|
208
309
|
|
|
209
310
|
`CacheManager` (see [Application-Level Caching](#application-level-caching-manual-getset)) uses a separate, simpler `cache:`-prefixed key namespace instead of this `|||`-separated format, since its keys aren't tied to HTTP requests.
|
|
210
311
|
|
|
312
|
+
> [!WARNING]
|
|
313
|
+
> **The default cache key carries no user identity.** The backend is shared by
|
|
314
|
+
> every worker and every caller, so caching an authenticated endpoint with the
|
|
315
|
+
> default key builder will serve one user's response to the next user who hits
|
|
316
|
+
> the same path.
|
|
317
|
+
>
|
|
318
|
+
> For any endpoint whose response depends on who is asking, do one of:
|
|
319
|
+
>
|
|
320
|
+
> 1. **`private=True`** — the response is never read from or written to the
|
|
321
|
+
> shared backend. `Cache-Control: private` still lets the user's own browser
|
|
322
|
+
> cache it, and `If-None-Match` revalidation still works against freshly
|
|
323
|
+
> rendered content.
|
|
324
|
+
> 2. **A key builder that includes the caller's identity** — use this when you
|
|
325
|
+
> do want a server-side cache per user.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from fastapi_cachex import cache
|
|
329
|
+
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# 1. Keep it out of the shared cache entirely.
|
|
333
|
+
@app.get("/me/profile")
|
|
334
|
+
@cache(ttl=60, private=True)
|
|
335
|
+
async def my_profile(user: CurrentUser):
|
|
336
|
+
return user.profile
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# 2. Or give each user their own entry.
|
|
340
|
+
def per_user_key(request: Request) -> str:
|
|
341
|
+
# `request.state.user_id` is populated by your authentication layer after
|
|
342
|
+
# it has verified the caller — never read the identity straight off an
|
|
343
|
+
# unverified request header (see the note below).
|
|
344
|
+
user_id = getattr(request.state, "user_id", "anonymous")
|
|
345
|
+
return (
|
|
346
|
+
f"{request.method}{CACHE_KEY_SEPARATOR}"
|
|
347
|
+
f"{request.headers.get('host', 'unknown')}{CACHE_KEY_SEPARATOR}"
|
|
348
|
+
f"{request.url.path}{CACHE_KEY_SEPARATOR}"
|
|
349
|
+
f"{request.query_params}{CACHE_KEY_SEPARATOR}{user_id}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@app.get("/me/dashboard")
|
|
354
|
+
@cache(ttl=60, private=True, key_builder=per_user_key)
|
|
355
|
+
async def my_dashboard(user: CurrentUser):
|
|
356
|
+
return build_dashboard(user)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
> [!CAUTION]
|
|
360
|
+
> The key builder decides who sees whose data, so the identity it reads must
|
|
361
|
+
> come from something already verified — a claim from a checked token, a user
|
|
362
|
+
> your dependency resolved, or a value your auth middleware wrote to
|
|
363
|
+
> `request.state`.
|
|
364
|
+
>
|
|
365
|
+
> ```python
|
|
366
|
+
> # ❌ Never do this: anyone can send this header.
|
|
367
|
+
> user_id = request.headers.get("x-user-id", "anonymous")
|
|
368
|
+
> ```
|
|
369
|
+
>
|
|
370
|
+
> A key built from a raw request header is a horizontal privilege escalation:
|
|
371
|
+
> sending `X-User-Id: <someone-else>` returns that user's cached response.
|
|
372
|
+
|
|
211
373
|
### Cache Hit Behavior
|
|
212
374
|
|
|
213
375
|
When a cached entry is valid (within TTL):
|
|
214
|
-
- **Default behavior**: Returns the cached content with
|
|
376
|
+
- **Default behavior**: Returns the cached content directly, with the status code and headers the handler originally produced, without re-executing the endpoint handler
|
|
215
377
|
- **With `If-None-Match` header**: Returns HTTP 304 Not Modified if the ETag matches
|
|
216
378
|
- **With `no-cache` directive**: Forces revalidation with fresh content before deciding on 304
|
|
379
|
+
- **With `private=True`**: Nothing is read from or written to the shared backend; the handler runs every time and only `If-None-Match` revalidation applies
|
|
217
380
|
|
|
218
381
|
This means **cached hits are extremely fast** - the endpoint handler function is never executed.
|
|
219
382
|
|
|
383
|
+
Only successful responses are stored. A response the handler *returns* with a
|
|
384
|
+
non-2xx status (for example `Response(..., status_code=404)`) is passed straight
|
|
385
|
+
through and never cached, so a transient error cannot replace or poison the last
|
|
386
|
+
good entry. `206 Partial Content` is excluded as well, since its body is only
|
|
387
|
+
meaningful for the `Range` request that produced it. `Set-Cookie` is never
|
|
388
|
+
stored or replayed.
|
|
389
|
+
|
|
390
|
+
### Atomic backend primitives
|
|
391
|
+
|
|
392
|
+
Every backend exposes two atomic operations on top of `get`/`set`/`delete`, for
|
|
393
|
+
values that are read and written by many concurrent requests:
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from fastapi_cachex import BackendProxy
|
|
397
|
+
|
|
398
|
+
backend = BackendProxy.get()
|
|
399
|
+
|
|
400
|
+
# Fixed-window counter: created on first use, `ttl` applies only then.
|
|
401
|
+
hits = await backend.increment(f"resend:{user_id}", ttl=86400)
|
|
402
|
+
if hits > 3:
|
|
403
|
+
raise TooManyRequests()
|
|
404
|
+
|
|
405
|
+
# One-shot value: of several concurrent callers exactly one gets the entry.
|
|
406
|
+
grant = await backend.get_and_delete(f"grant:{token}")
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- `increment(key, delta=1, ttl=None) -> int` — Memory does the read-modify-write
|
|
410
|
+
under its lock, Redis runs a Lua script (`EXISTS` + `INCRBY` + `EXPIRE`) and
|
|
411
|
+
Memcached uses `ADD` + `INCR`/`DECR` (Memcached counters stop at 0). The
|
|
412
|
+
counter is visible through `get()` as a `CacheEntry` with fingerprint
|
|
413
|
+
`COUNTER_FINGERPRINT` and the decimal value as content, so `delete`/`clear*`
|
|
414
|
+
and the monitoring routes treat it like any other entry. Incrementing a key
|
|
415
|
+
that holds a cached response raises `CacheXError`.
|
|
416
|
+
- `get_and_delete(key) -> CacheEntry | None` — Memory pops under its lock, Redis
|
|
417
|
+
uses `GETDEL` (server 6.2+) and Memcached returns the value only when its own
|
|
418
|
+
`DELETE` won. `StateManager.consume_state`, `CacheManager.delete` and
|
|
419
|
+
`invalidate()` are built on it.
|
|
420
|
+
|
|
421
|
+
Both have a non-atomic fallback on `BaseCacheBackend`, so a third-party backend
|
|
422
|
+
that only implements the abstract methods keeps working; override them to get
|
|
423
|
+
real atomicity.
|
|
424
|
+
|
|
220
425
|
### In-Memory Cache (default)
|
|
221
426
|
|
|
222
427
|
If you don't specify a backend, FastAPI-CacheX will use the in-memory cache by default.
|
|
@@ -249,6 +454,11 @@ BackendProxy.set(backend)
|
|
|
249
454
|
- Keys are namespaced with `fastapi_cachex:` prefix to avoid conflicts
|
|
250
455
|
- Consider using Redis backend if you need pattern-based cache clearing
|
|
251
456
|
|
|
457
|
+
The synchronous pymemcache client runs in worker threads and is connection-pooled,
|
|
458
|
+
so concurrent requests never share a socket. Writes wait for the server's
|
|
459
|
+
acknowledgement (`default_noreply=False`), which keeps a value readable from
|
|
460
|
+
any pooled connection as soon as `set()` returns.
|
|
461
|
+
|
|
252
462
|
### Redis
|
|
253
463
|
|
|
254
464
|
```python
|
|
@@ -277,6 +487,33 @@ backend = AsyncRedisCacheBackend(
|
|
|
277
487
|
BackendProxy.set(backend)
|
|
278
488
|
```
|
|
279
489
|
|
|
490
|
+
**Configuring from a model**: `RedisConfig` is a pydantic model with the same
|
|
491
|
+
settings and validation, which is handy when they come from environment
|
|
492
|
+
variables or a settings file:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from fastapi_cachex.backends import AsyncRedisCacheBackend
|
|
496
|
+
from fastapi_cachex.backends.config import RedisConfig
|
|
497
|
+
|
|
498
|
+
config = RedisConfig(
|
|
499
|
+
host="127.0.0.1",
|
|
500
|
+
port=6379,
|
|
501
|
+
password=None, # SecretStr | None
|
|
502
|
+
db=0,
|
|
503
|
+
encoding="utf-8", # how the client decodes server responses
|
|
504
|
+
socket_timeout=1.0, # seconds; applies to reads/writes
|
|
505
|
+
socket_connect_timeout=1.0,
|
|
506
|
+
key_prefix="fastapi_cachex:",
|
|
507
|
+
protocol=2, # RESP version, 2 or 3
|
|
508
|
+
)
|
|
509
|
+
backend = AsyncRedisCacheBackend.load_from_config(config)
|
|
510
|
+
BackendProxy.set(backend)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Keep `protocol=2` unless you need RESP3 features *and* your `hiredis` build
|
|
514
|
+
supports it (RESP3 needs hiredis >= 3.0). Redis 8.0 speaks RESP3, but an older
|
|
515
|
+
hiredis will fail to negotiate it.
|
|
516
|
+
|
|
280
517
|
## Performance Considerations
|
|
281
518
|
|
|
282
519
|
### Cache Hit Performance
|
|
@@ -303,8 +540,12 @@ async def expensive_operation():
|
|
|
303
540
|
|
|
304
541
|
- [Cache Flow Explanation](docs/CACHE_FLOW.md)
|
|
305
542
|
- [Development Guide](docs/DEVELOPMENT.md)
|
|
543
|
+
- [Known Limitations and Planned Work](docs/BACKLOG.md)
|
|
544
|
+
- [Changelog](CHANGELOG.md)
|
|
306
545
|
- [Contributing Guidelines](docs/CONTRIBUTING.md)
|
|
307
546
|
- [Session Management Guide](docs/SESSION.md) - Complete guide for session features
|
|
547
|
+
- [State Management Guide](docs/STATE.md) - One-shot OAuth/CSRF state tokens
|
|
548
|
+
- [JWT Claims Guide](docs/JWT_CLAIMS.md) - Claim design and extension points for JWT session tokens
|
|
308
549
|
|
|
309
550
|
## License
|
|
310
551
|
|