fast-cache-middleware 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl
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.
- fast_cache_middleware/__init__.py +35 -34
- fast_cache_middleware/controller.py +231 -231
- fast_cache_middleware/depends.py +66 -66
- fast_cache_middleware/exceptions.py +6 -6
- fast_cache_middleware/middleware.py +306 -293
- fast_cache_middleware/schemas.py +21 -21
- fast_cache_middleware/serializers.py +92 -92
- fast_cache_middleware/storages.py +238 -238
- {fast_cache_middleware-0.0.1.dist-info → fast_cache_middleware-0.0.3.dist-info}/METADATA +3 -2
- fast_cache_middleware-0.0.3.dist-info/RECORD +11 -0
- fast_cache_middleware-0.0.1.dist-info/RECORD +0 -11
- {fast_cache_middleware-0.0.1.dist-info → fast_cache_middleware-0.0.3.dist-info}/WHEEL +0 -0
@@ -1,34 +1,35 @@
|
|
1
|
-
"""FastCacheMiddleware - high-performance ASGI middleware for caching.
|
2
|
-
|
3
|
-
Route resolution approach:
|
4
|
-
- Route analysis at application startup
|
5
|
-
- Cache configuration extraction from FastAPI dependencies
|
6
|
-
- Efficient caching and invalidation based on routes
|
7
|
-
|
8
|
-
|
9
|
-
TODO:
|
10
|
-
- add check for dependencies for middleware exists. and raise error if not.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
from .
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"
|
24
|
-
|
25
|
-
|
26
|
-
"
|
27
|
-
"
|
28
|
-
|
29
|
-
|
30
|
-
"
|
31
|
-
|
32
|
-
|
33
|
-
"
|
34
|
-
|
1
|
+
"""FastCacheMiddleware - high-performance ASGI middleware for caching.
|
2
|
+
|
3
|
+
Route resolution approach:
|
4
|
+
- Route analysis at application startup
|
5
|
+
- Cache configuration extraction from FastAPI dependencies
|
6
|
+
- Efficient caching and invalidation based on routes
|
7
|
+
|
8
|
+
|
9
|
+
TODO:
|
10
|
+
- add check for dependencies for middleware exists. and raise error if not.
|
11
|
+
- automatically add x-cache-age to the OpenAPI schema (openapi_extra) based on caching dependency.
|
12
|
+
"""
|
13
|
+
|
14
|
+
from .controller import Controller
|
15
|
+
from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig
|
16
|
+
from .middleware import FastCacheMiddleware
|
17
|
+
from .storages import BaseStorage, InMemoryStorage
|
18
|
+
|
19
|
+
__version__ = "1.0.0"
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
# Main components
|
23
|
+
"FastCacheMiddleware",
|
24
|
+
"Controller",
|
25
|
+
# Configuration via dependencies
|
26
|
+
"CacheConfig",
|
27
|
+
"CacheDropConfig",
|
28
|
+
"BaseCacheConfigDepends",
|
29
|
+
# Storages
|
30
|
+
"BaseStorage",
|
31
|
+
"InMemoryStorage",
|
32
|
+
# Serialization
|
33
|
+
"BaseSerializer",
|
34
|
+
"DefaultSerializer",
|
35
|
+
]
|
@@ -1,231 +1,231 @@
|
|
1
|
-
import http
|
2
|
-
import logging
|
3
|
-
import typing as tp
|
4
|
-
from hashlib import blake2b
|
5
|
-
|
6
|
-
from starlette.requests import Request
|
7
|
-
from starlette.responses import Response
|
8
|
-
|
9
|
-
from .depends import CacheConfig, CacheDropConfig
|
10
|
-
from .storages import BaseStorage
|
11
|
-
|
12
|
-
logger = logging.getLogger(__name__)
|
13
|
-
|
14
|
-
KNOWN_HTTP_METHODS = [method.value for method in http.HTTPMethod]
|
15
|
-
|
16
|
-
|
17
|
-
def generate_key(request: Request) -> str:
|
18
|
-
"""Generates fast unique key for caching HTTP request.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
request: Starlette Request object.
|
22
|
-
|
23
|
-
Returns:
|
24
|
-
str: Unique key for caching, based on request method and path.
|
25
|
-
Uses fast blake2b hashing algorithm.
|
26
|
-
|
27
|
-
Note:
|
28
|
-
Does not consider scheme and host, as requests usually go to the same host.
|
29
|
-
Only considers method, path and query parameters for maximum performance.
|
30
|
-
"""
|
31
|
-
# Get only necessary components from scope
|
32
|
-
scope = request.scope
|
33
|
-
url = scope["path"]
|
34
|
-
if scope["query_string"]:
|
35
|
-
url += f"?{scope['query_string'].decode('ascii')}"
|
36
|
-
|
37
|
-
# Use fast blake2b algorithm with minimal digest size
|
38
|
-
key = blake2b(digest_size=8)
|
39
|
-
key.update(request.method.encode())
|
40
|
-
key.update(url.encode())
|
41
|
-
|
42
|
-
return key.hexdigest()
|
43
|
-
|
44
|
-
|
45
|
-
class Controller:
|
46
|
-
"""Caching controller for Starlette/FastAPI.
|
47
|
-
|
48
|
-
Responsibilities:
|
49
|
-
1. Define rules for caching requests and responses
|
50
|
-
2. Generate cache keys with custom functions
|
51
|
-
3. Manage TTL and validation of cached data
|
52
|
-
4. Check HTTP caching headers
|
53
|
-
5. Invalidate cache by URL patterns
|
54
|
-
|
55
|
-
Supports:
|
56
|
-
- Custom key generation functions via CacheConfig
|
57
|
-
- Cache invalidation by URL patterns via CacheDropConfig
|
58
|
-
- Standard HTTP caching headers (Cache-Control, ETag, Last-Modified)
|
59
|
-
- Cache lifetime configuration via max_age in CacheConfig
|
60
|
-
"""
|
61
|
-
|
62
|
-
def __init__(
|
63
|
-
self,
|
64
|
-
cacheable_methods: list[str] | None = None,
|
65
|
-
cacheable_status_codes: list[int] | None = None,
|
66
|
-
) -> None:
|
67
|
-
self.cacheable_methods = []
|
68
|
-
if cacheable_methods:
|
69
|
-
for method in cacheable_methods:
|
70
|
-
method = method.upper()
|
71
|
-
if method in KNOWN_HTTP_METHODS:
|
72
|
-
self.cacheable_methods.append(method)
|
73
|
-
else:
|
74
|
-
raise ValueError(f"Invalid HTTP method: {method}")
|
75
|
-
else:
|
76
|
-
self.cacheable_methods.append(http.HTTPMethod.GET.value)
|
77
|
-
|
78
|
-
self.cacheable_status_codes = cacheable_status_codes or [
|
79
|
-
http.HTTPStatus.OK.value,
|
80
|
-
http.HTTPStatus.MOVED_PERMANENTLY.value,
|
81
|
-
http.HTTPStatus.PERMANENT_REDIRECT.value,
|
82
|
-
]
|
83
|
-
|
84
|
-
async def is_cachable_request(self, request: Request) -> bool:
|
85
|
-
"""Determines if this request should be cached.
|
86
|
-
|
87
|
-
Args:
|
88
|
-
request: HTTP request
|
89
|
-
cache_config: Cache configuration
|
90
|
-
|
91
|
-
Returns:
|
92
|
-
bool: True if request should be cached
|
93
|
-
"""
|
94
|
-
# Cache only GET requests by default
|
95
|
-
if request.method not in self.cacheable_methods:
|
96
|
-
return False
|
97
|
-
|
98
|
-
# Check Cache-Control headers
|
99
|
-
# todo: add parsing cache-control function
|
100
|
-
cache_control = request.headers.get("cache-control", "").lower()
|
101
|
-
if "no-cache" in cache_control or "no-store" in cache_control:
|
102
|
-
return False
|
103
|
-
|
104
|
-
return True
|
105
|
-
|
106
|
-
async def is_cachable_response(self, response: Response) -> bool:
|
107
|
-
"""Determines if this response can be cached.
|
108
|
-
|
109
|
-
Args:
|
110
|
-
request: HTTP request
|
111
|
-
response: HTTP response
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
bool: True if response can be cached
|
115
|
-
"""
|
116
|
-
if response.status_code not in self.cacheable_status_codes:
|
117
|
-
return False
|
118
|
-
|
119
|
-
# Check Cache-Control headers
|
120
|
-
cache_control = response.headers.get("cache-control", "").lower()
|
121
|
-
if (
|
122
|
-
"no-cache" in cache_control
|
123
|
-
or "no-store" in cache_control
|
124
|
-
or "private" in cache_control
|
125
|
-
):
|
126
|
-
return False
|
127
|
-
|
128
|
-
# Check response size (don't cache too large responses)
|
129
|
-
if (
|
130
|
-
hasattr(response, "body")
|
131
|
-
and response.body
|
132
|
-
and len(response.body) > 1024 * 1024
|
133
|
-
): # 1MB
|
134
|
-
return False
|
135
|
-
|
136
|
-
return True
|
137
|
-
|
138
|
-
async def generate_cache_key(
|
139
|
-
self, request: Request, cache_config: CacheConfig
|
140
|
-
) -> str:
|
141
|
-
"""Generates cache key for request.
|
142
|
-
|
143
|
-
Args:
|
144
|
-
request: HTTP request
|
145
|
-
cache_config: Cache configuration
|
146
|
-
|
147
|
-
Returns:
|
148
|
-
str: Cache key
|
149
|
-
"""
|
150
|
-
# Use custom key generation function if available
|
151
|
-
if cache_config.key_func:
|
152
|
-
return cache_config.key_func(request)
|
153
|
-
|
154
|
-
# Use standard function
|
155
|
-
return generate_key(request)
|
156
|
-
|
157
|
-
async def cache_response(
|
158
|
-
self,
|
159
|
-
cache_key: str,
|
160
|
-
request: Request,
|
161
|
-
response: Response,
|
162
|
-
storage: BaseStorage,
|
163
|
-
ttl: tp.Optional[int] = None,
|
164
|
-
) -> None:
|
165
|
-
"""Saves response to cache.
|
166
|
-
|
167
|
-
Args:
|
168
|
-
cache_key: Cache key
|
169
|
-
request: HTTP request
|
170
|
-
response: HTTP response to cache
|
171
|
-
storage: Cache storage
|
172
|
-
ttl: Cache lifetime in seconds
|
173
|
-
todo: in meta can write etag and last_modified from response headers
|
174
|
-
"""
|
175
|
-
if await self.is_cachable_response(response):
|
176
|
-
response.headers["X-Cache-Status"] = "HIT"
|
177
|
-
await storage.store(cache_key, response, request, {"ttl": ttl})
|
178
|
-
else:
|
179
|
-
logger.debug("Skip caching for response: %s", response.status_code)
|
180
|
-
|
181
|
-
async def get_cached_response(
|
182
|
-
self, cache_key: str, storage: BaseStorage
|
183
|
-
) -> tp.Optional[Response]:
|
184
|
-
"""Gets cached response if it exists and is valid.
|
185
|
-
|
186
|
-
Args:
|
187
|
-
cache_key: Cache key
|
188
|
-
storage: Cache storage
|
189
|
-
|
190
|
-
Returns:
|
191
|
-
Response or None if cache is invalid/missing
|
192
|
-
"""
|
193
|
-
result = await storage.retrieve(cache_key)
|
194
|
-
if result is None:
|
195
|
-
return None
|
196
|
-
response, _, _ = result
|
197
|
-
return response
|
198
|
-
|
199
|
-
async def invalidate_cache(
|
200
|
-
self,
|
201
|
-
cache_drop_config: CacheDropConfig,
|
202
|
-
storage: BaseStorage,
|
203
|
-
) -> None:
|
204
|
-
"""Invalidates cache by configuration.
|
205
|
-
|
206
|
-
Args:
|
207
|
-
cache_drop_config: Cache invalidation configuration
|
208
|
-
storage: Cache storage
|
209
|
-
|
210
|
-
TODO: Comments on improvements:
|
211
|
-
|
212
|
-
1. Need to add pattern support in storage for bulk invalidation
|
213
|
-
by key prefix/mask (especially for Redis/Memcached)
|
214
|
-
|
215
|
-
2. Desirable to add bulk operations for removing multiple keys
|
216
|
-
in one storage request
|
217
|
-
|
218
|
-
3. Can add delayed/asynchronous invalidation via queue
|
219
|
-
for large datasets
|
220
|
-
|
221
|
-
4. Should add invalidation strategies:
|
222
|
-
- Immediate (current implementation)
|
223
|
-
- Delayed (via TTL)
|
224
|
-
- Partial (only specific fields)
|
225
|
-
|
226
|
-
5. Add tag support for grouping related caches
|
227
|
-
and their joint invalidation
|
228
|
-
"""
|
229
|
-
for path in cache_drop_config.paths:
|
230
|
-
await storage.remove(path)
|
231
|
-
logger.info("Invalidated cache for pattern: %s", path.pattern)
|
1
|
+
import http
|
2
|
+
import logging
|
3
|
+
import typing as tp
|
4
|
+
from hashlib import blake2b
|
5
|
+
|
6
|
+
from starlette.requests import Request
|
7
|
+
from starlette.responses import Response
|
8
|
+
|
9
|
+
from .depends import CacheConfig, CacheDropConfig
|
10
|
+
from .storages import BaseStorage
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
KNOWN_HTTP_METHODS = [method.value for method in http.HTTPMethod]
|
15
|
+
|
16
|
+
|
17
|
+
def generate_key(request: Request) -> str:
|
18
|
+
"""Generates fast unique key for caching HTTP request.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
request: Starlette Request object.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
str: Unique key for caching, based on request method and path.
|
25
|
+
Uses fast blake2b hashing algorithm.
|
26
|
+
|
27
|
+
Note:
|
28
|
+
Does not consider scheme and host, as requests usually go to the same host.
|
29
|
+
Only considers method, path and query parameters for maximum performance.
|
30
|
+
"""
|
31
|
+
# Get only necessary components from scope
|
32
|
+
scope = request.scope
|
33
|
+
url = scope["path"]
|
34
|
+
if scope["query_string"]:
|
35
|
+
url += f"?{scope['query_string'].decode('ascii')}"
|
36
|
+
|
37
|
+
# Use fast blake2b algorithm with minimal digest size
|
38
|
+
key = blake2b(digest_size=8)
|
39
|
+
key.update(request.method.encode())
|
40
|
+
key.update(url.encode())
|
41
|
+
|
42
|
+
return key.hexdigest()
|
43
|
+
|
44
|
+
|
45
|
+
class Controller:
|
46
|
+
"""Caching controller for Starlette/FastAPI.
|
47
|
+
|
48
|
+
Responsibilities:
|
49
|
+
1. Define rules for caching requests and responses
|
50
|
+
2. Generate cache keys with custom functions
|
51
|
+
3. Manage TTL and validation of cached data
|
52
|
+
4. Check HTTP caching headers
|
53
|
+
5. Invalidate cache by URL patterns
|
54
|
+
|
55
|
+
Supports:
|
56
|
+
- Custom key generation functions via CacheConfig
|
57
|
+
- Cache invalidation by URL patterns via CacheDropConfig
|
58
|
+
- Standard HTTP caching headers (Cache-Control, ETag, Last-Modified)
|
59
|
+
- Cache lifetime configuration via max_age in CacheConfig
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
cacheable_methods: list[str] | None = None,
|
65
|
+
cacheable_status_codes: list[int] | None = None,
|
66
|
+
) -> None:
|
67
|
+
self.cacheable_methods = []
|
68
|
+
if cacheable_methods:
|
69
|
+
for method in cacheable_methods:
|
70
|
+
method = method.upper()
|
71
|
+
if method in KNOWN_HTTP_METHODS:
|
72
|
+
self.cacheable_methods.append(method)
|
73
|
+
else:
|
74
|
+
raise ValueError(f"Invalid HTTP method: {method}")
|
75
|
+
else:
|
76
|
+
self.cacheable_methods.append(http.HTTPMethod.GET.value)
|
77
|
+
|
78
|
+
self.cacheable_status_codes = cacheable_status_codes or [
|
79
|
+
http.HTTPStatus.OK.value,
|
80
|
+
http.HTTPStatus.MOVED_PERMANENTLY.value,
|
81
|
+
http.HTTPStatus.PERMANENT_REDIRECT.value,
|
82
|
+
]
|
83
|
+
|
84
|
+
async def is_cachable_request(self, request: Request) -> bool:
|
85
|
+
"""Determines if this request should be cached.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
request: HTTP request
|
89
|
+
cache_config: Cache configuration
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True if request should be cached
|
93
|
+
"""
|
94
|
+
# Cache only GET requests by default
|
95
|
+
if request.method not in self.cacheable_methods:
|
96
|
+
return False
|
97
|
+
|
98
|
+
# Check Cache-Control headers
|
99
|
+
# todo: add parsing cache-control function
|
100
|
+
cache_control = request.headers.get("cache-control", "").lower()
|
101
|
+
if "no-cache" in cache_control or "no-store" in cache_control:
|
102
|
+
return False
|
103
|
+
|
104
|
+
return True
|
105
|
+
|
106
|
+
async def is_cachable_response(self, response: Response) -> bool:
|
107
|
+
"""Determines if this response can be cached.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
request: HTTP request
|
111
|
+
response: HTTP response
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
bool: True if response can be cached
|
115
|
+
"""
|
116
|
+
if response.status_code not in self.cacheable_status_codes:
|
117
|
+
return False
|
118
|
+
|
119
|
+
# Check Cache-Control headers
|
120
|
+
cache_control = response.headers.get("cache-control", "").lower()
|
121
|
+
if (
|
122
|
+
"no-cache" in cache_control
|
123
|
+
or "no-store" in cache_control
|
124
|
+
or "private" in cache_control
|
125
|
+
):
|
126
|
+
return False
|
127
|
+
|
128
|
+
# Check response size (don't cache too large responses)
|
129
|
+
if (
|
130
|
+
hasattr(response, "body")
|
131
|
+
and response.body
|
132
|
+
and len(response.body) > 1024 * 1024
|
133
|
+
): # 1MB
|
134
|
+
return False
|
135
|
+
|
136
|
+
return True
|
137
|
+
|
138
|
+
async def generate_cache_key(
|
139
|
+
self, request: Request, cache_config: CacheConfig
|
140
|
+
) -> str:
|
141
|
+
"""Generates cache key for request.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
request: HTTP request
|
145
|
+
cache_config: Cache configuration
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
str: Cache key
|
149
|
+
"""
|
150
|
+
# Use custom key generation function if available
|
151
|
+
if cache_config.key_func:
|
152
|
+
return cache_config.key_func(request)
|
153
|
+
|
154
|
+
# Use standard function
|
155
|
+
return generate_key(request)
|
156
|
+
|
157
|
+
async def cache_response(
|
158
|
+
self,
|
159
|
+
cache_key: str,
|
160
|
+
request: Request,
|
161
|
+
response: Response,
|
162
|
+
storage: BaseStorage,
|
163
|
+
ttl: tp.Optional[int] = None,
|
164
|
+
) -> None:
|
165
|
+
"""Saves response to cache.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
cache_key: Cache key
|
169
|
+
request: HTTP request
|
170
|
+
response: HTTP response to cache
|
171
|
+
storage: Cache storage
|
172
|
+
ttl: Cache lifetime in seconds
|
173
|
+
todo: in meta can write etag and last_modified from response headers
|
174
|
+
"""
|
175
|
+
if await self.is_cachable_response(response):
|
176
|
+
response.headers["X-Cache-Status"] = "HIT"
|
177
|
+
await storage.store(cache_key, response, request, {"ttl": ttl})
|
178
|
+
else:
|
179
|
+
logger.debug("Skip caching for response: %s", response.status_code)
|
180
|
+
|
181
|
+
async def get_cached_response(
|
182
|
+
self, cache_key: str, storage: BaseStorage
|
183
|
+
) -> tp.Optional[Response]:
|
184
|
+
"""Gets cached response if it exists and is valid.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
cache_key: Cache key
|
188
|
+
storage: Cache storage
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
Response or None if cache is invalid/missing
|
192
|
+
"""
|
193
|
+
result = await storage.retrieve(cache_key)
|
194
|
+
if result is None:
|
195
|
+
return None
|
196
|
+
response, _, _ = result
|
197
|
+
return response
|
198
|
+
|
199
|
+
async def invalidate_cache(
|
200
|
+
self,
|
201
|
+
cache_drop_config: CacheDropConfig,
|
202
|
+
storage: BaseStorage,
|
203
|
+
) -> None:
|
204
|
+
"""Invalidates cache by configuration.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
cache_drop_config: Cache invalidation configuration
|
208
|
+
storage: Cache storage
|
209
|
+
|
210
|
+
TODO: Comments on improvements:
|
211
|
+
|
212
|
+
1. Need to add pattern support in storage for bulk invalidation
|
213
|
+
by key prefix/mask (especially for Redis/Memcached)
|
214
|
+
|
215
|
+
2. Desirable to add bulk operations for removing multiple keys
|
216
|
+
in one storage request
|
217
|
+
|
218
|
+
3. Can add delayed/asynchronous invalidation via queue
|
219
|
+
for large datasets
|
220
|
+
|
221
|
+
4. Should add invalidation strategies:
|
222
|
+
- Immediate (current implementation)
|
223
|
+
- Delayed (via TTL)
|
224
|
+
- Partial (only specific fields)
|
225
|
+
|
226
|
+
5. Add tag support for grouping related caches
|
227
|
+
and their joint invalidation
|
228
|
+
"""
|
229
|
+
for path in cache_drop_config.paths:
|
230
|
+
await storage.remove(path)
|
231
|
+
logger.info("Invalidated cache for pattern: %s", path.pattern)
|
fast_cache_middleware/depends.py
CHANGED
@@ -1,66 +1,66 @@
|
|
1
|
-
import re
|
2
|
-
import typing as tp
|
3
|
-
|
4
|
-
from fastapi import params
|
5
|
-
from starlette.requests import Request
|
6
|
-
|
7
|
-
|
8
|
-
class BaseCacheConfigDepends(params.Depends):
|
9
|
-
"""Base class for cache configuration via ASGI scope extensions.
|
10
|
-
|
11
|
-
Uses standardized ASGI extensions mechanism for passing
|
12
|
-
configuration from route dependencies to middleware.
|
13
|
-
"""
|
14
|
-
|
15
|
-
use_cache: bool = True
|
16
|
-
|
17
|
-
def __call__(self, request: Request) -> None:
|
18
|
-
"""Saves configuration in ASGI scope extensions.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
request: HTTP request
|
22
|
-
"""
|
23
|
-
# Use standard ASGI extensions mechanism
|
24
|
-
if "extensions" not in request.scope:
|
25
|
-
request.scope["extensions"] = {}
|
26
|
-
|
27
|
-
if "fast_cache" not in request.scope["extensions"]:
|
28
|
-
request.scope["extensions"]["fast_cache"] = {}
|
29
|
-
|
30
|
-
request.scope["extensions"]["fast_cache"]["config"] = self
|
31
|
-
|
32
|
-
@property
|
33
|
-
def dependency(self) -> params.Depends:
|
34
|
-
return self
|
35
|
-
|
36
|
-
|
37
|
-
class CacheConfig(BaseCacheConfigDepends):
|
38
|
-
"""Cache configuration for route.
|
39
|
-
|
40
|
-
Args:
|
41
|
-
max_age: Cache lifetime in seconds
|
42
|
-
key_func: Cache key generation function
|
43
|
-
"""
|
44
|
-
|
45
|
-
def __init__(
|
46
|
-
self,
|
47
|
-
max_age: int = 5 * 60,
|
48
|
-
key_func: tp.Optional[tp.Callable[[Request], str]] = None,
|
49
|
-
) -> None:
|
50
|
-
self.max_age = max_age
|
51
|
-
self.key_func = key_func
|
52
|
-
|
53
|
-
|
54
|
-
class CacheDropConfig(BaseCacheConfigDepends):
|
55
|
-
"""Cache invalidation configuration for route.
|
56
|
-
|
57
|
-
Args:
|
58
|
-
paths: Path for cache invalidation. Can be string or regular expression.
|
59
|
-
If string, it will be converted to regular expression
|
60
|
-
that matches the beginning of request path.
|
61
|
-
"""
|
62
|
-
|
63
|
-
def __init__(self, paths: list[str | re.Pattern]) -> None:
|
64
|
-
self.paths: list[re.Pattern] = [
|
65
|
-
p if isinstance(p, re.Pattern) else re.compile(f"^{p}
|
66
|
-
]
|
1
|
+
import re
|
2
|
+
import typing as tp
|
3
|
+
|
4
|
+
from fastapi import params
|
5
|
+
from starlette.requests import Request
|
6
|
+
|
7
|
+
|
8
|
+
class BaseCacheConfigDepends(params.Depends):
|
9
|
+
"""Base class for cache configuration via ASGI scope extensions.
|
10
|
+
|
11
|
+
Uses standardized ASGI extensions mechanism for passing
|
12
|
+
configuration from route dependencies to middleware.
|
13
|
+
"""
|
14
|
+
|
15
|
+
use_cache: bool = True
|
16
|
+
|
17
|
+
def __call__(self, request: Request) -> None:
|
18
|
+
"""Saves configuration in ASGI scope extensions.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
request: HTTP request
|
22
|
+
"""
|
23
|
+
# Use standard ASGI extensions mechanism
|
24
|
+
if "extensions" not in request.scope:
|
25
|
+
request.scope["extensions"] = {}
|
26
|
+
|
27
|
+
if "fast_cache" not in request.scope["extensions"]:
|
28
|
+
request.scope["extensions"]["fast_cache"] = {}
|
29
|
+
|
30
|
+
request.scope["extensions"]["fast_cache"]["config"] = self
|
31
|
+
|
32
|
+
@property
|
33
|
+
def dependency(self) -> params.Depends:
|
34
|
+
return self
|
35
|
+
|
36
|
+
|
37
|
+
class CacheConfig(BaseCacheConfigDepends):
|
38
|
+
"""Cache configuration for route.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
max_age: Cache lifetime in seconds
|
42
|
+
key_func: Cache key generation function
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
max_age: int = 5 * 60,
|
48
|
+
key_func: tp.Optional[tp.Callable[[Request], str]] = None,
|
49
|
+
) -> None:
|
50
|
+
self.max_age = max_age
|
51
|
+
self.key_func = key_func
|
52
|
+
|
53
|
+
|
54
|
+
class CacheDropConfig(BaseCacheConfigDepends):
|
55
|
+
"""Cache invalidation configuration for route.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
paths: Path for cache invalidation. Can be string or regular expression.
|
59
|
+
If string, it will be converted to regular expression
|
60
|
+
that matches the beginning of request path.
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self, paths: list[str | re.Pattern]) -> None:
|
64
|
+
self.paths: list[re.Pattern] = [
|
65
|
+
p if isinstance(p, re.Pattern) else re.compile(f"^{p}") for p in paths
|
66
|
+
]
|