fastapi-cachex 0.1.4__tar.gz → 0.1.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.
Potentially problematic release.
This version of fastapi-cachex might be problematic. Click here for more details.
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/PKG-INFO +1 -1
- fastapi_cachex-0.1.5/fastapi_cachex/__init__.py +4 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/base.py +23 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/memcached.py +28 -1
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/memory.py +34 -0
- fastapi_cachex-0.1.5/fastapi_cachex/dependencies.py +14 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/PKG-INFO +1 -1
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/SOURCES.txt +2 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/pyproject.toml +1 -1
- fastapi_cachex-0.1.5/tests/test_dependencies.py +38 -0
- fastapi_cachex-0.1.4/fastapi_cachex/__init__.py +0 -2
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/LICENSE +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/README.md +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/cache.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/proxy.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex/types.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/dependency_links.txt +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/requires.txt +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/top_level.txt +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/setup.cfg +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/tests/test_cache.py +0 -0
- {fastapi_cachex-0.1.4 → fastapi_cachex-0.1.5}/tests/test_proxybackend.py +0 -0
|
@@ -25,3 +25,26 @@ class BaseCacheBackend(ABC):
|
|
|
25
25
|
@abstractmethod
|
|
26
26
|
async def clear(self) -> None:
|
|
27
27
|
"""Clear all cached responses."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
31
|
+
"""Clear cached responses for a specific path.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: The path to clear cache for
|
|
35
|
+
include_params: Whether to clear all parameter variations of the path
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Number of cache entries cleared
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def clear_pattern(self, pattern: str) -> int:
|
|
43
|
+
"""Clear cached responses matching a pattern.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
pattern: A glob pattern to match cache keys against (e.g., "/users/*")
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Number of cache entries cleared
|
|
50
|
+
"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
|
+
import warnings
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from fastapi_cachex.backends.base import BaseCacheBackend
|
|
@@ -25,7 +26,7 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
25
26
|
"pymemcache is not installed. Please install it with 'pip install pymemcache'"
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
self.client = HashClient(servers)
|
|
29
|
+
self.client = HashClient(servers, connect_timeout=5, timeout=5)
|
|
29
30
|
|
|
30
31
|
async def get(self, key: str) -> Optional[ETagContent]:
|
|
31
32
|
"""Get value from cache.
|
|
@@ -72,3 +73,29 @@ class MemcachedBackend(BaseCacheBackend):
|
|
|
72
73
|
async def clear(self) -> None:
|
|
73
74
|
"""Clear all values from cache."""
|
|
74
75
|
self.client.flush_all()
|
|
76
|
+
|
|
77
|
+
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
78
|
+
"""Clear cached responses for a specific path."""
|
|
79
|
+
if include_params:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
"Memcached backend does not support pattern-based key clearing. "
|
|
82
|
+
"The include_params option will have no effect.",
|
|
83
|
+
RuntimeWarning,
|
|
84
|
+
stacklevel=2,
|
|
85
|
+
)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
# If we're not including params, we can just try to delete the exact path
|
|
89
|
+
if self.client.delete(path, noreply=False):
|
|
90
|
+
return 1
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
async def clear_pattern(self, pattern: str) -> int: # noqa: ARG002
|
|
94
|
+
"""Clear cached responses matching a pattern."""
|
|
95
|
+
warnings.warn(
|
|
96
|
+
"Memcached backend does not support pattern matching. "
|
|
97
|
+
"Pattern-based cache clearing is not available.",
|
|
98
|
+
RuntimeWarning,
|
|
99
|
+
stacklevel=2,
|
|
100
|
+
)
|
|
101
|
+
return 0
|
|
@@ -53,6 +53,40 @@ class MemoryBackend(BaseCacheBackend):
|
|
|
53
53
|
async with self.lock:
|
|
54
54
|
self.cache.clear()
|
|
55
55
|
|
|
56
|
+
async def clear_path(self, path: str, include_params: bool = False) -> int:
|
|
57
|
+
"""Clear cached responses for a specific path."""
|
|
58
|
+
cleared_count = 0
|
|
59
|
+
async with self.lock:
|
|
60
|
+
keys_to_delete = []
|
|
61
|
+
for key in self.cache:
|
|
62
|
+
cache_path, *params = key.split(":", 1)
|
|
63
|
+
if cache_path == path and (include_params or not params):
|
|
64
|
+
keys_to_delete.append(key)
|
|
65
|
+
cleared_count += 1
|
|
66
|
+
|
|
67
|
+
for key in keys_to_delete:
|
|
68
|
+
del self.cache[key]
|
|
69
|
+
|
|
70
|
+
return cleared_count
|
|
71
|
+
|
|
72
|
+
async def clear_pattern(self, pattern: str) -> int:
|
|
73
|
+
"""Clear cached responses matching a pattern."""
|
|
74
|
+
import fnmatch
|
|
75
|
+
|
|
76
|
+
cleared_count = 0
|
|
77
|
+
async with self.lock:
|
|
78
|
+
keys_to_delete = []
|
|
79
|
+
for key in self.cache:
|
|
80
|
+
cache_path = key.split(":", 1)[0] # Get path part only
|
|
81
|
+
if fnmatch.fnmatch(cache_path, pattern):
|
|
82
|
+
keys_to_delete.append(key)
|
|
83
|
+
cleared_count += 1
|
|
84
|
+
|
|
85
|
+
for key in keys_to_delete:
|
|
86
|
+
del self.cache[key]
|
|
87
|
+
|
|
88
|
+
return cleared_count
|
|
89
|
+
|
|
56
90
|
async def _cleanup_task_impl(self) -> None:
|
|
57
91
|
try:
|
|
58
92
|
while True:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends
|
|
4
|
+
|
|
5
|
+
from fastapi_cachex.backends.base import BaseCacheBackend
|
|
6
|
+
from fastapi_cachex.proxy import BackendProxy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_cache_backend() -> BaseCacheBackend:
|
|
10
|
+
"""Dependency to get the current cache backend instance."""
|
|
11
|
+
return BackendProxy.get_backend()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CacheBackend = Annotated[BaseCacheBackend, Depends(get_cache_backend)]
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
fastapi_cachex/__init__.py
|
|
5
5
|
fastapi_cachex/cache.py
|
|
6
|
+
fastapi_cachex/dependencies.py
|
|
6
7
|
fastapi_cachex/directives.py
|
|
7
8
|
fastapi_cachex/exceptions.py
|
|
8
9
|
fastapi_cachex/proxy.py
|
|
@@ -18,4 +19,5 @@ fastapi_cachex/backends/base.py
|
|
|
18
19
|
fastapi_cachex/backends/memcached.py
|
|
19
20
|
fastapi_cachex/backends/memory.py
|
|
20
21
|
tests/test_cache.py
|
|
22
|
+
tests/test_dependencies.py
|
|
21
23
|
tests/test_proxybackend.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-cachex"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5" # Initial release version
|
|
4
4
|
description = "A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi import FastAPI
|
|
3
|
+
from fastapi.testclient import TestClient
|
|
4
|
+
|
|
5
|
+
from fastapi_cachex import BackendProxy
|
|
6
|
+
from fastapi_cachex import CacheBackend
|
|
7
|
+
from fastapi_cachex.backends import MemoryBackend
|
|
8
|
+
from fastapi_cachex.exceptions import BackendNotFoundError
|
|
9
|
+
|
|
10
|
+
# Setup FastAPI application
|
|
11
|
+
app = FastAPI()
|
|
12
|
+
client = TestClient(app)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Define test endpoint (not a test function)
|
|
16
|
+
@app.get("/test-backend")
|
|
17
|
+
async def backend_endpoint(backend: CacheBackend):
|
|
18
|
+
return {"backend_type": backend.__class__.__name__}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Actual test functions
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_get_cache_backend_no_backend():
|
|
24
|
+
"""Test that get_cache_backend raises BackendNotFoundError when no backend is set."""
|
|
25
|
+
BackendProxy.set_backend(None)
|
|
26
|
+
with pytest.raises(BackendNotFoundError):
|
|
27
|
+
client.get("/test-backend")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_get_cache_backend_with_memory_backend():
|
|
32
|
+
"""Test that get_cache_backend returns the configured backend."""
|
|
33
|
+
backend = MemoryBackend()
|
|
34
|
+
BackendProxy.set_backend(backend)
|
|
35
|
+
|
|
36
|
+
response = client.get("/test-backend")
|
|
37
|
+
assert response.status_code == 200
|
|
38
|
+
assert response.json() == {"backend_type": "MemoryBackend"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|