cachu 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl
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/__init__.py +1 -1
- cachu/backends/async_sqlite.py +106 -94
- {cachu-0.2.1.dist-info → cachu-0.2.3.dist-info}/METADATA +1 -1
- {cachu-0.2.1.dist-info → cachu-0.2.3.dist-info}/RECORD +6 -6
- {cachu-0.2.1.dist-info → cachu-0.2.3.dist-info}/WHEEL +0 -0
- {cachu-0.2.1.dist-info → cachu-0.2.3.dist-info}/top_level.txt +0 -0
cachu/__init__.py
CHANGED
|
@@ -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.3'
|
|
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
|
cachu/backends/async_sqlite.py
CHANGED
|
@@ -33,95 +33,109 @@ 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
|
-
|
|
65
|
-
"""
|
|
68
|
+
def _schedule_delete(self, key: str) -> None:
|
|
69
|
+
"""Schedule a background deletion task (fire-and-forget).
|
|
66
70
|
"""
|
|
67
|
-
async
|
|
71
|
+
async def _delete() -> None:
|
|
68
72
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
72
|
-
(key,),
|
|
73
|
-
)
|
|
74
|
-
row = await cursor.fetchone()
|
|
75
|
-
|
|
76
|
-
if row is None:
|
|
77
|
-
return NO_VALUE
|
|
78
|
-
|
|
79
|
-
value_blob, expires_at = row
|
|
80
|
-
if time.time() > expires_at:
|
|
73
|
+
async with self._write_lock:
|
|
74
|
+
conn = await self._ensure_initialized()
|
|
81
75
|
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
82
76
|
await conn.commit()
|
|
83
|
-
return NO_VALUE
|
|
84
|
-
|
|
85
|
-
return pickle.loads(value_blob)
|
|
86
77
|
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
asyncio.create_task(_delete())
|
|
81
|
+
|
|
82
|
+
async def get(self, key: str) -> Any:
|
|
83
|
+
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
conn = await self._ensure_initialized()
|
|
87
|
+
cursor = await conn.execute(
|
|
88
|
+
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
89
|
+
(key,),
|
|
90
|
+
)
|
|
91
|
+
row = await cursor.fetchone()
|
|
92
|
+
|
|
93
|
+
if row is None:
|
|
94
|
+
return NO_VALUE
|
|
95
|
+
|
|
96
|
+
value_blob, expires_at = row
|
|
97
|
+
if time.time() > expires_at:
|
|
98
|
+
self._schedule_delete(key)
|
|
87
99
|
return NO_VALUE
|
|
88
100
|
|
|
101
|
+
return pickle.loads(value_blob)
|
|
102
|
+
except Exception:
|
|
103
|
+
return NO_VALUE
|
|
104
|
+
|
|
89
105
|
async def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
90
106
|
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
91
107
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
row = await cursor.fetchone()
|
|
100
|
-
|
|
101
|
-
if row is None:
|
|
102
|
-
return NO_VALUE, None
|
|
108
|
+
try:
|
|
109
|
+
conn = await self._ensure_initialized()
|
|
110
|
+
cursor = await conn.execute(
|
|
111
|
+
'SELECT value, created_at, expires_at FROM cache WHERE key = ?',
|
|
112
|
+
(key,),
|
|
113
|
+
)
|
|
114
|
+
row = await cursor.fetchone()
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
107
|
-
await conn.commit()
|
|
108
|
-
return NO_VALUE, None
|
|
116
|
+
if row is None:
|
|
117
|
+
return NO_VALUE, None
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
value_blob, created_at, expires_at = row
|
|
120
|
+
if time.time() > expires_at:
|
|
121
|
+
self._schedule_delete(key)
|
|
112
122
|
return NO_VALUE, None
|
|
113
123
|
|
|
124
|
+
return pickle.loads(value_blob), created_at
|
|
125
|
+
except Exception:
|
|
126
|
+
return NO_VALUE, None
|
|
127
|
+
|
|
114
128
|
async def set(self, key: str, value: Any, ttl: int) -> None:
|
|
115
129
|
"""Set value with TTL in seconds.
|
|
116
130
|
"""
|
|
117
131
|
now = time.time()
|
|
118
132
|
value_blob = pickle.dumps(value)
|
|
119
133
|
|
|
120
|
-
async with self.
|
|
134
|
+
async with self._write_lock:
|
|
121
135
|
conn = await self._ensure_initialized()
|
|
122
136
|
await conn.execute(
|
|
123
|
-
|
|
124
|
-
VALUES (?, ?, ?, ?)
|
|
137
|
+
"""INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
|
|
138
|
+
VALUES (?, ?, ?, ?)""",
|
|
125
139
|
(key, value_blob, now, now + ttl),
|
|
126
140
|
)
|
|
127
141
|
await conn.commit()
|
|
@@ -129,7 +143,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
129
143
|
async def delete(self, key: str) -> None:
|
|
130
144
|
"""Delete value by key.
|
|
131
145
|
"""
|
|
132
|
-
async with self.
|
|
146
|
+
async with self._write_lock:
|
|
133
147
|
try:
|
|
134
148
|
conn = await self._ensure_initialized()
|
|
135
149
|
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
@@ -140,7 +154,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
140
154
|
async def clear(self, pattern: str | None = None) -> int:
|
|
141
155
|
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
142
156
|
"""
|
|
143
|
-
async with self.
|
|
157
|
+
async with self._write_lock:
|
|
144
158
|
try:
|
|
145
159
|
conn = await self._ensure_initialized()
|
|
146
160
|
if pattern is None:
|
|
@@ -168,22 +182,21 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
168
182
|
"""Iterate over keys matching pattern.
|
|
169
183
|
"""
|
|
170
184
|
now = time.time()
|
|
185
|
+
conn = await self._ensure_initialized()
|
|
171
186
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
(glob_pattern, now),
|
|
184
|
-
)
|
|
187
|
+
if pattern is None:
|
|
188
|
+
cursor = await conn.execute(
|
|
189
|
+
'SELECT key FROM cache WHERE expires_at > ?',
|
|
190
|
+
(now,),
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
194
|
+
cursor = await conn.execute(
|
|
195
|
+
'SELECT key FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
196
|
+
(glob_pattern, now),
|
|
197
|
+
)
|
|
185
198
|
|
|
186
|
-
|
|
199
|
+
all_keys = [row[0] for row in await cursor.fetchall()]
|
|
187
200
|
|
|
188
201
|
for key in all_keys:
|
|
189
202
|
yield key
|
|
@@ -193,25 +206,24 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
193
206
|
"""
|
|
194
207
|
now = time.time()
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
209
|
+
try:
|
|
210
|
+
conn = await self._ensure_initialized()
|
|
211
|
+
if pattern is None:
|
|
212
|
+
cursor = await conn.execute(
|
|
213
|
+
'SELECT COUNT(*) FROM cache WHERE expires_at > ?',
|
|
214
|
+
(now,),
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
218
|
+
cursor = await conn.execute(
|
|
219
|
+
'SELECT COUNT(*) FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
220
|
+
(glob_pattern, now),
|
|
221
|
+
)
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
row = await cursor.fetchone()
|
|
224
|
+
return row[0]
|
|
225
|
+
except Exception:
|
|
226
|
+
return 0
|
|
215
227
|
|
|
216
228
|
def _fnmatch_to_glob(self, pattern: str) -> str:
|
|
217
229
|
"""Convert fnmatch pattern to SQLite GLOB pattern.
|
|
@@ -223,7 +235,7 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
223
235
|
"""
|
|
224
236
|
now = time.time()
|
|
225
237
|
|
|
226
|
-
async with self.
|
|
238
|
+
async with self._write_lock:
|
|
227
239
|
conn = await self._ensure_initialized()
|
|
228
240
|
cursor = await conn.execute(
|
|
229
241
|
'SELECT COUNT(*) FROM cache WHERE expires_at <= ?',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
cachu/__init__.py,sha256
|
|
1
|
+
cachu/__init__.py,sha256=vMcuJYvSfRUsMwlFrpjS8FOuQiqPBP9e4dRlJbsvpZ0,1286
|
|
2
2
|
cachu/async_decorator.py,sha256=Jx2fHESLlld7NZiD2-6kcozukJtp5efnt4cMhntDDRA,8939
|
|
3
3
|
cachu/async_operations.py,sha256=eVqhZk3FVLNip_abjnCzG8AajzvJTtXbpL--dpMXBlc,5597
|
|
4
4
|
cachu/config.py,sha256=KtcDGpSTJmjRrcNLz9_Om3O814oJJ3p8gntB84Pd6Dk,5922
|
|
@@ -10,12 +10,12 @@ cachu/backends/__init__.py,sha256=Jn2yBAMmJ8d0J_NyjOtxRt7UTyMLf1rlY8QJ049hXE8,13
|
|
|
10
10
|
cachu/backends/async_base.py,sha256=oZ3K3PhsYkbgZxFLFk3_NbxBxtNopqS90HZBizwg_q8,1394
|
|
11
11
|
cachu/backends/async_memory.py,sha256=SQvSHeWbySa52BnQLF75nhVXgsydubNu84a8hvSzQSc,3457
|
|
12
12
|
cachu/backends/async_redis.py,sha256=8kefPIoIJDAZ6C6HJCvHqKFMDS10sJYh8YcJMpXpQm8,4455
|
|
13
|
-
cachu/backends/async_sqlite.py,sha256=
|
|
13
|
+
cachu/backends/async_sqlite.py,sha256=iS1YaVakzK7msKL3BVmnZc_n73-V5fUz1wCQJxEY0ak,8730
|
|
14
14
|
cachu/backends/file.py,sha256=Pu01VtgHDgK6ev5hqyZXuJRCSB2VbNKHQ4w4nNKNyeI,298
|
|
15
15
|
cachu/backends/memory.py,sha256=kIgrVU8k_3Aquyj2PDf8IPbTjCITM_0V5GU47m3fJmo,3138
|
|
16
16
|
cachu/backends/redis.py,sha256=yE5rEBgOij9QOeC1VhWdIbGCgi442q-aWfmbbG4aNSE,3858
|
|
17
17
|
cachu/backends/sqlite.py,sha256=whduN5G_bN6ZJNuCBwbraDcadv_sg0j-OEiFnP8EEsk,7803
|
|
18
|
-
cachu-0.2.
|
|
19
|
-
cachu-0.2.
|
|
20
|
-
cachu-0.2.
|
|
21
|
-
cachu-0.2.
|
|
18
|
+
cachu-0.2.3.dist-info/METADATA,sha256=UXittsVjHwFjAGq6Fl8LAS_2zTKs14BYc9KvOEJHX9I,11992
|
|
19
|
+
cachu-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
cachu-0.2.3.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
|
|
21
|
+
cachu-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|