fast-cache-middleware 0.0.4__py3-none-any.whl โ†’ 0.0.5__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.
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  import re
3
3
  import time
4
- import typing as tp
5
4
  from collections import OrderedDict
5
+ from typing import Any, Dict, Optional, Tuple, Union
6
6
 
7
7
  from starlette.requests import Request
8
8
  from starlette.responses import Response
@@ -14,7 +14,7 @@ from .serializers import BaseSerializer, JSONSerializer, Metadata
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
  # Define type for stored response
17
- StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata]
17
+ StoredResponse: TypeAlias = Tuple[Response, Request, Metadata]
18
18
 
19
19
 
20
20
  # Define base class for cache storage
@@ -28,8 +28,8 @@ class BaseStorage:
28
28
 
29
29
  def __init__(
30
30
  self,
31
- serializer: tp.Optional[BaseSerializer] = None,
32
- ttl: tp.Optional[tp.Union[int, float]] = None,
31
+ serializer: Optional[BaseSerializer] = None,
32
+ ttl: Optional[Union[int, float]] = None,
33
33
  ) -> None:
34
34
  self._serializer = serializer or JSONSerializer()
35
35
 
@@ -43,7 +43,7 @@ class BaseStorage:
43
43
  ) -> None:
44
44
  raise NotImplementedError()
45
45
 
46
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
46
+ async def retrieve(self, key: str) -> Optional[StoredResponse]:
47
47
  raise NotImplementedError()
48
48
 
49
49
  async def remove(self, path: re.Pattern) -> None:
@@ -70,8 +70,8 @@ class InMemoryStorage(BaseStorage):
70
70
  def __init__(
71
71
  self,
72
72
  max_size: int = 1000,
73
- serializer: tp.Optional[BaseSerializer] = None,
74
- ttl: tp.Optional[tp.Union[int, float]] = None,
73
+ serializer: Optional[BaseSerializer] = None,
74
+ ttl: Optional[Union[int, float]] = None,
75
75
  ) -> None:
76
76
  super().__init__(serializer=serializer, ttl=ttl)
77
77
 
@@ -87,7 +87,7 @@ class InMemoryStorage(BaseStorage):
87
87
  # OrderedDict for efficient LRU
88
88
  self._storage: OrderedDict[str, StoredResponse] = OrderedDict()
89
89
  # Separate expiry time storage for fast TTL checking
90
- self._expiry_times: tp.Dict[str, float] = {}
90
+ self._expiry_times: Dict[str, float] = {}
91
91
  self._last_expiry_check_time: float = 0
92
92
  self._expiry_check_interval: float = 60
93
93
 
@@ -126,7 +126,7 @@ class InMemoryStorage(BaseStorage):
126
126
 
127
127
  self._cleanup_lru_items()
128
128
 
129
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
129
+ async def retrieve(self, key: str) -> Optional[StoredResponse]:
130
130
  """Gets response from cache with lazy TTL checking.
131
131
 
132
132
  Element moves to the end to update LRU position.
@@ -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,13 @@
1
+ fast_cache_middleware/__init__.py,sha256=0wuBPKrzjyBA2K9Fnm-xzC1mYSXCMuYr1uPutNXCI5Y,983
2
+ fast_cache_middleware/_helpers.py,sha256=0VbqByNP5AL7mEJpdZLYwef2YhgZsGJOf1tJBshuIZI,904
3
+ fast_cache_middleware/controller.py,sha256=gWGS6r6mWiCOS-cxuzniukzunKowyrnkvl1psamYJHg,7216
4
+ fast_cache_middleware/depends.py,sha256=oHahWfC4PN-rGXbUbmK8TWVlztGO5eswZzaIZdEcfs4,1431
5
+ fast_cache_middleware/exceptions.py,sha256=lv3p2-wAj6UZI7czVSy91eZtuXItr1hZHW2LDz8fI9s,109
6
+ fast_cache_middleware/middleware.py,sha256=ptUwKIe-dpd8kdA9DZDSE98luGtom2BTDvNzlUF9qlM,11940
7
+ fast_cache_middleware/schemas.py,sha256=h7Gq4Pdc1DVkHFGUWUZnI063DXcX-n66Uky9z88cOl8,2331
8
+ fast_cache_middleware/serializers.py,sha256=_aBMbqVWVNYo77xhc7qKuVHg3opIfh8Wa0bYO2j6xn4,2342
9
+ fast_cache_middleware/storages.py,sha256=lErquGfVMDXFZayFDuHm3MKq6Z-0D9-Iru7I1U7b5kE,7614
10
+ fast_cache_middleware-0.0.5.dist-info/LICENSE,sha256=8YvnSj0hf8-FrLOBF2gLYdVKdJsvS83pgYwGZ0EN-Xk,1069
11
+ fast_cache_middleware-0.0.5.dist-info/METADATA,sha256=MYnxUUzrwifLSUrtk_pRHgXtKnk3RFpeGOUtNE2Md4Q,9547
12
+ fast_cache_middleware-0.0.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
13
+ fast_cache_middleware-0.0.5.dist-info/RECORD,,