simple-dep-cache 0.2.0__tar.gz → 0.2.1__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.
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/PKG-INFO +1 -1
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/decorators.py +76 -18
- simple_dep_cache-0.2.1/tests/test_silent_backend_errors.py +207 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.claude/hehe +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/publish-pypi.yaml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/static-check.yml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/test.yml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.gitignore +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.pre-commit-config.yaml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.python-version +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.ruff.toml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/CHANGELOG.md +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/README.md +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/docker-compose.yml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/pyproject.toml +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/__init__.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/backends.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/config.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/context.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/events.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/factories.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/fakes.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/manager.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/redis_backends.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/types.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/utils.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/__init__.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_cache_manager.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_config.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_context.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_decorators.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_end_to_end_redis_simple.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_fakes.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_redis_backends.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_types.py +0 -0
- {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple-dep-cache
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Redis-based caching library with intelligent dependency tracking for Python applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/tintty1/simple-dep-cache
|
|
6
6
|
Project-URL: Repository, https://github.com/tintty1/simple-dep-cache
|
|
@@ -149,6 +149,32 @@ def _handle_callback_error(error: Exception, cache_manager: CacheManager, contex
|
|
|
149
149
|
pass
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
def _handle_backend_error(operation: str, func_name: str, error: Exception, silent: bool) -> None:
|
|
153
|
+
"""Handle backend errors by logging or re-raising based on silent flag."""
|
|
154
|
+
if not silent:
|
|
155
|
+
raise
|
|
156
|
+
logger = logging.getLogger(__name__)
|
|
157
|
+
logger.warning("Backend error during %s for %s: %s", operation, func_name, error, exc_info=True)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _safe_backend_op(op: Callable, silent: bool, func_name: str, operation: str) -> Any:
|
|
161
|
+
"""Execute a backend operation with optional error silencing."""
|
|
162
|
+
try:
|
|
163
|
+
return op()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
_handle_backend_error(operation, func_name, e, silent)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _safe_backend_op_async(op: Callable, silent: bool, func_name: str, operation: str) -> Any:
|
|
170
|
+
"""Execute an async backend operation with optional error silencing."""
|
|
171
|
+
try:
|
|
172
|
+
return await op()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
_handle_backend_error(operation, func_name, e, silent)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
152
178
|
def _cache_result_or_exception_sync(
|
|
153
179
|
cache_manager: CacheManager,
|
|
154
180
|
cache_key: str,
|
|
@@ -211,6 +237,7 @@ def cache_with_deps(
|
|
|
211
237
|
dependencies: set | None = None,
|
|
212
238
|
cache_exception_types: list[type[Exception]] | None = None,
|
|
213
239
|
callback: Callable | None = None,
|
|
240
|
+
silent_backend_errors: bool = False,
|
|
214
241
|
) -> Callable:
|
|
215
242
|
"""
|
|
216
243
|
Decorator for caching function results with dependency tracking.
|
|
@@ -226,6 +253,9 @@ def cache_with_deps(
|
|
|
226
253
|
callback: Callback function invoked on cache hit or miss.
|
|
227
254
|
Called with keyword arguments: func, cache_manager, args, kwargs, is_hit, cached_result
|
|
228
255
|
is_hit=True for cache hits, False for cache misses (optional)
|
|
256
|
+
silent_backend_errors: If True, silently log backend errors (e.g., Redis connection errors)
|
|
257
|
+
instead of raising them. The decorated function will execute normally when backend
|
|
258
|
+
errors occur. Defaults to False (optional)
|
|
229
259
|
"""
|
|
230
260
|
|
|
231
261
|
def decorator(func: Callable) -> Callable:
|
|
@@ -244,7 +274,14 @@ def cache_with_deps(
|
|
|
244
274
|
|
|
245
275
|
cache_key = _generate_cache_key(func, args, kwargs)
|
|
246
276
|
|
|
247
|
-
|
|
277
|
+
# Try to get from cache with optional error silencing
|
|
278
|
+
cached_result = await _safe_backend_op_async(
|
|
279
|
+
lambda: active_cache_manager.aget(cache_key),
|
|
280
|
+
silent_backend_errors,
|
|
281
|
+
func.__qualname__,
|
|
282
|
+
"cache get",
|
|
283
|
+
)
|
|
284
|
+
|
|
248
285
|
if cached_result is not None:
|
|
249
286
|
cache_hit_result = _handle_cache_hit(cached_result)
|
|
250
287
|
if cache_hit_result is not None:
|
|
@@ -284,14 +321,21 @@ def cache_with_deps(
|
|
|
284
321
|
finally:
|
|
285
322
|
current_deps = get_current_dependencies()
|
|
286
323
|
effective_ttl = get_cache_ttl()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
324
|
+
|
|
325
|
+
# Try to set cache with optional error silencing
|
|
326
|
+
await _safe_backend_op_async(
|
|
327
|
+
lambda: _cache_result_or_exception_async(
|
|
328
|
+
active_cache_manager,
|
|
329
|
+
cache_key,
|
|
330
|
+
result,
|
|
331
|
+
exception,
|
|
332
|
+
current_deps,
|
|
333
|
+
effective_ttl,
|
|
334
|
+
cache_exception_types,
|
|
335
|
+
),
|
|
336
|
+
silent_backend_errors,
|
|
337
|
+
func.__qualname__,
|
|
338
|
+
"cache set",
|
|
295
339
|
)
|
|
296
340
|
|
|
297
341
|
# Invoke callback for cache miss
|
|
@@ -338,7 +382,14 @@ def cache_with_deps(
|
|
|
338
382
|
|
|
339
383
|
cache_key = _generate_cache_key(func, args, kwargs)
|
|
340
384
|
|
|
341
|
-
|
|
385
|
+
# Try to get from cache with optional error silencing
|
|
386
|
+
cached_result = _safe_backend_op(
|
|
387
|
+
lambda: active_cache_manager.get(cache_key),
|
|
388
|
+
silent_backend_errors,
|
|
389
|
+
func.__qualname__,
|
|
390
|
+
"cache get",
|
|
391
|
+
)
|
|
392
|
+
|
|
342
393
|
if cached_result is not None:
|
|
343
394
|
cache_hit_result = _handle_cache_hit(cached_result)
|
|
344
395
|
if cache_hit_result is not None:
|
|
@@ -368,14 +419,21 @@ def cache_with_deps(
|
|
|
368
419
|
finally:
|
|
369
420
|
current_deps = get_current_dependencies()
|
|
370
421
|
effective_ttl = get_cache_ttl()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
422
|
+
|
|
423
|
+
# Try to set cache with optional error silencing
|
|
424
|
+
_safe_backend_op(
|
|
425
|
+
lambda: _cache_result_or_exception_sync(
|
|
426
|
+
active_cache_manager,
|
|
427
|
+
cache_key,
|
|
428
|
+
result,
|
|
429
|
+
exception,
|
|
430
|
+
current_deps,
|
|
431
|
+
effective_ttl,
|
|
432
|
+
cache_exception_types,
|
|
433
|
+
),
|
|
434
|
+
silent_backend_errors,
|
|
435
|
+
func.__qualname__,
|
|
436
|
+
"cache set",
|
|
379
437
|
)
|
|
380
438
|
|
|
381
439
|
# Invoke callback for cache miss
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Tests for the silent_backend_errors parameter in cache_with_deps decorator."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from simple_dep_cache.decorators import cache_with_deps
|
|
8
|
+
from simple_dep_cache.fakes import FakeAsyncCacheBackend, FakeCacheBackend, FakeConfig
|
|
9
|
+
from simple_dep_cache.manager import get_or_create_cache_manager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestSilentBackendErrors:
|
|
13
|
+
"""Test cases for silent_backend_errors parameter."""
|
|
14
|
+
|
|
15
|
+
def test_silent_backend_errors_on_get(self):
|
|
16
|
+
"""Test that backend errors during get are silently logged."""
|
|
17
|
+
# Create a fake backend that will raise an error on get
|
|
18
|
+
config = FakeConfig(prefix="test_get_error")
|
|
19
|
+
backend = FakeCacheBackend(config)
|
|
20
|
+
cache_manager = get_or_create_cache_manager(
|
|
21
|
+
name="test_get_error", config=config, backend=backend
|
|
22
|
+
)
|
|
23
|
+
assert cache_manager is not None
|
|
24
|
+
|
|
25
|
+
# Mock the backend's get method to raise an error
|
|
26
|
+
backend.get = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
27
|
+
|
|
28
|
+
call_count = 0
|
|
29
|
+
|
|
30
|
+
@cache_with_deps(name=cache_manager.name, silent_backend_errors=True)
|
|
31
|
+
def my_function(x):
|
|
32
|
+
nonlocal call_count
|
|
33
|
+
call_count += 1
|
|
34
|
+
return x * 2
|
|
35
|
+
|
|
36
|
+
# Should execute function normally despite backend error
|
|
37
|
+
result = my_function(5)
|
|
38
|
+
assert result == 10
|
|
39
|
+
assert call_count == 1
|
|
40
|
+
|
|
41
|
+
def test_silent_backend_errors_on_set(self):
|
|
42
|
+
"""Test that backend errors during set are silently logged."""
|
|
43
|
+
# Create a fake backend that will raise an error on set
|
|
44
|
+
config = FakeConfig(prefix="test_set_error")
|
|
45
|
+
backend = FakeCacheBackend(config)
|
|
46
|
+
cache_manager = get_or_create_cache_manager(
|
|
47
|
+
name="test_set_error", config=config, backend=backend
|
|
48
|
+
)
|
|
49
|
+
assert cache_manager is not None
|
|
50
|
+
|
|
51
|
+
# Mock the backend's set method to raise an error
|
|
52
|
+
backend.set = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
53
|
+
|
|
54
|
+
call_count = 0
|
|
55
|
+
|
|
56
|
+
@cache_with_deps(name=cache_manager.name, silent_backend_errors=True)
|
|
57
|
+
def my_function(x):
|
|
58
|
+
nonlocal call_count
|
|
59
|
+
call_count += 1
|
|
60
|
+
return x * 2
|
|
61
|
+
|
|
62
|
+
# Should execute function normally despite backend error
|
|
63
|
+
result = my_function(5)
|
|
64
|
+
assert result == 10
|
|
65
|
+
assert call_count == 1
|
|
66
|
+
|
|
67
|
+
def test_silent_backend_errors_disabled_by_default(self):
|
|
68
|
+
"""Test that backend errors are raised by default (silent_backend_errors=False)."""
|
|
69
|
+
# Create a fake backend that will raise an error on get
|
|
70
|
+
config = FakeConfig(prefix="test_error_default")
|
|
71
|
+
backend = FakeCacheBackend(config)
|
|
72
|
+
cache_manager = get_or_create_cache_manager(
|
|
73
|
+
name="test_error_default", config=config, backend=backend
|
|
74
|
+
)
|
|
75
|
+
assert cache_manager is not None
|
|
76
|
+
|
|
77
|
+
# Mock the backend's get method to raise an error
|
|
78
|
+
backend.get = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
79
|
+
|
|
80
|
+
@cache_with_deps(name=cache_manager.name)
|
|
81
|
+
def my_function(x):
|
|
82
|
+
return x * 2
|
|
83
|
+
|
|
84
|
+
# Should raise the backend error
|
|
85
|
+
with pytest.raises(ConnectionError, match="Redis connection failed"):
|
|
86
|
+
my_function(5)
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_silent_backend_errors_with_async(self):
|
|
90
|
+
"""Test that silent_backend_errors works with async functions."""
|
|
91
|
+
# Create a fake async backend that will raise an error on get
|
|
92
|
+
config = FakeConfig(prefix="test_async_error")
|
|
93
|
+
async_backend = FakeAsyncCacheBackend(config)
|
|
94
|
+
cache_manager = get_or_create_cache_manager(
|
|
95
|
+
name="test_async_error",
|
|
96
|
+
config=config,
|
|
97
|
+
async_backend=async_backend,
|
|
98
|
+
create_async_backend=True,
|
|
99
|
+
)
|
|
100
|
+
assert cache_manager is not None
|
|
101
|
+
|
|
102
|
+
# Mock the backend's get method to raise an error
|
|
103
|
+
async def mock_get(key):
|
|
104
|
+
raise ConnectionError("Redis connection failed")
|
|
105
|
+
|
|
106
|
+
async_backend.get = mock_get
|
|
107
|
+
|
|
108
|
+
call_count = 0
|
|
109
|
+
|
|
110
|
+
@cache_with_deps(name=cache_manager.name, silent_backend_errors=True)
|
|
111
|
+
async def my_async_function(x):
|
|
112
|
+
nonlocal call_count
|
|
113
|
+
call_count += 1
|
|
114
|
+
return x * 2
|
|
115
|
+
|
|
116
|
+
# Should execute function normally despite backend error
|
|
117
|
+
result = await my_async_function(5)
|
|
118
|
+
assert result == 10
|
|
119
|
+
assert call_count == 1
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_silent_backend_errors_async_disabled_by_default(self):
|
|
123
|
+
"""Test that backend errors are raised by default in async functions."""
|
|
124
|
+
# Create a fake async backend that will raise an error on get
|
|
125
|
+
config = FakeConfig(prefix="test_async_error_default")
|
|
126
|
+
async_backend = FakeAsyncCacheBackend(config)
|
|
127
|
+
cache_manager = get_or_create_cache_manager(
|
|
128
|
+
name="test_async_error_default",
|
|
129
|
+
config=config,
|
|
130
|
+
async_backend=async_backend,
|
|
131
|
+
create_async_backend=True,
|
|
132
|
+
)
|
|
133
|
+
assert cache_manager is not None
|
|
134
|
+
|
|
135
|
+
# Mock the backend's get method to raise an error
|
|
136
|
+
async def mock_get(key):
|
|
137
|
+
raise ConnectionError("Redis connection failed")
|
|
138
|
+
|
|
139
|
+
async_backend.get = mock_get
|
|
140
|
+
|
|
141
|
+
@cache_with_deps(name=cache_manager.name)
|
|
142
|
+
async def my_async_function(x):
|
|
143
|
+
return x * 2
|
|
144
|
+
|
|
145
|
+
# Should raise the backend error
|
|
146
|
+
with pytest.raises(ConnectionError, match="Redis connection failed"):
|
|
147
|
+
await my_async_function(5)
|
|
148
|
+
|
|
149
|
+
def test_silent_backend_errors_multiple_calls(self):
|
|
150
|
+
"""Test that function executes correctly with persistent backend errors."""
|
|
151
|
+
config = FakeConfig(prefix="test_multiple_calls")
|
|
152
|
+
backend = FakeCacheBackend(config)
|
|
153
|
+
cache_manager = get_or_create_cache_manager(
|
|
154
|
+
name="test_multiple_calls", config=config, backend=backend
|
|
155
|
+
)
|
|
156
|
+
assert cache_manager is not None
|
|
157
|
+
|
|
158
|
+
# Mock the backend to always fail
|
|
159
|
+
backend.get = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
160
|
+
backend.set = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
161
|
+
|
|
162
|
+
call_count = 0
|
|
163
|
+
|
|
164
|
+
@cache_with_deps(name=cache_manager.name, silent_backend_errors=True)
|
|
165
|
+
def my_function(x):
|
|
166
|
+
nonlocal call_count
|
|
167
|
+
call_count += 1
|
|
168
|
+
return x * 2
|
|
169
|
+
|
|
170
|
+
# Call multiple times - should work each time
|
|
171
|
+
result1 = my_function(5)
|
|
172
|
+
result2 = my_function(10)
|
|
173
|
+
result3 = my_function(5) # Same arg as first call
|
|
174
|
+
|
|
175
|
+
assert result1 == 10
|
|
176
|
+
assert result2 == 20
|
|
177
|
+
assert result3 == 10
|
|
178
|
+
assert call_count == 3 # Function called every time (no caching due to backend errors)
|
|
179
|
+
|
|
180
|
+
def test_silent_backend_errors_partial_failure(self):
|
|
181
|
+
"""Test behavior when only get fails but set works."""
|
|
182
|
+
config = FakeConfig(prefix="test_partial_failure")
|
|
183
|
+
backend = FakeCacheBackend(config)
|
|
184
|
+
cache_manager = get_or_create_cache_manager(
|
|
185
|
+
name="test_partial_failure", config=config, backend=backend
|
|
186
|
+
)
|
|
187
|
+
assert cache_manager is not None
|
|
188
|
+
|
|
189
|
+
# Mock the backend's get to fail, but set works normally
|
|
190
|
+
backend.get = Mock(side_effect=ConnectionError("Redis connection failed"))
|
|
191
|
+
call_count = 0
|
|
192
|
+
|
|
193
|
+
@cache_with_deps(name=cache_manager.name, silent_backend_errors=True)
|
|
194
|
+
def my_function(x):
|
|
195
|
+
nonlocal call_count
|
|
196
|
+
call_count += 1
|
|
197
|
+
return x * 2
|
|
198
|
+
|
|
199
|
+
# First call: get fails, function executes, set succeeds
|
|
200
|
+
result1 = my_function(5)
|
|
201
|
+
assert result1 == 10
|
|
202
|
+
assert call_count == 1
|
|
203
|
+
|
|
204
|
+
# Second call: get still fails, function executes again
|
|
205
|
+
result2 = my_function(5)
|
|
206
|
+
assert result2 == 10
|
|
207
|
+
assert call_count == 2 # Function called again due to get failure
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|