fastapi-cachex 0.3.8__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/PKG-INFO +30 -14
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/README.md +29 -11
- fastapi_cachex-0.4.0/fastapi_cachex/__init__.py +149 -0
- fastapi_cachex-0.4.0/fastapi_cachex/_deprecation.py +68 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/base.py +120 -22
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/codec.py +16 -2
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/config.py +0 -1
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memcached.py +140 -34
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/memory.py +32 -27
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/redis.py +138 -85
- fastapi_cachex-0.4.0/fastapi_cachex/cache.py +1450 -0
- fastapi_cachex-0.4.0/fastapi_cachex/cache_key.py +224 -0
- fastapi_cachex-0.4.0/fastapi_cachex/exceptions.py +26 -0
- fastapi_cachex-0.4.0/fastapi_cachex/headers.py +32 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/lock.py +4 -1
- fastapi_cachex-0.4.0/fastapi_cachex/manager.py +554 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/proxy.py +12 -38
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/routes.py +52 -55
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/__init__.py +14 -3
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/config.py +103 -8
- fastapi_cachex-0.4.0/fastapi_cachex/session/dependencies.py +435 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/manager.py +152 -26
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/middleware.py +192 -175
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/models.py +51 -1
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/token_serializers.py +28 -14
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/__init__.py +10 -1
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/exceptions.py +1 -1
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/manager.py +25 -9
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/types.py +71 -4
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/pyproject.toml +3 -2
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/pyproject.toml.orig +6 -3
- fastapi_cachex-0.3.8/fastapi_cachex/__init__.py +0 -121
- fastapi_cachex-0.3.8/fastapi_cachex/cache.py +0 -810
- fastapi_cachex-0.3.8/fastapi_cachex/exceptions.py +0 -58
- fastapi_cachex-0.3.8/fastapi_cachex/manager.py +0 -253
- fastapi_cachex-0.3.8/fastapi_cachex/session/dependencies.py +0 -245
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/LICENSE +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/manager_proxy.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/proxy.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/dependencies.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.3.8 → fastapi_cachex-0.4.0}/fastapi_cachex/state/proxy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-cachex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
|
|
5
5
|
Keywords: fastapi,cache,etag,cache-control,redis,memcached,in-memory
|
|
6
6
|
Author: allen0099
|
|
@@ -26,7 +26,6 @@ Requires-Dist: itsdangerous>=1.1.0
|
|
|
26
26
|
Requires-Dist: pydantic>=2.7.0
|
|
27
27
|
Requires-Dist: starlette>=1.0.0
|
|
28
28
|
Requires-Dist: pyjwt>=2.9.0 ; extra == 'jwt'
|
|
29
|
-
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcache'
|
|
30
29
|
Requires-Dist: pymemcache>=4.0.0 ; extra == 'memcached'
|
|
31
30
|
Requires-Dist: redis[hiredis]>=5.3.0 ; extra == 'redis'
|
|
32
31
|
Requires-Dist: orjson>=3.4.7 ; extra == 'redis'
|
|
@@ -36,7 +35,6 @@ Project-URL: Repository, https://github.com/allen0099/FastAPI-CacheX.git
|
|
|
36
35
|
Project-URL: Issues, https://github.com/allen0099/FastAPI-CacheX/issues
|
|
37
36
|
Project-URL: Documentation, https://fastapi-cachex.readthedocs.io/
|
|
38
37
|
Provides-Extra: jwt
|
|
39
|
-
Provides-Extra: memcache
|
|
40
38
|
Provides-Extra: memcached
|
|
41
39
|
Provides-Extra: redis
|
|
42
40
|
Description-Content-Type: text/markdown
|
|
@@ -57,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
|
57
55
|
|
|
58
56
|
[English](https://fastapi-cachex.readthedocs.io/en/latest/) | [繁體中文](https://fastapi-cachex.readthedocs.io/zh-tw/latest/)
|
|
59
57
|
|
|
60
|
-
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, application-level caching
|
|
58
|
+
A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, and application-level caching.
|
|
61
59
|
|
|
62
60
|
**Documentation:** <https://fastapi-cachex.readthedocs.io/en/latest/> — guides and the full API reference.
|
|
63
61
|
|
|
@@ -69,9 +67,9 @@ A high-performance caching extension for FastAPI: a server-side response cache w
|
|
|
69
67
|
your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`.
|
|
70
68
|
- **Backends** — in-memory, Redis and Memcached, with atomic counters,
|
|
71
69
|
one-shot values and locks.
|
|
72
|
-
- **Sessions (
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
- **Sessions and OAuth state (deprecated)**: signed session tokens and one-time
|
|
71
|
+
OAuth state tokens. Both are deprecated in 0.4.0 and removed in 0.5.0; see
|
|
72
|
+
[where to move](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/#session-state-deprecated).
|
|
75
73
|
|
|
76
74
|
## Installation
|
|
77
75
|
|
|
@@ -85,7 +83,7 @@ backends and the optional session transports ship as extras:
|
|
|
85
83
|
| Extra | Install | Pulls in | Needed for |
|
|
86
84
|
|-------|---------|----------|------------|
|
|
87
85
|
| `redis` | `uv add "fastapi-cachex[redis]"` | `redis[hiredis]`, `orjson` | `AsyncRedisCacheBackend` |
|
|
88
|
-
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend`
|
|
86
|
+
| `memcached` | `uv add "fastapi-cachex[memcached]"` | `pymemcache` | `MemcachedBackend` |
|
|
89
87
|
| `jwt` | `uv add "fastapi-cachex[jwt]"` | `PyJWT` | `SessionConfig(token_format="jwt")` |
|
|
90
88
|
|
|
91
89
|
Extras combine: `uv add "fastapi-cachex[redis,jwt]"`.
|
|
@@ -114,27 +112,45 @@ 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.
|
|
115
|
+
# Cache any JSON value in your own code. Concurrent misses run
|
|
116
|
+
# build_report once: get_or_set() locks by default.
|
|
118
117
|
return await cache.get_or_set("report", build_report, ttl=300)
|
|
119
118
|
```
|
|
120
119
|
|
|
120
|
+
> [!IMPORTANT]
|
|
121
|
+
> Put `@cache` **below** the route decorator. FastAPI registers whatever function
|
|
122
|
+
> reaches `@app.get(...)`; with `@cache` on top, FastAPI registers the
|
|
123
|
+
> undecorated handler, so the route works but nothing is cached and nothing warns
|
|
124
|
+
> (see [Decorator order](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/#decorator-order)).
|
|
125
|
+
>
|
|
126
|
+
> ```python
|
|
127
|
+
> @app.get("/items") # ✅ route decorator first,
|
|
128
|
+
> @cache(ttl=60) # @cache directly above the function
|
|
129
|
+
> async def items(): ...
|
|
130
|
+
>
|
|
131
|
+
> @cache(ttl=60) # ❌ never called: nothing is cached
|
|
132
|
+
> @app.get("/items")
|
|
133
|
+
> async def items(): ...
|
|
134
|
+
> ```
|
|
135
|
+
|
|
121
136
|
> [!WARNING]
|
|
122
137
|
> The default cache key carries no user identity. Cache authenticated endpoints
|
|
123
|
-
> with `private=True` or a per-user key builder
|
|
138
|
+
> with `private=True` or a per-user key builder plus `cache_authorized=True`
|
|
139
|
+
> (requests with `Authorization` or a session otherwise bypass the backend) — see
|
|
124
140
|
> [Authenticated endpoints](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/#authenticated-endpoints).
|
|
125
141
|
|
|
126
142
|
## Documentation
|
|
127
143
|
|
|
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
|
|
128
145
|
- [HTTP caching](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/) — the `@cache` decorator, Cache-Control directives, cache keys, invalidation and monitoring routes
|
|
129
|
-
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
130
146
|
- [Application cache](https://fastapi-cachex.readthedocs.io/en/latest/APP_CACHE/) — `CacheManager`
|
|
131
147
|
- [Backends](https://fastapi-cachex.readthedocs.io/en/latest/BACKENDS/) — choosing and configuring a backend, atomic primitives
|
|
132
|
-
- [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
133
|
-
- [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) — one-shot OAuth/CSRF state tokens
|
|
134
148
|
- [Distributed lock](https://fastapi-cachex.readthedocs.io/en/latest/LOCK/) — `CacheLock` for multi-process mutual exclusion
|
|
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/)
|
|
150
|
+
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
135
151
|
- [Runnable examples](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples) — one complete app per feature, each covered by the test suite
|
|
136
152
|
- [API reference](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/)
|
|
137
|
-
- [Development guide](https://fastapi-cachex.readthedocs.io/en/latest/DEVELOPMENT/) and [contributing](https://fastapi-cachex.readthedocs.io/en/latest/CONTRIBUTING/)
|
|
153
|
+
- [Development guide](https://fastapi-cachex.readthedocs.io/en/latest/DEVELOPMENT/) and [contributing](https://fastapi-cachex.readthedocs.io/en/latest/CONTRIBUTING/) · [Security policy](https://github.com/allen0099/FastAPI-CacheX/blob/master/SECURITY.md) — report vulnerabilities privately, not in public issues
|
|
138
154
|
- [Changelog](https://github.com/allen0099/FastAPI-CacheX/blob/master/CHANGELOG.md) · [Known limitations and planned work](https://github.com/allen0099/FastAPI-CacheX/issues)
|
|
139
155
|
|
|
140
156
|
## License
|
|
@@ -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,27 +71,45 @@ 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.
|
|
74
|
+
# Cache any JSON value in your own code. Concurrent misses run
|
|
75
|
+
# build_report once: get_or_set() locks by default.
|
|
75
76
|
return await cache.get_or_set("report", build_report, ttl=300)
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
> [!IMPORTANT]
|
|
80
|
+
> Put `@cache` **below** the route decorator. FastAPI registers whatever function
|
|
81
|
+
> reaches `@app.get(...)`; with `@cache` on top, FastAPI registers the
|
|
82
|
+
> undecorated handler, so the route works but nothing is cached and nothing warns
|
|
83
|
+
> (see [Decorator order](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/#decorator-order)).
|
|
84
|
+
>
|
|
85
|
+
> ```python
|
|
86
|
+
> @app.get("/items") # ✅ route decorator first,
|
|
87
|
+
> @cache(ttl=60) # @cache directly above the function
|
|
88
|
+
> async def items(): ...
|
|
89
|
+
>
|
|
90
|
+
> @cache(ttl=60) # ❌ never called: nothing is cached
|
|
91
|
+
> @app.get("/items")
|
|
92
|
+
> async def items(): ...
|
|
93
|
+
> ```
|
|
94
|
+
|
|
78
95
|
> [!WARNING]
|
|
79
96
|
> The default cache key carries no user identity. Cache authenticated endpoints
|
|
80
|
-
> with `private=True` or a per-user key builder
|
|
97
|
+
> with `private=True` or a per-user key builder plus `cache_authorized=True`
|
|
98
|
+
> (requests with `Authorization` or a session otherwise bypass the backend) — see
|
|
81
99
|
> [Authenticated endpoints](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/#authenticated-endpoints).
|
|
82
100
|
|
|
83
101
|
## Documentation
|
|
84
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 upgrade from 0.3.x
|
|
85
104
|
- [HTTP caching](https://fastapi-cachex.readthedocs.io/en/latest/HTTP_CACHING/) — the `@cache` decorator, Cache-Control directives, cache keys, invalidation and monitoring routes
|
|
86
|
-
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
87
105
|
- [Application cache](https://fastapi-cachex.readthedocs.io/en/latest/APP_CACHE/) — `CacheManager`
|
|
88
106
|
- [Backends](https://fastapi-cachex.readthedocs.io/en/latest/BACKENDS/) — choosing and configuring a backend, atomic primitives
|
|
89
|
-
- [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/)
|
|
90
|
-
- [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) — one-shot OAuth/CSRF state tokens
|
|
91
107
|
- [Distributed lock](https://fastapi-cachex.readthedocs.io/en/latest/LOCK/) — `CacheLock` for multi-process mutual exclusion
|
|
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
|
+
- [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request
|
|
92
110
|
- [Runnable examples](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples) — one complete app per feature, each covered by the test suite
|
|
93
111
|
- [API reference](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/)
|
|
94
|
-
- [Development guide](https://fastapi-cachex.readthedocs.io/en/latest/DEVELOPMENT/) and [contributing](https://fastapi-cachex.readthedocs.io/en/latest/CONTRIBUTING/)
|
|
112
|
+
- [Development guide](https://fastapi-cachex.readthedocs.io/en/latest/DEVELOPMENT/) and [contributing](https://fastapi-cachex.readthedocs.io/en/latest/CONTRIBUTING/) · [Security policy](https://github.com/allen0099/FastAPI-CacheX/blob/master/SECURITY.md) — report vulnerabilities privately, not in public issues
|
|
95
113
|
- [Changelog](https://github.com/allen0099/FastAPI-CacheX/blob/master/CHANGELOG.md) · [Known limitations and planned work](https://github.com/allen0099/FastAPI-CacheX/issues)
|
|
96
114
|
|
|
97
115
|
## License
|
|
@@ -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())
|
|
@@ -4,13 +4,21 @@ import warnings
|
|
|
4
4
|
from abc import ABC
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
from collections.abc import Iterable
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
7
9
|
from typing import Any
|
|
8
10
|
|
|
9
11
|
from fastapi_cachex.types import CACHE_KEY_SEPARATOR
|
|
12
|
+
from fastapi_cachex.types import HTTP_KEY_FORMAT_TAG
|
|
10
13
|
from fastapi_cachex.types import CacheEntry
|
|
11
14
|
from fastapi_cachex.types import counter_entry
|
|
12
15
|
from fastapi_cachex.types import counter_value
|
|
13
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
# Type-only: typing.Self is 3.11+, and typing_extensions is not a runtime
|
|
19
|
+
# dependency.
|
|
20
|
+
from typing_extensions import Self
|
|
21
|
+
|
|
14
22
|
|
|
15
23
|
def warn_if_path_shaped(pattern: str, cleared: int) -> None:
|
|
16
24
|
"""Warn when a ``clear_pattern`` that cleared nothing was written as a path.
|
|
@@ -23,13 +31,14 @@ def warn_if_path_shaped(pattern: str, cleared: int) -> None:
|
|
|
23
31
|
paths (stored directly through ``set``) stay silent when they work.
|
|
24
32
|
"""
|
|
25
33
|
if cleared == 0 and pattern.startswith("/") and CACHE_KEY_SEPARATOR not in pattern:
|
|
34
|
+
sep = CACHE_KEY_SEPARATOR
|
|
26
35
|
warnings.warn(
|
|
27
36
|
f"clear_pattern({pattern!r}) cleared nothing. Patterns match whole "
|
|
28
|
-
f"cache keys, which look like 'method{
|
|
29
|
-
f"{
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
f"
|
|
37
|
+
f"cache keys, which look like '{HTTP_KEY_FORMAT_TAG}{sep}method{sep}"
|
|
38
|
+
f"host{sep}path{sep}query', so a bare path matches no HTTP cache "
|
|
39
|
+
"entry. Use clear_path(path, include_params=True) to clear by "
|
|
40
|
+
"path, or write the whole key out as "
|
|
41
|
+
f"'{HTTP_KEY_FORMAT_TAG}{sep}GET{sep}*{sep}{pattern}{sep}*'.",
|
|
33
42
|
RuntimeWarning,
|
|
34
43
|
stacklevel=3,
|
|
35
44
|
)
|
|
@@ -89,7 +98,33 @@ def validate_delta(delta: int) -> int:
|
|
|
89
98
|
|
|
90
99
|
|
|
91
100
|
class BaseCacheBackend(ABC):
|
|
92
|
-
"""Base class for all cache backends.
|
|
101
|
+
"""Base class for all cache backends.
|
|
102
|
+
|
|
103
|
+
Every backend is an async context manager: ``async with`` returns the
|
|
104
|
+
backend itself and calls ``aclose()`` on the way out.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
async def aclose(self) -> None: # noqa: B027 - a no-op default, not abstract
|
|
108
|
+
"""Release what the backend holds open: connections, background tasks.
|
|
109
|
+
|
|
110
|
+
Call it once on shutdown, typically at the end of a FastAPI lifespan.
|
|
111
|
+
It is safe to call more than once. The base implementation does
|
|
112
|
+
nothing, for backends with nothing to release; the built-in backends
|
|
113
|
+
override it.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
async def __aenter__(self) -> "Self":
|
|
117
|
+
"""Return the backend itself."""
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
async def __aexit__(
|
|
121
|
+
self,
|
|
122
|
+
exc_type: type[BaseException] | None,
|
|
123
|
+
exc_value: BaseException | None,
|
|
124
|
+
traceback: TracebackType | None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Close the backend with ``aclose()``."""
|
|
127
|
+
await self.aclose()
|
|
93
128
|
|
|
94
129
|
@abstractmethod
|
|
95
130
|
async def get(self, key: str) -> CacheEntry | None:
|
|
@@ -105,23 +140,50 @@ class BaseCacheBackend(ABC):
|
|
|
105
140
|
"""
|
|
106
141
|
|
|
107
142
|
@abstractmethod
|
|
108
|
-
async def delete(self, key: str) ->
|
|
109
|
-
"""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
|
+
"""
|
|
110
149
|
|
|
111
150
|
async def delete_many(self, keys: Iterable[str]) -> int:
|
|
112
151
|
"""Remove every key in ``keys``; returns how many were removed.
|
|
113
152
|
|
|
114
|
-
The base implementation deletes one key at a time and
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
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.
|
|
118
156
|
"""
|
|
119
157
|
count = 0
|
|
120
158
|
for key in keys:
|
|
121
|
-
await self.
|
|
122
|
-
|
|
159
|
+
if await self._delete_reporting(key):
|
|
160
|
+
count += 1
|
|
123
161
|
return count
|
|
124
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
|
+
|
|
125
187
|
async def get_and_delete(self, key: str) -> CacheEntry | None:
|
|
126
188
|
"""Atomically retrieve and remove a cached entry.
|
|
127
189
|
|
|
@@ -137,8 +199,10 @@ class BaseCacheBackend(ABC):
|
|
|
137
199
|
The entry that was stored under ``key``, or ``None`` if there was none
|
|
138
200
|
"""
|
|
139
201
|
value = await self.get(key)
|
|
140
|
-
if value is not
|
|
141
|
-
|
|
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
|
|
142
206
|
return value
|
|
143
207
|
|
|
144
208
|
async def set_if_absent(
|
|
@@ -191,8 +255,7 @@ class BaseCacheBackend(ABC):
|
|
|
191
255
|
"""
|
|
192
256
|
if await self.get(key) != expected:
|
|
193
257
|
return False
|
|
194
|
-
await self.
|
|
195
|
-
return True
|
|
258
|
+
return await self._delete_reporting(key)
|
|
196
259
|
|
|
197
260
|
async def expire_if_equals(self, key: str, expected: CacheEntry, ttl: int) -> bool:
|
|
198
261
|
"""Update expiry on ``key`` to ``ttl`` seconds only while it still holds ``expected``.
|
|
@@ -220,6 +283,39 @@ class BaseCacheBackend(ABC):
|
|
|
220
283
|
await self.set(key, expected, ttl=ttl)
|
|
221
284
|
return True
|
|
222
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
|
+
|
|
223
319
|
async def increment(self, key: str, delta: int = 1, ttl: int | None = None) -> int:
|
|
224
320
|
"""Atomically add ``delta`` to the integer counter stored at ``key``.
|
|
225
321
|
|
|
@@ -246,7 +342,8 @@ class BaseCacheBackend(ABC):
|
|
|
246
342
|
Raises:
|
|
247
343
|
CacheXError: If ``key`` holds a cached response instead of a counter
|
|
248
344
|
TypeError: If ``delta`` or ``ttl`` is not an ``int``
|
|
249
|
-
ValueError: If ``ttl`` is out of range
|
|
345
|
+
ValueError: If ``ttl`` is out of range, or ``delta`` does not fit
|
|
346
|
+
in a signed 64-bit integer
|
|
250
347
|
"""
|
|
251
348
|
validate_delta(delta)
|
|
252
349
|
validate_ttl(ttl)
|
|
@@ -277,10 +374,11 @@ class BaseCacheBackend(ABC):
|
|
|
277
374
|
|
|
278
375
|
The pattern is matched against the whole logical key — the key as the
|
|
279
376
|
caller sees it, without whatever prefix the backend adds internally.
|
|
280
|
-
HTTP cache keys are ``method
|
|
281
|
-
path means writing the other components
|
|
377
|
+
HTTP cache keys are ``http:v2|method|host|path|query`` (see
|
|
378
|
+
``CacheKey``), so matching a path means writing the other components
|
|
379
|
+
out::
|
|
282
380
|
|
|
283
|
-
await backend.clear_pattern("GET
|
|
381
|
+
await backend.clear_pattern("http:v2|GET|*|/users/*")
|
|
284
382
|
await backend.clear_pattern("cache:user:*") # a CacheManager key
|
|
285
383
|
|
|
286
384
|
To clear by path, prefer ``clear_path(path, include_params=...)``: it
|