cachu 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.
- {cachu-0.2.0 → cachu-0.2.1}/PKG-INFO +1 -1
- {cachu-0.2.0 → cachu-0.2.1}/pyproject.toml +1 -1
- {cachu-0.2.0 → cachu-0.2.1}/setup.cfg +1 -1
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/__init__.py +1 -1
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/async_decorator.py +2 -1
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/decorator.py +2 -2
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu.egg-info/PKG-INFO +1 -1
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu.egg-info/SOURCES.txt +2 -1
- cachu-0.2.1/tests/test_ttl_isolation.py +246 -0
- {cachu-0.2.0 → cachu-0.2.1}/README.md +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/async_operations.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/__init__.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/async_base.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/async_memory.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/async_redis.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/async_sqlite.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/file.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/memory.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/redis.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/backends/sqlite.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/config.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/keys.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/operations.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu/types.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu.egg-info/dependency_links.txt +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu.egg-info/requires.txt +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/src/cachu.egg-info/top_level.txt +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_async_memory.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_async_redis.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_async_sqlite.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_clearing.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_config.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_defaultcache.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_delete_keys.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_disable.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_exclude_params.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_file_cache.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_integration.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_memory_cache.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_namespace.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_namespace_isolation.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_redis_cache.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_set_keys.py +0 -0
- {cachu-0.2.0 → cachu-0.2.1}/tests/test_sqlite_backend.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Flexible caching library with support for memory, file, and Redis backends.
|
|
2
2
|
"""
|
|
3
|
-
__version__ = '0.2.
|
|
3
|
+
__version__ = '0.2.1'
|
|
4
4
|
|
|
5
5
|
from .async_decorator import async_cache, clear_async_backends
|
|
6
6
|
from .async_decorator import get_async_backend, get_async_cache_info
|
|
@@ -66,7 +66,8 @@ async def _get_async_backend(package: str | None, backend_type: str, ttl: int) -
|
|
|
66
66
|
async def get_async_backend(
|
|
67
67
|
backend_type: str | None = None,
|
|
68
68
|
package: str | None = None,
|
|
69
|
-
|
|
69
|
+
*,
|
|
70
|
+
ttl: int,
|
|
70
71
|
) -> AsyncBackend:
|
|
71
72
|
"""Get an async backend instance.
|
|
72
73
|
|
|
@@ -4,9 +4,9 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from functools import wraps
|
|
8
9
|
from typing import Any
|
|
9
|
-
from collections.abc import Callable
|
|
10
10
|
|
|
11
11
|
from .backends import NO_VALUE, Backend
|
|
12
12
|
from .backends.file import FileBackend
|
|
@@ -61,7 +61,7 @@ def _get_backend(package: str | None, backend_type: str, ttl: int) -> Backend:
|
|
|
61
61
|
return backend
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def get_backend(backend_type: str | None = None, package: str | None = None, ttl: int
|
|
64
|
+
def get_backend(backend_type: str | None = None, package: str | None = None, *, ttl: int) -> Backend:
|
|
65
65
|
"""Get a backend instance.
|
|
66
66
|
|
|
67
67
|
Args:
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Tests for TTL-based backend isolation.
|
|
2
|
+
|
|
3
|
+
These tests verify that different TTL values result in separate backends
|
|
4
|
+
(and separate database files for file backend), documenting the behavior
|
|
5
|
+
that can cause confusion when using get_async_backend() without matching TTL.
|
|
6
|
+
"""
|
|
7
|
+
import cachu
|
|
8
|
+
import pytest
|
|
9
|
+
from cachu.async_decorator import _get_async_backend, get_async_backend
|
|
10
|
+
from cachu.decorator import _get_backend, clear_backends, get_backend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(autouse=True)
|
|
14
|
+
async def clear_async_backends():
|
|
15
|
+
"""Clear async backends before and after each test.
|
|
16
|
+
"""
|
|
17
|
+
await cachu.clear_async_backends()
|
|
18
|
+
yield
|
|
19
|
+
await cachu.clear_async_backends()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def clear_sync_backends():
|
|
24
|
+
"""Clear sync backends before and after each test.
|
|
25
|
+
"""
|
|
26
|
+
clear_backends()
|
|
27
|
+
yield
|
|
28
|
+
clear_backends()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_different_ttl_creates_separate_backends_memory(temp_cache_dir):
|
|
32
|
+
"""Verify different TTLs create separate memory backend instances.
|
|
33
|
+
"""
|
|
34
|
+
backend_5min = await _get_async_backend(None, 'memory', 300)
|
|
35
|
+
backend_24h = await _get_async_backend(None, 'memory', 86400)
|
|
36
|
+
|
|
37
|
+
assert backend_5min is not backend_24h
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_same_ttl_reuses_backend(temp_cache_dir):
|
|
41
|
+
"""Verify same TTL reuses the same backend instance.
|
|
42
|
+
"""
|
|
43
|
+
backend1 = await _get_async_backend(None, 'memory', 300)
|
|
44
|
+
backend2 = await _get_async_backend(None, 'memory', 300)
|
|
45
|
+
|
|
46
|
+
assert backend1 is backend2
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def test_ttl_to_filename_seconds(temp_cache_dir):
|
|
50
|
+
"""Verify TTL < 60s maps to cache{ttl}sec.db filename.
|
|
51
|
+
"""
|
|
52
|
+
backend = await _get_async_backend(None, 'file', 30)
|
|
53
|
+
assert 'cache30sec.db' in backend._filepath
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def test_ttl_to_filename_minutes(temp_cache_dir):
|
|
57
|
+
"""Verify TTL 60-3599s maps to cache{minutes}min.db filename.
|
|
58
|
+
"""
|
|
59
|
+
backend = await _get_async_backend(None, 'file', 300)
|
|
60
|
+
assert 'cache5min.db' in backend._filepath
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def test_ttl_to_filename_hours(temp_cache_dir):
|
|
64
|
+
"""Verify TTL >= 3600s maps to cache{hours}hour.db filename.
|
|
65
|
+
"""
|
|
66
|
+
backend = await _get_async_backend(None, 'file', 86400)
|
|
67
|
+
assert 'cache24hour.db' in backend._filepath
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def test_different_ttl_uses_different_files(temp_cache_dir):
|
|
71
|
+
"""Verify different TTLs create backends with different database files.
|
|
72
|
+
"""
|
|
73
|
+
backend_5min = await _get_async_backend(None, 'file', 300)
|
|
74
|
+
backend_24h = await _get_async_backend(None, 'file', 86400)
|
|
75
|
+
|
|
76
|
+
assert backend_5min._filepath != backend_24h._filepath
|
|
77
|
+
assert 'cache5min.db' in backend_5min._filepath
|
|
78
|
+
assert 'cache24hour.db' in backend_24h._filepath
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_count_returns_zero_with_wrong_ttl(temp_cache_dir):
|
|
82
|
+
"""Document gotcha: count() returns 0 when querying with wrong TTL.
|
|
83
|
+
|
|
84
|
+
This test documents the behavior that caused the kynex-proxy bug:
|
|
85
|
+
caching with ttl=86400 but querying count with ttl=300 returns 0
|
|
86
|
+
because they use different database files.
|
|
87
|
+
"""
|
|
88
|
+
backend_cache = await _get_async_backend(None, 'file', 86400)
|
|
89
|
+
await backend_cache.set('key1', 'value1', 86400)
|
|
90
|
+
await backend_cache.set('key2', 'value2', 86400)
|
|
91
|
+
|
|
92
|
+
backend_query = await _get_async_backend(None, 'file', 300)
|
|
93
|
+
wrong_ttl_count = await backend_query.count()
|
|
94
|
+
|
|
95
|
+
correct_ttl_count = await backend_cache.count()
|
|
96
|
+
|
|
97
|
+
assert wrong_ttl_count == 0
|
|
98
|
+
assert correct_ttl_count == 2
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def test_get_async_backend_public_api_with_ttl(temp_cache_dir):
|
|
102
|
+
"""Verify public get_async_backend() respects TTL parameter.
|
|
103
|
+
"""
|
|
104
|
+
backend1 = await get_async_backend(backend_type='file', ttl=300)
|
|
105
|
+
backend2 = await get_async_backend(backend_type='file', ttl=86400)
|
|
106
|
+
|
|
107
|
+
assert backend1 is not backend2
|
|
108
|
+
assert 'cache5min.db' in backend1._filepath
|
|
109
|
+
assert 'cache24hour.db' in backend2._filepath
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def test_async_get_backend_requires_ttl(temp_cache_dir):
|
|
113
|
+
"""Verify get_async_backend() requires ttl parameter.
|
|
114
|
+
"""
|
|
115
|
+
with pytest.raises(TypeError, match='ttl'):
|
|
116
|
+
await get_async_backend(backend_type='file')
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def test_decorator_and_get_backend_must_match_ttl(temp_cache_dir):
|
|
120
|
+
"""Demonstrate correct pattern: decorator TTL must match get_backend TTL.
|
|
121
|
+
"""
|
|
122
|
+
call_count = 0
|
|
123
|
+
|
|
124
|
+
@cachu.async_cache(ttl=86400, backend='file')
|
|
125
|
+
async def cached_func(x: int) -> int:
|
|
126
|
+
nonlocal call_count
|
|
127
|
+
call_count += 1
|
|
128
|
+
return x * 2
|
|
129
|
+
|
|
130
|
+
await cached_func(5)
|
|
131
|
+
assert call_count == 1
|
|
132
|
+
|
|
133
|
+
backend_correct = await get_async_backend(backend_type='file', ttl=86400)
|
|
134
|
+
correct_count = await backend_correct.count()
|
|
135
|
+
assert correct_count >= 1
|
|
136
|
+
|
|
137
|
+
backend_wrong = await get_async_backend(backend_type='file', ttl=300)
|
|
138
|
+
wrong_count = await backend_wrong.count()
|
|
139
|
+
assert wrong_count == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_sync_different_ttl_creates_separate_backends_memory(temp_cache_dir):
|
|
143
|
+
"""Verify different TTLs create separate memory backend instances.
|
|
144
|
+
"""
|
|
145
|
+
backend_5min = _get_backend(None, 'memory', 300)
|
|
146
|
+
backend_24h = _get_backend(None, 'memory', 86400)
|
|
147
|
+
|
|
148
|
+
assert backend_5min is not backend_24h
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_sync_same_ttl_reuses_backend(temp_cache_dir):
|
|
152
|
+
"""Verify same TTL reuses the same backend instance.
|
|
153
|
+
"""
|
|
154
|
+
backend1 = _get_backend(None, 'memory', 300)
|
|
155
|
+
backend2 = _get_backend(None, 'memory', 300)
|
|
156
|
+
|
|
157
|
+
assert backend1 is backend2
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_sync_ttl_to_filename_seconds(temp_cache_dir):
|
|
161
|
+
"""Verify TTL < 60s maps to cache{ttl}sec.db filename.
|
|
162
|
+
"""
|
|
163
|
+
backend = _get_backend(None, 'file', 30)
|
|
164
|
+
assert 'cache30sec.db' in backend._filepath
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_sync_ttl_to_filename_minutes(temp_cache_dir):
|
|
168
|
+
"""Verify TTL 60-3599s maps to cache{minutes}min.db filename.
|
|
169
|
+
"""
|
|
170
|
+
backend = _get_backend(None, 'file', 300)
|
|
171
|
+
assert 'cache5min.db' in backend._filepath
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_sync_ttl_to_filename_hours(temp_cache_dir):
|
|
175
|
+
"""Verify TTL >= 3600s maps to cache{hours}hour.db filename.
|
|
176
|
+
"""
|
|
177
|
+
backend = _get_backend(None, 'file', 86400)
|
|
178
|
+
assert 'cache24hour.db' in backend._filepath
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_sync_different_ttl_uses_different_files(temp_cache_dir):
|
|
182
|
+
"""Verify different TTLs create backends with different database files.
|
|
183
|
+
"""
|
|
184
|
+
backend_5min = _get_backend(None, 'file', 300)
|
|
185
|
+
backend_24h = _get_backend(None, 'file', 86400)
|
|
186
|
+
|
|
187
|
+
assert backend_5min._filepath != backend_24h._filepath
|
|
188
|
+
assert 'cache5min.db' in backend_5min._filepath
|
|
189
|
+
assert 'cache24hour.db' in backend_24h._filepath
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_sync_count_returns_zero_with_wrong_ttl(temp_cache_dir):
|
|
193
|
+
"""Document gotcha: count() returns 0 when querying with wrong TTL.
|
|
194
|
+
"""
|
|
195
|
+
backend_cache = _get_backend(None, 'file', 86400)
|
|
196
|
+
backend_cache.set('key1', 'value1', 86400)
|
|
197
|
+
backend_cache.set('key2', 'value2', 86400)
|
|
198
|
+
|
|
199
|
+
backend_query = _get_backend(None, 'file', 300)
|
|
200
|
+
wrong_ttl_count = backend_query.count()
|
|
201
|
+
|
|
202
|
+
correct_ttl_count = backend_cache.count()
|
|
203
|
+
|
|
204
|
+
assert wrong_ttl_count == 0
|
|
205
|
+
assert correct_ttl_count == 2
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_sync_get_backend_public_api_with_ttl(temp_cache_dir):
|
|
209
|
+
"""Verify public get_backend() respects TTL parameter.
|
|
210
|
+
"""
|
|
211
|
+
backend1 = get_backend(backend_type='file', ttl=300)
|
|
212
|
+
backend2 = get_backend(backend_type='file', ttl=86400)
|
|
213
|
+
|
|
214
|
+
assert backend1 is not backend2
|
|
215
|
+
assert 'cache5min.db' in backend1._filepath
|
|
216
|
+
assert 'cache24hour.db' in backend2._filepath
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_sync_get_backend_requires_ttl(temp_cache_dir):
|
|
220
|
+
"""Verify get_backend() requires ttl parameter.
|
|
221
|
+
"""
|
|
222
|
+
with pytest.raises(TypeError, match='ttl'):
|
|
223
|
+
get_backend(backend_type='file')
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_sync_decorator_and_get_backend_must_match_ttl(temp_cache_dir):
|
|
227
|
+
"""Demonstrate correct pattern: decorator TTL must match get_backend TTL.
|
|
228
|
+
"""
|
|
229
|
+
call_count = 0
|
|
230
|
+
|
|
231
|
+
@cachu.cache(ttl=86400, backend='file')
|
|
232
|
+
def cached_func(x: int) -> int:
|
|
233
|
+
nonlocal call_count
|
|
234
|
+
call_count += 1
|
|
235
|
+
return x * 2
|
|
236
|
+
|
|
237
|
+
cached_func(5)
|
|
238
|
+
assert call_count == 1
|
|
239
|
+
|
|
240
|
+
backend_correct = get_backend(backend_type='file', ttl=86400)
|
|
241
|
+
correct_count = backend_correct.count()
|
|
242
|
+
assert correct_count >= 1
|
|
243
|
+
|
|
244
|
+
backend_wrong = get_backend(backend_type='file', ttl=300)
|
|
245
|
+
wrong_count = backend_wrong.count()
|
|
246
|
+
assert wrong_count == 0
|
|
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
|
|
File without changes
|
|
File without changes
|