fastapi-cachex 0.1.3__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.

Files changed (26) hide show
  1. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/PKG-INFO +4 -40
  2. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/README.md +3 -39
  3. fastapi_cachex-0.1.5/fastapi_cachex/__init__.py +4 -0
  4. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/base.py +23 -0
  5. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/memcached.py +28 -1
  6. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/memory.py +34 -0
  7. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/cache.py +95 -65
  8. fastapi_cachex-0.1.5/fastapi_cachex/dependencies.py +14 -0
  9. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/PKG-INFO +4 -40
  10. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/SOURCES.txt +2 -0
  11. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/pyproject.toml +1 -1
  12. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/tests/test_cache.py +82 -0
  13. fastapi_cachex-0.1.5/tests/test_dependencies.py +38 -0
  14. fastapi_cachex-0.1.3/fastapi_cachex/__init__.py +0 -2
  15. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/LICENSE +0 -0
  16. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/backends/__init__.py +0 -0
  17. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/directives.py +0 -0
  18. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/exceptions.py +0 -0
  19. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/proxy.py +0 -0
  20. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/py.typed +0 -0
  21. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex/types.py +0 -0
  22. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/dependency_links.txt +0 -0
  23. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/requires.txt +0 -0
  24. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/fastapi_cachex.egg-info/top_level.txt +0 -0
  25. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/setup.cfg +0 -0
  26. {fastapi_cachex-0.1.3 → fastapi_cachex-0.1.5}/tests/test_proxybackend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cachex
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
5
5
  Author-email: Allen <s96016641@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -126,46 +126,10 @@ BackendProxy.set_backend(backend)
126
126
 
127
127
  Redis support is under development and will be available in future releases.
128
128
 
129
- ## Development Guide
129
+ ## Documentation
130
130
 
131
- ### Running Tests
132
-
133
- 1. Run unit tests:
134
-
135
- ```bash
136
- pytest
137
- ```
138
-
139
- 2. Run tests with coverage report:
140
-
141
- ```bash
142
- pytest --cov=fastapi_cachex
143
- ```
144
-
145
- ### Using tox
146
-
147
- tox ensures the code works across different Python versions (3.10-3.13).
148
-
149
- 1. Install all Python versions
150
- 2. Run tox:
151
-
152
- ```bash
153
- tox
154
- ```
155
-
156
- To run for a specific Python version:
157
-
158
- ```bash
159
- tox -e py310 # only run for Python 3.10
160
- ```
161
-
162
- ## Contributing
163
-
164
- 1. Fork the project
165
- 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
166
- 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
167
- 4. Push to the branch (`git push origin feature/AmazingFeature`)
168
- 5. Open a Pull Request
131
+ - [Development Guide](docs/DEVELOPMENT.md)
132
+ - [Contributing Guidelines](docs/CONTRIBUTING.md)
169
133
 
170
134
  ## License
171
135
 
@@ -95,46 +95,10 @@ BackendProxy.set_backend(backend)
95
95
 
96
96
  Redis support is under development and will be available in future releases.
97
97
 
98
- ## Development Guide
98
+ ## Documentation
99
99
 
100
- ### Running Tests
101
-
102
- 1. Run unit tests:
103
-
104
- ```bash
105
- pytest
106
- ```
107
-
108
- 2. Run tests with coverage report:
109
-
110
- ```bash
111
- pytest --cov=fastapi_cachex
112
- ```
113
-
114
- ### Using tox
115
-
116
- tox ensures the code works across different Python versions (3.10-3.13).
117
-
118
- 1. Install all Python versions
119
- 2. Run tox:
120
-
121
- ```bash
122
- tox
123
- ```
124
-
125
- To run for a specific Python version:
126
-
127
- ```bash
128
- tox -e py310 # only run for Python 3.10
129
- ```
130
-
131
- ## Contributing
132
-
133
- 1. Fork the project
134
- 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
135
- 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
136
- 4. Push to the branch (`git push origin feature/AmazingFeature`)
137
- 5. Open a Pull Request
100
+ - [Development Guide](docs/DEVELOPMENT.md)
101
+ - [Contributing Guidelines](docs/CONTRIBUTING.md)
138
102
 
139
103
  ## License
140
104
 
