cachu 0.2.1__py3-none-any.whl → 0.2.2__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.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._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
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
- async with self._lock:
68
- 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
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
- value_blob, expires_at = row
80
- if time.time() > expires_at:
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
- return pickle.loads(value_blob)
86
- except Exception:
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
- 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
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
- 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
101
+ if row is None:
102
+ return NO_VALUE, None
109
103
 
110
- return pickle.loads(value_blob), created_at
111
- except Exception:
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._lock:
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._lock:
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._lock:
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
- 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
- )
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
- all_keys = [row[0] for row in await cursor.fetchall()]
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
- 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
- )
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
- row = await cursor.fetchone()
212
- return row[0]
213
- except Exception:
214
- return 0
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._lock:
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 <= ?',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachu
3
- Version: 0.2.1
3
+ Version: 0.2.2
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=aJsGOKN5kfurhCuJjlefhO1yCZFxgf1P7JZk44Jxofc,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=HbdV1ih-xMSfl8a4-S8xhyiFjkEBPwDlDkCT9AMe4Us,8143
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.2.dist-info/METADATA,sha256=dZL33-xMXzoEepH7HJ1BhSYyS14NFQj8E4wTW6cVEk4,11992
19
+ cachu-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ cachu-0.2.2.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
21
+ cachu-0.2.2.dist-info/RECORD,,
File without changes