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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """Flexible caching library with support for memory, file, and Redis backends.
2
2
  """
3
- __version__ = '0.2.1'
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
@@ -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._lock = asyncio.Lock()
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
- if self._connection is None:
43
- aiosqlite = _get_aiosqlite_module()
44
- self._connection = await aiosqlite.connect(self._filepath)
45
-
46
- if not self._initialized:
47
- await self._connection.execute('''
48
- CREATE TABLE IF NOT EXISTS cache (
49
- key TEXT PRIMARY KEY,
50
- value BLOB NOT NULL,
51
- created_at REAL NOT NULL,
52
- expires_at REAL NOT NULL
53
- )
54
- ''')
55
- await self._connection.execute('''
56
- CREATE INDEX IF NOT EXISTS idx_cache_expires
57
- ON cache(expires_at)
58
- ''')
59
- await self._connection.commit()
60
- self._initialized = True
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
- async def get(self, key: str) -> Any:
65
- """Get value by key. Returns NO_VALUE if not found or expired.
68
+ def _schedule_delete(self, key: str) -> None:
69
+ """Schedule a background deletion task (fire-and-forget).
66
70
  """
67
- async with self._lock:
71
+ async def _delete() -> None:
68
72
  try:
69
- conn = await self._ensure_initialized()
70
- cursor = await conn.execute(
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
- async with self._lock:
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()
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
- value_blob, created_at, expires_at = row
105
- if time.time() > expires_at:
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
- return pickle.loads(value_blob), created_at
111
- except Exception:
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._lock:
134
+ async with self._write_lock:
121
135
  conn = await self._ensure_initialized()
122
136
  await conn.execute(
123
- '''INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
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._lock:
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._lock:
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
- async with self._lock:
173
- conn = await self._ensure_initialized()
174
- if pattern is None:
175
- cursor = await conn.execute(
176
- 'SELECT key FROM cache WHERE expires_at > ?',
177
- (now,),
178
- )
179
- else:
180
- glob_pattern = self._fnmatch_to_glob(pattern)
181
- cursor = await conn.execute(
182
- 'SELECT key FROM cache WHERE key GLOB ? AND expires_at > ?',
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
- all_keys = [row[0] for row in await cursor.fetchall()]
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
- async with self._lock:
197
- try:
198
- conn = await self._ensure_initialized()
199
- if pattern is None:
200
- cursor = await conn.execute(
201
- 'SELECT COUNT(*) FROM cache WHERE expires_at > ?',
202
- (now,),
203
- )
204
- else:
205
- glob_pattern = self._fnmatch_to_glob(pattern)
206
- cursor = await conn.execute(
207
- 'SELECT COUNT(*) FROM cache WHERE key GLOB ? AND expires_at > ?',
208
- (glob_pattern, now),
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
- row = await cursor.fetchone()
212
- return row[0]
213
- except Exception:
214
- return 0
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._lock:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachu
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Flexible caching library with sync and async support for memory, file (SQLite), and Redis backends
5
5
  Author: bissli
6
6
  License-Expression: 0BSD
@@ -1,4 +1,4 @@
1
- cachu/__init__.py,sha256=-eqMY3cCuepixdZ-FefQsRXPufVSgrEMlYkYwylSlTM,1286
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=r-c1cNVl6JEApMGhw8Qw7843Vuj_LVRAM-MGgoIjah0,8423
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.1.dist-info/METADATA,sha256=XZxtIkb4Mqd3Mbbw0DAlyfW5N1NJeUKiEPP6ybzIS8Q,11992
19
- cachu-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
- cachu-0.2.1.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
21
- cachu-0.2.1.dist-info/RECORD,,
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