@@ -0,0 +1,4 @@
1
+ from .cache import cache as cache
2
+ from .dependencies import CacheBackend as CacheBackend
3
+ from .dependencies import get_cache_backend as get_cache_backend
4
+ from .proxy import BackendProxy as BackendProxy
@@ -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:
@@ -120,6 +120,43 @@ def cache( # noqa: C901
120
120
  else:
121
121
  request_name = found_request.name
122
122
 
123
+ async def get_cache_control(cache_control: CacheControl) -> str: # noqa: C901
124
+ # Set Cache-Control headers
125
+ if no_cache:
126
+ cache_control.add(DirectiveType.NO_CACHE)
127
+ if must_revalidate:
128
+ cache_control.add(DirectiveType.MUST_REVALIDATE)
129
+ else:
130
+ # Handle normal cache control cases
131
+ # 1. Access scope (public/private)
132
+ if public:
133
+ cache_control.add(DirectiveType.PUBLIC)
134
+ elif private:
135
+ cache_control.add(DirectiveType.PRIVATE)
136
+
137
+ # 2. Cache time settings
138
+ if ttl is not None:
139
+ cache_control.add(DirectiveType.MAX_AGE, ttl)
140
+
141
+ # 3. Validation related
142
+ if must_revalidate:
143
+ cache_control.add(DirectiveType.MUST_REVALIDATE)
144
+
145
+ # 4. Stale response handling
146
+ if stale is not None and stale_ttl is None:
147
+ raise CacheXError("stale_ttl must be set if stale is used")
148
+
149
+ if stale == "revalidate":
150
+ cache_control.add(DirectiveType.STALE_WHILE_REVALIDATE, stale_ttl)
151
+ elif stale == "error":
152
+ cache_control.add(DirectiveType.STALE_IF_ERROR, stale_ttl)
153
+
154
+ # 5. Special flags
155
+ if immutable:
156
+ cache_control.add(DirectiveType.IMMUTABLE)
157
+
158
+ return str(cache_control)
159
+
123
160
  @wraps(func)
124
161
  async def wrapper(*args: Any, **kwargs: Any) -> Response: # noqa: C901
125
162
  if found_request:
@@ -135,79 +172,72 @@ def cache( # noqa: C901
135
172
  if req.method != "GET":
136
173
  return await get_response(func, req, *args, **kwargs)
137
174
 
138
- # Generate cache key
175
+ # Generate cache key and prepare headers
139
176
  cache_key = f"{req.url.path}:{req.query_params}"
140
-
141
- # Check if the data is already in the cache
142
- cached_data = await cache_backend.get(cache_key)
143
-
144
- if cached_data and cached_data.etag == req.headers.get("if-none-match"):
145
- return Response(
146
- status_code=HTTP_304_NOT_MODIFIED,
147
- headers={"ETag": cached_data.etag},
148
- )
149
-
150
- # Get the response
151
- response = await get_response(func, req, *args, **kwargs)
152
-
153
- # Generate ETag (hash based on response content)
154
- etag = f'W/"{hashlib.md5(response.body).hexdigest()}"' # noqa: S324
155
-
156
- # Add ETag to response headers
157
- response.headers["ETag"] = etag
158
-
159
- # Handle Cache-Control header
160
- cache_control = CacheControl()
177
+ client_etag = req.headers.get("if-none-match")
178
+ cache_control = await get_cache_control(CacheControl())
161
179
 
162
180
  # Handle special case: no-store (highest priority)
163
181
  if no_store:
164
- cache_control.add(DirectiveType.NO_STORE)
165
- response.headers["Cache-Control"] = str(cache_control)
182
+ response = await get_response(func, req, *args, **kwargs)
183
+ cc = CacheControl()
184
+ cc.add(DirectiveType.NO_STORE)
185
+ response.headers["Cache-Control"] = str(cc)
166
186
  return response
167
187
 
168
- # Handle special case: no-cache
169
- if no_cache:
170
- cache_control.add(DirectiveType.NO_CACHE)
171
- if must_revalidate:
172
- cache_control.add(DirectiveType.MUST_REVALIDATE)
173
- response.headers["Cache-Control"] = str(cache_control)
174
- return response
188
+ # Check cache and handle ETag validation
189
+ cached_data = await cache_backend.get(cache_key)
175
190
 
176
- # Handle normal cache control cases
177
- # 1. Access scope (public/private)
178
- if public:
179
- cache_control.add(DirectiveType.PUBLIC)
180
- elif private:
181
- cache_control.add(DirectiveType.PRIVATE)
182
-
183
- # 2. Cache time settings
184
- if ttl is not None:
185
- cache_control.add(DirectiveType.MAX_AGE, ttl)
186
-
187
- # 3. Validation related
188
- if must_revalidate:
189
- cache_control.add(DirectiveType.MUST_REVALIDATE)
190
-
191
- # 4. Stale response handling
192
- if stale is not None and stale_ttl is None:
193
- raise CacheXError("stale_ttl must be set if stale is used")
194
-
195
- if stale == "revalidate":
196
- cache_control.add(DirectiveType.STALE_WHILE_REVALIDATE, stale_ttl)
197
- elif stale == "error":
198
- cache_control.add(DirectiveType.STALE_IF_ERROR, stale_ttl)
199
-
200
- # 5. Special flags
201
- if immutable:
202
- cache_control.add(DirectiveType.IMMUTABLE)
203
-
204
- # Store the data in the cache
205
- await cache_backend.set(
206
- cache_key, ETagContent(etag, response.body), ttl=ttl
207
- )
191
+ current_response = None
192
+ current_etag = None
193
+
194
+ if client_etag:
195
+ if no_cache:
196
+ # Get fresh response first if using no-cache
197
+ current_response = await get_response(func, req, *args, **kwargs)
198
+ current_etag = (
199
+ f'W/"{hashlib.md5(current_response.body).hexdigest()}"' # noqa: S324
200
+ )
201
+
202
+ if client_etag == current_etag:
203
+ # For no-cache, compare fresh data with client's ETag
204
+ return Response(
205
+ status_code=HTTP_304_NOT_MODIFIED,
206
+ headers={
207
+ "ETag": current_etag,
208
+ "Cache-Control": cache_control,
209
+ },
210
+ )
211
+
212
+ # Compare with cached ETag
213
+ elif (
214
+ cached_data and client_etag == cached_data.etag
215
+ ): # pragma: no branch
216
+ return Response(
217
+ status_code=HTTP_304_NOT_MODIFIED,
218
+ headers={
219
+ "ETag": cached_data.etag,
220
+ "Cache-Control": cache_control,
221
+ },
222
+ )
223
+
224
+ if not current_response or not current_etag:
225
+ # Retrieve the current response if not already done
226
+ current_response = await get_response(func, req, *args, **kwargs)
227
+ current_etag = f'W/"{hashlib.md5(current_response.body).hexdigest()}"' # noqa: S324
228
+
229
+ # Set ETag header
230
+ current_response.headers["ETag"] = current_etag
231
+
232
+ # Update cache if needed
233
+ if not cached_data or cached_data.etag != current_etag:
234
+ # Store in cache if data changed
235
+ await cache_backend.set(
236
+ cache_key, ETagContent(current_etag, current_response.body), ttl=ttl
237
+ )
208
238
 
209
- response.headers["Cache-Control"] = str(cache_control)
210
- return response
239
+ current_response.headers["Cache-Control"] = cache_control
240
+ return current_response
211
241
 
212
242
  # Update the wrapper with the new signature
213
243
  update_wrapper(wrapper, func)
@@ -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)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cachex
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
5
5
  Author-email: Allen <s96016641@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -126,46 +126,10 @@ BackendProxy.set_backend(backend)
126
126
 
127
127
  Redis support is under development and will be available in future releases.
128
128
 
129
- ## Development Guide
129
+ ## Documentation
130
130
 
131
- ### Running Tests
132
-
133
- 1. Run unit tests:
134
-
135
- ```bash
136
- pytest
137
- ```
138
-
139
- 2. Run tests with coverage report:
140
-
141
- ```bash
142
- pytest --cov=fastapi_cachex
143
- ```
144
-
145
- ### Using tox
146
-
147
- tox ensures the code works across different Python versions (3.10-3.13).
148
-
149
- 1. Install all Python versions
150
- 2. Run tox:
151
-
152
- ```bash
153
- tox
154
- ```
155
-
156
- To run for a specific Python version:
157
-
158
- ```bash
159
- tox -e py310 # only run for Python 3.10
160
- ```
161
-
162
- ## Contributing
163
-
164
- 1. Fork the project
165
- 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
166
- 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
167
- 4. Push to the branch (`git push origin feature/AmazingFeature`)
168
- 5. Open a Pull Request
131
+ - [Development Guide](docs/DEVELOPMENT.md)
132
+ - [Contributing Guidelines](docs/CONTRIBUTING.md)
169
133
 
170
134
  ## License
171
135
 
@@ -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" # Initial release version
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"
@@ -27,6 +27,28 @@ def test_default_cache():
27
27
  assert "ETag" in response.headers
28
28
 
29
29
 
30
+ def test_cache_with_ttl():
31
+ @app.get("/cache-with-ttl")
32
+ @cache(ttl=3)
33
+ async def cache_with_ttl_endpoint():
34
+ return Response(
35
+ content=b'{"message": "This endpoint has a TTL of 30 seconds"}',
36
+ media_type="application/json",
37
+ )
38
+
39
+ response = client.get("/cache-with-ttl")
40
+ assert response.status_code == 200
41
+ assert response.headers["Cache-Control"] == "max-age=3"
42
+ assert "ETag" in response.headers
43
+
44
+ response2 = client.get(
45
+ "/cache-with-ttl", headers={"If-None-Match": response.headers["ETag"]}
46
+ )
47
+ assert response2.status_code == 304
48
+ assert response2.headers["Cache-Control"] == "max-age=3"
49
+ assert response2.headers["ETag"] == response.headers["ETag"]
50
+
51
+
30
52
  def test_ttl_endpoint():
31
53
  @app.get("/ttl")
32
54
  @cache(60)
@@ -344,3 +366,63 @@ def test_response_class_with_etag():
344
366
  etag = response1.headers["ETag"]
345
367
  response2 = client.get("/html-etag", headers={"If-None-Match": etag})
346
368
  assert response2.status_code == 304 # Not Modified
369
+
370
+
371
+ def test_no_cache_with_unchanged_data():
372
+ """Test no-cache behavior when data hasn't changed."""
373
+ counter = 0
374
+
375
+ @app.get("/no-cache-unchanged")
376
+ @cache(no_cache=True)
377
+ async def no_cache_unchanged_endpoint():
378
+ return {"message": "This endpoint uses no-cache", "counter": counter}
379
+
380
+ # First request should return 200
381
+ response1 = client.get("/no-cache-unchanged")
382
+ assert response1.status_code == 200
383
+ assert response1.json() == {"message": "This endpoint uses no-cache", "counter": 0}
384
+ etag1 = response1.headers["ETag"]
385
+ assert "no-cache" in response1.headers["Cache-Control"].lower()
386
+
387
+ # Second request with ETag should return 304 as data hasn't changed
388
+ response2 = client.get("/no-cache-unchanged", headers={"If-None-Match": etag1})
389
+ assert response2.status_code == 304
390
+ assert "ETag" in response2.headers
391
+ assert response2.headers["ETag"] == etag1
392
+
393
+ # Third request without ETag should return 200 but same data
394
+ response3 = client.get("/no-cache-unchanged")
395
+ assert response3.status_code == 200
396
+ assert response3.json() == {"message": "This endpoint uses no-cache", "counter": 0}
397
+ assert response3.headers["ETag"] == etag1
398
+
399
+
400
+ def test_no_cache_with_changing_data():
401
+ """Test no-cache behavior when data changes between requests."""
402
+ counter = {"value": 0}
403
+
404
+ @app.get("/no-cache-changing")
405
+ @cache(no_cache=True)
406
+ async def no_cache_changing_endpoint():
407
+ counter["value"] += 1
408
+ return {"message": "This endpoint uses no-cache", "counter": counter["value"]}
409
+
410
+ # First request
411
+ response1 = client.get("/no-cache-changing")
412
+ assert response1.status_code == 200
413
+ assert response1.json() == {"message": "This endpoint uses no-cache", "counter": 1}
414
+ etag1 = response1.headers["ETag"]
415
+ assert "no-cache" in response1.headers["Cache-Control"].lower()
416
+
417
+ # Second request with previous ETag should still return 200 with new data
418
+ response2 = client.get("/no-cache-changing", headers={"If-None-Match": etag1})
419
+ assert response2.status_code == 200 # Not 304 because data changed
420
+ assert response2.json() == {"message": "This endpoint uses no-cache", "counter": 2}
421
+ etag2 = response2.headers["ETag"]
422
+ assert etag2 != etag1 # ETags should be different as content changed
423
+
424
+ # Third request with latest ETag
425
+ response3 = client.get("/no-cache-changing", headers={"If-None-Match": etag2})
426
+ assert response3.status_code == 200
427
+ assert response3.json() == {"message": "This endpoint uses no-cache", "counter": 3}
428
+ assert response3.headers["ETag"] != etag2 # ETag should change again
@@ -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"}
@@ -1,2 +0,0 @@
1
- from .cache import cache as cache
2
- from .proxy import BackendProxy as BackendProxy
File without changes
File without changes