fast-cache-middleware 0.0.4__tar.gz → 0.0.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.
- fast_cache_middleware-0.0.5/LICENSE +21 -0
- fast_cache_middleware-0.0.5/PKG-INFO +344 -0
- fast_cache_middleware-0.0.5/README.md +328 -0
- fast_cache_middleware-0.0.5/fast_cache_middleware/_helpers.py +28 -0
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/controller.py +11 -10
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/depends.py +7 -19
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/middleware.py +203 -128
- fast_cache_middleware-0.0.5/fast_cache_middleware/schemas.py +78 -0
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/serializers.py +8 -27
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/storages.py +9 -9
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/pyproject.toml +4 -1
- fast_cache_middleware-0.0.4/PKG-INFO +0 -369
- fast_cache_middleware-0.0.4/README.md +0 -354
- fast_cache_middleware-0.0.4/fast_cache_middleware/schemas.py +0 -21
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/__init__.py +0 -0
- {fast_cache_middleware-0.0.4 → fast_cache_middleware-0.0.5}/fast_cache_middleware/exceptions.py +0 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Vadim Vakilov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,344 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: fast-cache-middleware
|
3
|
+
Version: 0.0.5
|
4
|
+
Summary: An intelligent middleware for caching FastAPI responses
|
5
|
+
License: MIT
|
6
|
+
Author: www.chud0@gmail.com
|
7
|
+
Requires-Python: >=3.11,<4.0
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Requires-Dist: fastapi (>=0.111.1,<1.0.0)
|
14
|
+
Requires-Dist: pydantic (>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0)
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# FastCacheMiddleware
|
18
|
+
|
19
|
+
🚀 **High-performance ASGI middleware for caching with route resolution approach**
|
20
|
+
|
21
|
+
## ✨ Key Features
|
22
|
+
|
23
|
+
FastCacheMiddleware uses a **route resolution approach** - it analyzes application routes at startup and extracts cache configurations from FastAPI dependencies.
|
24
|
+
|
25
|
+
### 🔧 How it works
|
26
|
+
|
27
|
+
1. **At application startup:**
|
28
|
+
- Middleware analyzes all routes and their dependencies
|
29
|
+
- Extracts `CacheConfig` and `CacheDropConfig` from dependencies
|
30
|
+
- Creates internal route index with caching configurations
|
31
|
+
|
32
|
+
2. **During request processing:**
|
33
|
+
- Checks HTTP method (cache only GET, invalidate for POST/PUT/DELETE)
|
34
|
+
- Finds matching route by path and method
|
35
|
+
- Extracts cache configuration from pre-analyzed dependencies
|
36
|
+
- Performs caching or invalidation according to configuration
|
37
|
+
|
38
|
+
### 💡 Benefits
|
39
|
+
|
40
|
+
- **⚡ High performance** - pre-route analysis
|
41
|
+
- **🎯 Easy integration** - standard FastAPI dependencies
|
42
|
+
- **🔧 Flexible configuration** - custom key functions, route-level TTL
|
43
|
+
- **🛡️ Automatic invalidation** - cache invalidation for modifying requests
|
44
|
+
- **📊 Minimal overhead** - efficient handling of large numbers of routes
|
45
|
+
|
46
|
+
## 📦 Installation
|
47
|
+
|
48
|
+
```bash
|
49
|
+
pip install fast-cache-middleware
|
50
|
+
```
|
51
|
+
|
52
|
+
## 🎯 Quick Start
|
53
|
+
|
54
|
+
```python
|
55
|
+
import uvicorn
|
56
|
+
from fastapi import FastAPI
|
57
|
+
|
58
|
+
from fast_cache_middleware import CacheConfig, CacheDropConfig, FastCacheMiddleware
|
59
|
+
|
60
|
+
app = FastAPI()
|
61
|
+
|
62
|
+
# Add middleware - it will automatically analyze routes
|
63
|
+
app.add_middleware(FastCacheMiddleware)
|
64
|
+
|
65
|
+
|
66
|
+
# Routes with caching
|
67
|
+
@app.get("/users/{user_id}", dependencies=[CacheConfig(max_age=300)])
|
68
|
+
async def get_user(user_id: int) -> dict[str, int | str]:
|
69
|
+
"""This endpoint is cached for 5 minutes."""
|
70
|
+
# Simulate database load
|
71
|
+
return {"user_id": user_id, "name": f"User {user_id}"}
|
72
|
+
|
73
|
+
|
74
|
+
# Routes with cache invalidation
|
75
|
+
@app.post(
|
76
|
+
"/users/{user_id}",
|
77
|
+
dependencies=[CacheDropConfig(paths=["/users/*", "/api/users/*"])],
|
78
|
+
)
|
79
|
+
async def update_user(user_id: int) -> dict[str, int | str]:
|
80
|
+
"""It will invalidate cache for all /users/* paths."""
|
81
|
+
return {"user_id": user_id, "status": "updated"}
|
82
|
+
|
83
|
+
|
84
|
+
if __name__ == "__main__":
|
85
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
86
|
+
```
|
87
|
+
|
88
|
+
## 🔧 Configuration
|
89
|
+
|
90
|
+
### CacheConfig
|
91
|
+
|
92
|
+
Configure caching for GET requests:
|
93
|
+
|
94
|
+
```python
|
95
|
+
from fast_cache_middleware import CacheConfig
|
96
|
+
|
97
|
+
# Simple caching
|
98
|
+
CacheConfig(max_age=300) # 5 minutes
|
99
|
+
|
100
|
+
# With custom key function, for personalized cache
|
101
|
+
def key_func(request: Request):
|
102
|
+
user_id = request.headers.get("Authorization", "anonymous")
|
103
|
+
path = request.url.path
|
104
|
+
query = str(request.query_params)
|
105
|
+
return f"{path}:{user_id}:{query}"
|
106
|
+
|
107
|
+
CacheConfig(max_age=600, key_func=key_func) # 10 minutes
|
108
|
+
```
|
109
|
+
|
110
|
+
### CacheDropConfig
|
111
|
+
|
112
|
+
Configure cache invalidation for modifying requests:
|
113
|
+
|
114
|
+
```python
|
115
|
+
# Paths can be matched by startswith
|
116
|
+
CacheDropConfig(
|
117
|
+
paths=[
|
118
|
+
"/users/", # Will match /users/123, /users/profile, etc.
|
119
|
+
"/api/", # Will match all API paths
|
120
|
+
]
|
121
|
+
)
|
122
|
+
|
123
|
+
# Paths can be matched by regexp
|
124
|
+
CacheDropConfig(
|
125
|
+
paths=[
|
126
|
+
r"^/users/\d+$", # Will match /users/123, /users/456, etc.
|
127
|
+
r"^/api/.*", # Will match all API paths
|
128
|
+
]
|
129
|
+
)
|
130
|
+
|
131
|
+
# You can mix regexp and simple string matching - use what's more convenient
|
132
|
+
CacheDropConfig(
|
133
|
+
paths=[
|
134
|
+
"/users/", # Simple prefix match
|
135
|
+
r"^/api/\w+/\d+$" # Regexp for specific API endpoints
|
136
|
+
]
|
137
|
+
)
|
138
|
+
```
|
139
|
+
|
140
|
+
## 🏗️ Architecture
|
141
|
+
|
142
|
+
### System Components
|
143
|
+
|
144
|
+
```
|
145
|
+
FastCacheMiddleware
|
146
|
+
├── RouteInfo # Route information with cache configuration
|
147
|
+
├── Controller # Caching logic and validation
|
148
|
+
├── Storage # Storages (InMemory, Redis, etc.)
|
149
|
+
├── Serializers # Cached data serialization
|
150
|
+
└── Dependencies # FastAPI dependencies for configuration
|
151
|
+
```
|
152
|
+
|
153
|
+
### Request Processing Flow
|
154
|
+
|
155
|
+
```mermaid
|
156
|
+
graph TD
|
157
|
+
A[HTTP Request] --> B{Route analysis done?}
|
158
|
+
B -->|No| C[Analyze application routes]
|
159
|
+
C --> D[Save route configurations]
|
160
|
+
B -->|Yes| E{Method supports caching?}
|
161
|
+
D --> E
|
162
|
+
E -->|No| F[Pass to application]
|
163
|
+
E -->|Yes| G[Find matching route]
|
164
|
+
G --> H{Route found?}
|
165
|
+
H -->|No| F
|
166
|
+
H -->|Yes| I{GET request + CacheConfig?}
|
167
|
+
I -->|Yes| J[Check cache]
|
168
|
+
J --> K{Cache found?}
|
169
|
+
K -->|Yes| L[Return from cache]
|
170
|
+
K -->|No| M[Execute request + save to cache]
|
171
|
+
I -->|No| N{POST/PUT/DELETE + CacheDropConfig?}
|
172
|
+
N -->|Yes| O[Invalidate cache]
|
173
|
+
N -->|No| F
|
174
|
+
O --> F
|
175
|
+
M --> P[Return response]
|
176
|
+
```
|
177
|
+
|
178
|
+
## 🎛️ Storages
|
179
|
+
|
180
|
+
### InMemoryStorage (default)
|
181
|
+
|
182
|
+
```python
|
183
|
+
from fast_cache_middleware import FastCacheMiddleware, InMemoryStorage
|
184
|
+
|
185
|
+
storage = InMemoryStorage(max_size=1000)
|
186
|
+
app.add_middleware(FastCacheMiddleware, storage=storage)
|
187
|
+
```
|
188
|
+
|
189
|
+
InMemoryStorage uses batch cleanup for better performance
|
190
|
+
|
191
|
+
### Custom Storage
|
192
|
+
|
193
|
+
```python
|
194
|
+
from fast_cache_middleware import BaseStorage
|
195
|
+
|
196
|
+
class RedisStorage(BaseStorage):
|
197
|
+
def __init__(self, redis_url: str):
|
198
|
+
import redis
|
199
|
+
self.redis = redis.from_url(redis_url)
|
200
|
+
|
201
|
+
async def store(self, key: str, response, request, metadata):
|
202
|
+
# Implementation for saving to Redis
|
203
|
+
pass
|
204
|
+
|
205
|
+
async def retrieve(self, key: str):
|
206
|
+
# Implementation for retrieving from Redis
|
207
|
+
pass
|
208
|
+
|
209
|
+
app.add_middleware(FastCacheMiddleware, storage=RedisStorage("redis://localhost"))
|
210
|
+
```
|
211
|
+
|
212
|
+
## 🧪 Testing
|
213
|
+
|
214
|
+
```python
|
215
|
+
import pytest
|
216
|
+
from httpx import AsyncClient
|
217
|
+
from examples.basic import app
|
218
|
+
|
219
|
+
@pytest.mark.asyncio
|
220
|
+
async def test_caching():
|
221
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
222
|
+
# First request - cache miss
|
223
|
+
response1 = await client.get("/users/1")
|
224
|
+
assert response1.status_code == 200
|
225
|
+
|
226
|
+
# Second request - cache hit (should be faster)
|
227
|
+
response2 = await client.get("/users/1")
|
228
|
+
assert response2.status_code == 200
|
229
|
+
assert response1.json() == response2.json()
|
230
|
+
|
231
|
+
@pytest.mark.asyncio
|
232
|
+
async def test_cache_invalidation():
|
233
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
234
|
+
# Cache data
|
235
|
+
await client.get("/users/1")
|
236
|
+
|
237
|
+
# Invalidate cache
|
238
|
+
await client.post("/users/1", json={})
|
239
|
+
|
240
|
+
# Next GET should execute new request
|
241
|
+
response = await client.get("/users/1")
|
242
|
+
assert response.status_code == 200
|
243
|
+
```
|
244
|
+
|
245
|
+
## 📊 Performance
|
246
|
+
|
247
|
+
### Benchmarks
|
248
|
+
|
249
|
+
- **Route analysis**: ~5ms for 100 routes at startup
|
250
|
+
- **Route lookup**: ~0.1ms per request (O(n) by number of cached routes)
|
251
|
+
- **Cache hit**: ~1ms per request
|
252
|
+
- **Cache miss**: original request time + ~2ms for saving
|
253
|
+
|
254
|
+
### Optimization
|
255
|
+
|
256
|
+
```python
|
257
|
+
# For applications with many routes
|
258
|
+
app.add_middleware(
|
259
|
+
FastCacheMiddleware,
|
260
|
+
storage=InMemoryStorage(max_size=10000), # Increase cache size
|
261
|
+
controller=Controller(default_ttl=3600) # Increase default TTL
|
262
|
+
)
|
263
|
+
```
|
264
|
+
|
265
|
+
## 🔒 Security
|
266
|
+
|
267
|
+
### Cache Isolation
|
268
|
+
|
269
|
+
```python
|
270
|
+
def user_specific_cache() -> CacheConfig:
|
271
|
+
def secure_key_func(request):
|
272
|
+
# Include user token in key
|
273
|
+
token = request.headers.get("authorization", "").split(" ")[-1]
|
274
|
+
return f"{request.url.path}:token:{token}"
|
275
|
+
|
276
|
+
return CacheConfig(max_age=300, key_func=secure_key_func)
|
277
|
+
|
278
|
+
@app.get("/private/data", dependencies=[Depends(user_specific_cache)])
|
279
|
+
async def get_private_data():
|
280
|
+
return {"sensitive": "data"}
|
281
|
+
```
|
282
|
+
|
283
|
+
### Header Validation
|
284
|
+
|
285
|
+
Middleware automatically respects standard HTTP caching headers:
|
286
|
+
|
287
|
+
- `Cache-Control: no-cache` - skip cache
|
288
|
+
- `Cache-Control: no-store` - forbid caching
|
289
|
+
- `Cache-Control: private` - don't cache private responses
|
290
|
+
|
291
|
+
|
292
|
+
## 🛠️ Advanced Usage
|
293
|
+
|
294
|
+
### Custom Controller
|
295
|
+
|
296
|
+
```python
|
297
|
+
from fast_cache_middleware import Controller
|
298
|
+
|
299
|
+
class CustomController(Controller):
|
300
|
+
async def is_cachable_request(self, request):
|
301
|
+
# Custom logic - don't cache admin requests
|
302
|
+
if request.headers.get("x-admin-request"):
|
303
|
+
return False
|
304
|
+
return await super().should_cache_request(request)
|
305
|
+
|
306
|
+
async def generate_cache_key(self, request):
|
307
|
+
# Add API version to key
|
308
|
+
version = request.headers.get("api-version", "v1")
|
309
|
+
base_key = await super().generate_cache_key(request)
|
310
|
+
return f"{version}:{base_key}"
|
311
|
+
|
312
|
+
app.add_middleware(
|
313
|
+
FastCacheMiddleware,
|
314
|
+
controller=CustomController()
|
315
|
+
)
|
316
|
+
```
|
317
|
+
|
318
|
+
## 📝 Examples
|
319
|
+
|
320
|
+
More examples in the `examples/` folder:
|
321
|
+
|
322
|
+
- **quick_start.py** - minimal example showing basic caching and invalidation
|
323
|
+
- **basic.py** - basic usage with FastAPI
|
324
|
+
|
325
|
+
## 🤝 Contributing
|
326
|
+
|
327
|
+
```bash
|
328
|
+
git clone https://github.com/chud0/FastCacheMiddleware
|
329
|
+
cd FastCacheMiddleware
|
330
|
+
poetry install --with dev
|
331
|
+
./scripts/test.sh
|
332
|
+
```
|
333
|
+
|
334
|
+
## 📄 License
|
335
|
+
|
336
|
+
MIT License - see [LICENSE](LICENSE)
|
337
|
+
|
338
|
+
---
|
339
|
+
|
340
|
+
⭐ **Like the project? Give it a star!**
|
341
|
+
|
342
|
+
🐛 **Found a bug?** [Create an issue](https://github.com/chud0/FastCacheMiddleware/issues)
|
343
|
+
|
344
|
+
💡 **Have an idea?** [Suggest a feature](https://github.com/chud0/FastCacheMiddleware/discussions/categories/ideas)
|
@@ -0,0 +1,328 @@
|
|
1
|
+
# FastCacheMiddleware
|
2
|
+
|
3
|
+
🚀 **High-performance ASGI middleware for caching with route resolution approach**
|
4
|
+
|
5
|
+
## ✨ Key Features
|
6
|
+
|
7
|
+
FastCacheMiddleware uses a **route resolution approach** - it analyzes application routes at startup and extracts cache configurations from FastAPI dependencies.
|
8
|
+
|
9
|
+
### 🔧 How it works
|
10
|
+
|
11
|
+
1. **At application startup:**
|
12
|
+
- Middleware analyzes all routes and their dependencies
|
13
|
+
- Extracts `CacheConfig` and `CacheDropConfig` from dependencies
|
14
|
+
- Creates internal route index with caching configurations
|
15
|
+
|
16
|
+
2. **During request processing:**
|
17
|
+
- Checks HTTP method (cache only GET, invalidate for POST/PUT/DELETE)
|
18
|
+
- Finds matching route by path and method
|
19
|
+
- Extracts cache configuration from pre-analyzed dependencies
|
20
|
+
- Performs caching or invalidation according to configuration
|
21
|
+
|
22
|
+
### 💡 Benefits
|
23
|
+
|
24
|
+
- **⚡ High performance** - pre-route analysis
|
25
|
+
- **🎯 Easy integration** - standard FastAPI dependencies
|
26
|
+
- **🔧 Flexible configuration** - custom key functions, route-level TTL
|
27
|
+
- **🛡️ Automatic invalidation** - cache invalidation for modifying requests
|
28
|
+
- **📊 Minimal overhead** - efficient handling of large numbers of routes
|
29
|
+
|
30
|
+
## 📦 Installation
|
31
|
+
|
32
|
+
```bash
|
33
|
+
pip install fast-cache-middleware
|
34
|
+
```
|
35
|
+
|
36
|
+
## 🎯 Quick Start
|
37
|
+
|
38
|
+
```python
|
39
|
+
import uvicorn
|
40
|
+
from fastapi import FastAPI
|
41
|
+
|
42
|
+
from fast_cache_middleware import CacheConfig, CacheDropConfig, FastCacheMiddleware
|
43
|
+
|
44
|
+
app = FastAPI()
|
45
|
+
|
46
|
+
# Add middleware - it will automatically analyze routes
|
47
|
+
app.add_middleware(FastCacheMiddleware)
|
48
|
+
|
49
|
+
|
50
|
+
# Routes with caching
|
51
|
+
@app.get("/users/{user_id}", dependencies=[CacheConfig(max_age=300)])
|
52
|
+
async def get_user(user_id: int) -> dict[str, int | str]:
|
53
|
+
"""This endpoint is cached for 5 minutes."""
|
54
|
+
# Simulate database load
|
55
|
+
return {"user_id": user_id, "name": f"User {user_id}"}
|
56
|
+
|
57
|
+
|
58
|
+
# Routes with cache invalidation
|
59
|
+
@app.post(
|
60
|
+
"/users/{user_id}",
|
61
|
+
dependencies=[CacheDropConfig(paths=["/users/*", "/api/users/*"])],
|
62
|
+
)
|
63
|
+
async def update_user(user_id: int) -> dict[str, int | str]:
|
64
|
+
"""It will invalidate cache for all /users/* paths."""
|
65
|
+
return {"user_id": user_id, "status": "updated"}
|
66
|
+
|
67
|
+
|
68
|
+
if __name__ == "__main__":
|
69
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
70
|
+
```
|
71
|
+
|
72
|
+
## 🔧 Configuration
|
73
|
+
|
74
|
+
### CacheConfig
|
75
|
+
|
76
|
+
Configure caching for GET requests:
|
77
|
+
|
78
|
+
```python
|
79
|
+
from fast_cache_middleware import CacheConfig
|
80
|
+
|
81
|
+
# Simple caching
|
82
|
+
CacheConfig(max_age=300) # 5 minutes
|
83
|
+
|
84
|
+
# With custom key function, for personalized cache
|
85
|
+
def key_func(request: Request):
|
86
|
+
user_id = request.headers.get("Authorization", "anonymous")
|
87
|
+
path = request.url.path
|
88
|
+
query = str(request.query_params)
|
89
|
+
return f"{path}:{user_id}:{query}"
|
90
|
+
|
91
|
+
CacheConfig(max_age=600, key_func=key_func) # 10 minutes
|
92
|
+
```
|
93
|
+
|
94
|
+
### CacheDropConfig
|
95
|
+
|
96
|
+
Configure cache invalidation for modifying requests:
|
97
|
+
|
98
|
+
```python
|
99
|
+
# Paths can be matched by startswith
|
100
|
+
CacheDropConfig(
|
101
|
+
paths=[
|
102
|
+
"/users/", # Will match /users/123, /users/profile, etc.
|
103
|
+
"/api/", # Will match all API paths
|
104
|
+
]
|
105
|
+
)
|
106
|
+
|
107
|
+
# Paths can be matched by regexp
|
108
|
+
CacheDropConfig(
|
109
|
+
paths=[
|
110
|
+
r"^/users/\d+$", # Will match /users/123, /users/456, etc.
|
111
|
+
r"^/api/.*", # Will match all API paths
|
112
|
+
]
|
113
|
+
)
|
114
|
+
|
115
|
+
# You can mix regexp and simple string matching - use what's more convenient
|
116
|
+
CacheDropConfig(
|
117
|
+
paths=[
|
118
|
+
"/users/", # Simple prefix match
|
119
|
+
r"^/api/\w+/\d+$" # Regexp for specific API endpoints
|
120
|
+
]
|
121
|
+
)
|
122
|
+
```
|
123
|
+
|
124
|
+
## 🏗️ Architecture
|
125
|
+
|
126
|
+
### System Components
|
127
|
+
|
128
|
+
```
|
129
|
+
FastCacheMiddleware
|
130
|
+
├── RouteInfo # Route information with cache configuration
|
131
|
+
├── Controller # Caching logic and validation
|
132
|
+
├── Storage # Storages (InMemory, Redis, etc.)
|
133
|
+
├── Serializers # Cached data serialization
|
134
|
+
└── Dependencies # FastAPI dependencies for configuration
|
135
|
+
```
|
136
|
+
|
137
|
+
### Request Processing Flow
|
138
|
+
|
139
|
+
```mermaid
|
140
|
+
graph TD
|
141
|
+
A[HTTP Request] --> B{Route analysis done?}
|
142
|
+
B -->|No| C[Analyze application routes]
|
143
|
+
C --> D[Save route configurations]
|
144
|
+
B -->|Yes| E{Method supports caching?}
|
145
|
+
D --> E
|
146
|
+
E -->|No| F[Pass to application]
|
147
|
+
E -->|Yes| G[Find matching route]
|
148
|
+
G --> H{Route found?}
|
149
|
+
H -->|No| F
|
150
|
+
H -->|Yes| I{GET request + CacheConfig?}
|
151
|
+
I -->|Yes| J[Check cache]
|
152
|
+
J --> K{Cache found?}
|
153
|
+
K -->|Yes| L[Return from cache]
|
154
|
+
K -->|No| M[Execute request + save to cache]
|
155
|
+
I -->|No| N{POST/PUT/DELETE + CacheDropConfig?}
|
156
|
+
N -->|Yes| O[Invalidate cache]
|
157
|
+
N -->|No| F
|
158
|
+
O --> F
|
159
|
+
M --> P[Return response]
|
160
|
+
```
|
161
|
+
|
162
|
+
## 🎛️ Storages
|
163
|
+
|
164
|
+
### InMemoryStorage (default)
|
165
|
+
|
166
|
+
```python
|
167
|
+
from fast_cache_middleware import FastCacheMiddleware, InMemoryStorage
|
168
|
+
|
169
|
+
storage = InMemoryStorage(max_size=1000)
|
170
|
+
app.add_middleware(FastCacheMiddleware, storage=storage)
|
171
|
+
```
|
172
|
+
|
173
|
+
InMemoryStorage uses batch cleanup for better performance
|
174
|
+
|
175
|
+
### Custom Storage
|
176
|
+
|
177
|
+
```python
|
178
|
+
from fast_cache_middleware import BaseStorage
|
179
|
+
|
180
|
+
class RedisStorage(BaseStorage):
|
181
|
+
def __init__(self, redis_url: str):
|
182
|
+
import redis
|
183
|
+
self.redis = redis.from_url(redis_url)
|
184
|
+
|
185
|
+
async def store(self, key: str, response, request, metadata):
|
186
|
+
# Implementation for saving to Redis
|
187
|
+
pass
|
188
|
+
|
189
|
+
async def retrieve(self, key: str):
|
190
|
+
# Implementation for retrieving from Redis
|
191
|
+
pass
|
192
|
+
|
193
|
+
app.add_middleware(FastCacheMiddleware, storage=RedisStorage("redis://localhost"))
|
194
|
+
```
|
195
|
+
|
196
|
+
## 🧪 Testing
|
197
|
+
|
198
|
+
```python
|
199
|
+
import pytest
|
200
|
+
from httpx import AsyncClient
|
201
|
+
from examples.basic import app
|
202
|
+
|
203
|
+
@pytest.mark.asyncio
|
204
|
+
async def test_caching():
|
205
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
206
|
+
# First request - cache miss
|
207
|
+
response1 = await client.get("/users/1")
|
208
|
+
assert response1.status_code == 200
|
209
|
+
|
210
|
+
# Second request - cache hit (should be faster)
|
211
|
+
response2 = await client.get("/users/1")
|
212
|
+
assert response2.status_code == 200
|
213
|
+
assert response1.json() == response2.json()
|
214
|
+
|
215
|
+
@pytest.mark.asyncio
|
216
|
+
async def test_cache_invalidation():
|
217
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
218
|
+
# Cache data
|
219
|
+
await client.get("/users/1")
|
220
|
+
|
221
|
+
# Invalidate cache
|
222
|
+
await client.post("/users/1", json={})
|
223
|
+
|
224
|
+
# Next GET should execute new request
|
225
|
+
response = await client.get("/users/1")
|
226
|
+
assert response.status_code == 200
|
227
|
+
```
|
228
|
+
|
229
|
+
## 📊 Performance
|
230
|
+
|
231
|
+
### Benchmarks
|
232
|
+
|
233
|
+
- **Route analysis**: ~5ms for 100 routes at startup
|
234
|
+
- **Route lookup**: ~0.1ms per request (O(n) by number of cached routes)
|
235
|
+
- **Cache hit**: ~1ms per request
|
236
|
+
- **Cache miss**: original request time + ~2ms for saving
|
237
|
+
|
238
|
+
### Optimization
|
239
|
+
|
240
|
+
```python
|
241
|
+
# For applications with many routes
|
242
|
+
app.add_middleware(
|
243
|
+
FastCacheMiddleware,
|
244
|
+
storage=InMemoryStorage(max_size=10000), # Increase cache size
|
245
|
+
controller=Controller(default_ttl=3600) # Increase default TTL
|
246
|
+
)
|
247
|
+
```
|
248
|
+
|
249
|
+
## 🔒 Security
|
250
|
+
|
251
|
+
### Cache Isolation
|
252
|
+
|
253
|
+
```python
|
254
|
+
def user_specific_cache() -> CacheConfig:
|
255
|
+
def secure_key_func(request):
|
256
|
+
# Include user token in key
|
257
|
+
token = request.headers.get("authorization", "").split(" ")[-1]
|
258
|
+
return f"{request.url.path}:token:{token}"
|
259
|
+
|
260
|
+
return CacheConfig(max_age=300, key_func=secure_key_func)
|
261
|
+
|
262
|
+
@app.get("/private/data", dependencies=[Depends(user_specific_cache)])
|
263
|
+
async def get_private_data():
|
264
|
+
return {"sensitive": "data"}
|
265
|
+
```
|
266
|
+
|
267
|
+
### Header Validation
|
268
|
+
|
269
|
+
Middleware automatically respects standard HTTP caching headers:
|
270
|
+
|
271
|
+
- `Cache-Control: no-cache` - skip cache
|
272
|
+
- `Cache-Control: no-store` - forbid caching
|
273
|
+
- `Cache-Control: private` - don't cache private responses
|
274
|
+
|
275
|
+
|
276
|
+
## 🛠️ Advanced Usage
|
277
|
+
|
278
|
+
### Custom Controller
|
279
|
+
|
280
|
+
```python
|
281
|
+
from fast_cache_middleware import Controller
|
282
|
+
|
283
|
+
class CustomController(Controller):
|
284
|
+
async def is_cachable_request(self, request):
|
285
|
+
# Custom logic - don't cache admin requests
|
286
|
+
if request.headers.get("x-admin-request"):
|
287
|
+
return False
|
288
|
+
return await super().should_cache_request(request)
|
289
|
+
|
290
|
+
async def generate_cache_key(self, request):
|
291
|
+
# Add API version to key
|
292
|
+
version = request.headers.get("api-version", "v1")
|
293
|
+
base_key = await super().generate_cache_key(request)
|
294
|
+
return f"{version}:{base_key}"
|
295
|
+
|
296
|
+
app.add_middleware(
|
297
|
+
FastCacheMiddleware,
|
298
|
+
controller=CustomController()
|
299
|
+
)
|
300
|
+
```
|
301
|
+
|
302
|
+
## 📝 Examples
|
303
|
+
|
304
|
+
More examples in the `examples/` folder:
|
305
|
+
|
306
|
+
- **quick_start.py** - minimal example showing basic caching and invalidation
|
307
|
+
- **basic.py** - basic usage with FastAPI
|
308
|
+
|
309
|
+
## 🤝 Contributing
|
310
|
+
|
311
|
+
```bash
|
312
|
+
git clone https://github.com/chud0/FastCacheMiddleware
|
313
|
+
cd FastCacheMiddleware
|
314
|
+
poetry install --with dev
|
315
|
+
./scripts/test.sh
|
316
|
+
```
|
317
|
+
|
318
|
+
## 📄 License
|
319
|
+
|
320
|
+
MIT License - see [LICENSE](LICENSE)
|
321
|
+
|
322
|
+
---
|
323
|
+
|
324
|
+
⭐ **Like the project? Give it a star!**
|
325
|
+
|
326
|
+
🐛 **Found a bug?** [Create an issue](https://github.com/chud0/FastCacheMiddleware/issues)
|
327
|
+
|
328
|
+
💡 **Have an idea?** [Suggest a feature](https://github.com/chud0/FastCacheMiddleware/discussions/categories/ideas)
|