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.
Files changed (36) hide show
  1. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/PKG-INFO +1 -1
  2. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/decorators.py +76 -18
  3. simple_dep_cache-0.2.1/tests/test_silent_backend_errors.py +207 -0
  4. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.claude/hehe +0 -0
  5. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/publish-pypi.yaml +0 -0
  6. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/static-check.yml +0 -0
  7. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.github/workflows/test.yml +0 -0
  8. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.gitignore +0 -0
  9. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.pre-commit-config.yaml +0 -0
  10. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.python-version +0 -0
  11. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/.ruff.toml +0 -0
  12. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/CHANGELOG.md +0 -0
  13. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/README.md +0 -0
  14. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/docker-compose.yml +0 -0
  15. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/pyproject.toml +0 -0
  16. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/__init__.py +0 -0
  17. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/backends.py +0 -0
  18. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/config.py +0 -0
  19. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/context.py +0 -0
  20. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/events.py +0 -0
  21. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/factories.py +0 -0
  22. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/fakes.py +0 -0
  23. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/manager.py +0 -0
  24. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/redis_backends.py +0 -0
  25. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/types.py +0 -0
  26. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/src/simple_dep_cache/utils.py +0 -0
  27. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/__init__.py +0 -0
  28. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_cache_manager.py +0 -0
  29. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_config.py +0 -0
  30. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_context.py +0 -0
  31. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_decorators.py +0 -0
  32. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_end_to_end_redis_simple.py +0 -0
  33. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_fakes.py +0 -0
  34. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_redis_backends.py +0 -0
  35. {simple_dep_cache-0.2.0 → simple_dep_cache-0.2.1}/tests/test_types.py +0 -0
  36. {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.0
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
- cached_result = await active_cache_manager.aget(cache_key)
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
- await _cache_result_or_exception_async(
288
- active_cache_manager,
289
- cache_key,
290
- result,
291
- exception,
292
- current_deps,
293
- effective_ttl,
294
- cache_exception_types,
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
- cached_result = active_cache_manager.get(cache_key)
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
- _cache_result_or_exception_sync(
372
- active_cache_manager,
373
- cache_key,
374
- result,
375
- exception,
376
- current_deps,
377
- effective_ttl,
378
- cache_exception_types,
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