cachu 0.2.1__tar.gz → 0.2.2__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.1 → cachu-0.2.2}/PKG-INFO +1 -1
- {cachu-0.2.1 → cachu-0.2.2}/pyproject.toml +1 -1
- {cachu-0.2.1 → cachu-0.2.2}/setup.cfg +1 -1
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/__init__.py +1 -1
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/async_sqlite.py +88 -92
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu.egg-info/PKG-INFO +1 -1
- {cachu-0.2.1 → cachu-0.2.2}/README.md +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/async_decorator.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/async_operations.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/__init__.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/async_base.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/async_memory.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/async_redis.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/file.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/memory.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/redis.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/backends/sqlite.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/config.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/decorator.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/keys.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/operations.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu/types.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu.egg-info/SOURCES.txt +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu.egg-info/dependency_links.txt +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu.egg-info/requires.txt +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/src/cachu.egg-info/top_level.txt +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_async_memory.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_async_redis.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_async_sqlite.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_clearing.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_config.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_defaultcache.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_delete_keys.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_disable.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_exclude_params.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_file_cache.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_integration.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_memory_cache.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_namespace.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_namespace_isolation.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_redis_cache.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_set_keys.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_sqlite_backend.py +0 -0
- {cachu-0.2.1 → cachu-0.2.2}/tests/test_ttl_isolation.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.2'
|
|
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
|
|
@@ -33,91 +33,89 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
33
33
|
def __init__(self, filepath: str) -> None:
|
|
34
34
|
self._filepath = filepath
|
|
35
35
|
self._connection: aiosqlite.Connection | None = None
|
|
36
|
-
self.
|
|
36
|
+
self._init_lock = asyncio.Lock()
|
|
37
|
+
self._write_lock = asyncio.Lock()
|
|
37
38
|
self._initialized = False
|
|
38
39
|
|
|
39
40
|
async def _ensure_initialized(self) -> 'aiosqlite.Connection':
|
|
40
41
|
"""Ensure database is initialized and return connection.
|
|
41
42
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
async with self._init_lock:
|
|
44
|
+
if self._connection is None:
|
|
45
|
+
aiosqlite = _get_aiosqlite_module()
|
|
46
|
+
self._connection = await aiosqlite.connect(self._filepath)
|
|
47
|
+
await self._connection.execute('PRAGMA journal_mode=WAL')
|
|
48
|
+
await self._connection.execute('PRAGMA busy_timeout=5000')
|
|
49
|
+
|
|
50
|
+
if not self._initialized:
|
|
51
|
+
await self._connection.execute('''
|
|
52
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value BLOB NOT NULL,
|
|
55
|
+
created_at REAL NOT NULL,
|
|
56
|
+
expires_at REAL NOT NULL
|
|
57
|
+
)
|
|
58
|
+
''')
|
|
59
|
+
await self._connection.execute('''
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_cache_expires
|
|
61
|
+
ON cache(expires_at)
|
|
62
|
+
''')
|
|
63
|
+
await self._connection.commit()
|
|
64
|
+
self._initialized = True
|
|
61
65
|
|
|
62
66
|
return self._connection
|
|
63
67
|
|
|
64
68
|
async def get(self, key: str) -> Any:
|
|
65
69
|
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
66
70
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
row = await cursor.fetchone()
|
|
75
|
-
|
|
76
|
-
if row is None:
|
|
77
|
-
return NO_VALUE
|
|
71
|
+
try:
|
|
72
|
+
conn = await self._ensure_initialized()
|
|
73
|
+
cursor = await conn.execute(
|
|
74
|
+
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
75
|
+
(key,),
|
|
76
|
+
)
|
|
77
|
+
row = await cursor.fetchone()
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
82
|
-
await conn.commit()
|
|
83
|
-
return NO_VALUE
|
|
79
|
+
if row is None:
|
|
80
|
+
return NO_VALUE
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
value_blob, expires_at = row
|
|
83
|
+
if time.time() > expires_at:
|
|
87
84
|
return NO_VALUE
|
|
88
85
|
|
|
86
|
+
return pickle.loads(value_blob)
|
|
87
|
+
except Exception:
|
|
88
|
+
return NO_VALUE
|
|
89
|
+
|
|
89
90
|
async def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
90
91
|
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
91
92
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
row = await cursor.fetchone()
|
|
100
|
-
|
|
101
|
-
if row is None:
|
|
102
|
-
return NO_VALUE, None
|
|
93
|
+
try:
|
|
94
|
+
conn = await self._ensure_initialized()
|
|
95
|
+
cursor = await conn.execute(
|
|
96
|
+
'SELECT value, created_at, expires_at FROM cache WHERE key = ?',
|
|
97
|
+
(key,),
|
|
98
|
+
)
|
|
99
|
+
row = await cursor.fetchone()
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
107
|
-
await conn.commit()
|
|
108
|
-
return NO_VALUE, None
|
|
101
|
+
if row is None:
|
|
102
|
+
return NO_VALUE, None
|
|
109
103
|
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
value_blob, created_at, expires_at = row
|
|
105
|
+
if time.time() > expires_at:
|
|
112
106
|
return NO_VALUE, None
|
|
113
107
|
|
|
108
|
+
return pickle.loads(value_blob), created_at
|
|
109
|
+
except Exception:
|
|
110
|
+
return NO_VALUE, None
|
|
111
|
+
|
|
114
112
|
async def set(self, key: str, value: Any, ttl: int) -> None:
|
|
115
113
|
"""Set value with TTL in seconds.
|
|
116
114
|
"""
|
|
117
115
|
now = time.time()
|
|
118
116
|
value_blob = pickle.dumps(value)
|
|
119
117
|
|
|
120
|
-
async with self.
|
|
118
|
+
async with self._write_lock:
|
|
121
119
|
conn = await self._ensure_initialized()
|
|
122
120
|
await conn.execute(
|
|
123
121
|
'''INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
|
|
@@ -129,7 +127,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
129
127
|
async def delete(self, key: str) -> None:
|
|
130
128
|
"""Delete value by key.
|
|
131
129
|
"""
|
|
132
|
-
async with self.
|
|
130
|
+
async with self._write_lock:
|
|
133
131
|
try:
|
|
134
132
|
conn = await self._ensure_initialized()
|
|
135
133
|
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
@@ -140,7 +138,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
140
138
|
async def clear(self, pattern: str | None = None) -> int:
|
|
141
139
|
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
142
140
|
"""
|
|
143
|
-
async with self.
|
|
141
|
+
async with self._write_lock:
|
|
144
142
|
try:
|
|
145
143
|
conn = await self._ensure_initialized()
|
|
146
144
|
if pattern is None:
|
|
@@ -168,22 +166,21 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
168
166
|
"""Iterate over keys matching pattern.
|
|
169
167
|
"""
|
|
170
168
|
now = time.time()
|
|
169
|
+
conn = await self._ensure_initialized()
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
(glob_pattern, now),
|
|
184
|
-
)
|
|
171
|
+
if pattern is None:
|
|
172
|
+
cursor = await conn.execute(
|
|
173
|
+
'SELECT key FROM cache WHERE expires_at > ?',
|
|
174
|
+
(now,),
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
178
|
+
cursor = await conn.execute(
|
|
179
|
+
'SELECT key FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
180
|
+
(glob_pattern, now),
|
|
181
|
+
)
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
all_keys = [row[0] for row in await cursor.fetchall()]
|
|
187
184
|
|
|
188
185
|
for key in all_keys:
|
|
189
186
|
yield key
|
|
@@ -193,25 +190,24 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
193
190
|
"""
|
|
194
191
|
now = time.time()
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
193
|
+
try:
|
|
194
|
+
conn = await self._ensure_initialized()
|
|
195
|
+
if pattern is None:
|
|
196
|
+
cursor = await conn.execute(
|
|
197
|
+
'SELECT COUNT(*) FROM cache WHERE expires_at > ?',
|
|
198
|
+
(now,),
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
202
|
+
cursor = await conn.execute(
|
|
203
|
+
'SELECT COUNT(*) FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
204
|
+
(glob_pattern, now),
|
|
205
|
+
)
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
row = await cursor.fetchone()
|
|
208
|
+
return row[0]
|
|
209
|
+
except Exception:
|
|
210
|
+
return 0
|
|
215
211
|
|
|
216
212
|
def _fnmatch_to_glob(self, pattern: str) -> str:
|
|
217
213
|
"""Convert fnmatch pattern to SQLite GLOB pattern.
|
|
@@ -223,7 +219,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
223
219
|
"""
|
|
224
220
|
now = time.time()
|
|
225
221
|
|
|
226
|
-
async with self.
|
|
222
|
+
async with self._write_lock:
|
|
227
223
|
conn = await self._ensure_initialized()
|
|
228
224
|
cursor = await conn.execute(
|
|
229
225
|
'SELECT COUNT(*) FROM cache WHERE expires_at <= ?',
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|