cachu 0.1.2__py3-none-any.whl → 0.2.0__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 +18 -1
- cachu/async_decorator.py +261 -0
- cachu/async_operations.py +178 -0
- cachu/backends/__init__.py +6 -1
- cachu/backends/async_base.py +50 -0
- cachu/backends/async_memory.py +111 -0
- cachu/backends/async_redis.py +141 -0
- cachu/backends/async_sqlite.py +244 -0
- cachu/backends/file.py +7 -155
- cachu/backends/sqlite.py +240 -0
- cachu/decorator.py +4 -9
- cachu/operations.py +24 -16
- {cachu-0.1.2.dist-info → cachu-0.2.0.dist-info}/METADATA +84 -22
- cachu-0.2.0.dist-info/RECORD +22 -0
- {cachu-0.1.2.dist-info → cachu-0.2.0.dist-info}/WHEEL +1 -1
- cachu-0.1.2.dist-info/RECORD +0 -15
- {cachu-0.1.2.dist-info → cachu-0.2.0.dist-info}/top_level.txt +0 -0
cachu/backends/sqlite.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""SQLite-based cache backend.
|
|
2
|
+
"""
|
|
3
|
+
import fnmatch
|
|
4
|
+
import pickle
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import NO_VALUE, Backend
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SqliteBackend(Backend):
|
|
15
|
+
"""SQLite file-based cache backend.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, filepath: str) -> None:
|
|
19
|
+
self._filepath = filepath
|
|
20
|
+
self._lock = threading.RLock()
|
|
21
|
+
self._init_db()
|
|
22
|
+
|
|
23
|
+
def _init_db(self) -> None:
|
|
24
|
+
"""Initialize database schema.
|
|
25
|
+
"""
|
|
26
|
+
with self._lock:
|
|
27
|
+
conn = sqlite3.connect(self._filepath)
|
|
28
|
+
try:
|
|
29
|
+
conn.execute('''
|
|
30
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
31
|
+
key TEXT PRIMARY KEY,
|
|
32
|
+
value BLOB NOT NULL,
|
|
33
|
+
created_at REAL NOT NULL,
|
|
34
|
+
expires_at REAL NOT NULL
|
|
35
|
+
)
|
|
36
|
+
''')
|
|
37
|
+
conn.execute('''
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_cache_expires
|
|
39
|
+
ON cache(expires_at)
|
|
40
|
+
''')
|
|
41
|
+
conn.commit()
|
|
42
|
+
finally:
|
|
43
|
+
conn.close()
|
|
44
|
+
|
|
45
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
46
|
+
"""Get a database connection.
|
|
47
|
+
"""
|
|
48
|
+
return sqlite3.connect(self._filepath)
|
|
49
|
+
|
|
50
|
+
def get(self, key: str) -> Any:
|
|
51
|
+
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
52
|
+
"""
|
|
53
|
+
with self._lock:
|
|
54
|
+
conn = self._get_connection()
|
|
55
|
+
try:
|
|
56
|
+
cursor = conn.execute(
|
|
57
|
+
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
58
|
+
(key,),
|
|
59
|
+
)
|
|
60
|
+
row = cursor.fetchone()
|
|
61
|
+
|
|
62
|
+
if row is None:
|
|
63
|
+
return NO_VALUE
|
|
64
|
+
|
|
65
|
+
value_blob, expires_at = row
|
|
66
|
+
if time.time() > expires_at:
|
|
67
|
+
conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
68
|
+
conn.commit()
|
|
69
|
+
return NO_VALUE
|
|
70
|
+
|
|
71
|
+
return pickle.loads(value_blob)
|
|
72
|
+
except Exception:
|
|
73
|
+
return NO_VALUE
|
|
74
|
+
finally:
|
|
75
|
+
conn.close()
|
|
76
|
+
|
|
77
|
+
def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
78
|
+
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
79
|
+
"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
conn = self._get_connection()
|
|
82
|
+
try:
|
|
83
|
+
cursor = conn.execute(
|
|
84
|
+
'SELECT value, created_at, expires_at FROM cache WHERE key = ?',
|
|
85
|
+
(key,),
|
|
86
|
+
)
|
|
87
|
+
row = cursor.fetchone()
|
|
88
|
+
|
|
89
|
+
if row is None:
|
|
90
|
+
return NO_VALUE, None
|
|
91
|
+
|
|
92
|
+
value_blob, created_at, expires_at = row
|
|
93
|
+
if time.time() > expires_at:
|
|
94
|
+
conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
95
|
+
conn.commit()
|
|
96
|
+
return NO_VALUE, None
|
|
97
|
+
|
|
98
|
+
return pickle.loads(value_blob), created_at
|
|
99
|
+
except Exception:
|
|
100
|
+
return NO_VALUE, None
|
|
101
|
+
finally:
|
|
102
|
+
conn.close()
|
|
103
|
+
|
|
104
|
+
def set(self, key: str, value: Any, ttl: int) -> None:
|
|
105
|
+
"""Set value with TTL in seconds.
|
|
106
|
+
"""
|
|
107
|
+
now = time.time()
|
|
108
|
+
value_blob = pickle.dumps(value)
|
|
109
|
+
|
|
110
|
+
with self._lock:
|
|
111
|
+
conn = self._get_connection()
|
|
112
|
+
try:
|
|
113
|
+
conn.execute(
|
|
114
|
+
'''INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
|
|
115
|
+
VALUES (?, ?, ?, ?)''',
|
|
116
|
+
(key, value_blob, now, now + ttl),
|
|
117
|
+
)
|
|
118
|
+
conn.commit()
|
|
119
|
+
finally:
|
|
120
|
+
conn.close()
|
|
121
|
+
|
|
122
|
+
def delete(self, key: str) -> None:
|
|
123
|
+
"""Delete value by key.
|
|
124
|
+
"""
|
|
125
|
+
with self._lock:
|
|
126
|
+
conn = self._get_connection()
|
|
127
|
+
try:
|
|
128
|
+
conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
129
|
+
conn.commit()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
finally:
|
|
133
|
+
conn.close()
|
|
134
|
+
|
|
135
|
+
def clear(self, pattern: str | None = None) -> int:
|
|
136
|
+
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
137
|
+
"""
|
|
138
|
+
with self._lock:
|
|
139
|
+
conn = self._get_connection()
|
|
140
|
+
try:
|
|
141
|
+
if pattern is None:
|
|
142
|
+
cursor = conn.execute('SELECT COUNT(*) FROM cache')
|
|
143
|
+
count = cursor.fetchone()[0]
|
|
144
|
+
conn.execute('DELETE FROM cache')
|
|
145
|
+
conn.commit()
|
|
146
|
+
return count
|
|
147
|
+
|
|
148
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
149
|
+
cursor = conn.execute(
|
|
150
|
+
'SELECT COUNT(*) FROM cache WHERE key GLOB ?',
|
|
151
|
+
(glob_pattern,),
|
|
152
|
+
)
|
|
153
|
+
count = cursor.fetchone()[0]
|
|
154
|
+
conn.execute('DELETE FROM cache WHERE key GLOB ?', (glob_pattern,))
|
|
155
|
+
conn.commit()
|
|
156
|
+
return count
|
|
157
|
+
except Exception:
|
|
158
|
+
return 0
|
|
159
|
+
finally:
|
|
160
|
+
conn.close()
|
|
161
|
+
|
|
162
|
+
def keys(self, pattern: str | None = None) -> Iterator[str]:
|
|
163
|
+
"""Iterate over keys matching pattern.
|
|
164
|
+
"""
|
|
165
|
+
now = time.time()
|
|
166
|
+
|
|
167
|
+
with self._lock:
|
|
168
|
+
conn = self._get_connection()
|
|
169
|
+
try:
|
|
170
|
+
if pattern is None:
|
|
171
|
+
cursor = conn.execute(
|
|
172
|
+
'SELECT key FROM cache WHERE expires_at > ?',
|
|
173
|
+
(now,),
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
177
|
+
cursor = conn.execute(
|
|
178
|
+
'SELECT key FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
179
|
+
(glob_pattern, now),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
all_keys = [row[0] for row in cursor.fetchall()]
|
|
183
|
+
finally:
|
|
184
|
+
conn.close()
|
|
185
|
+
|
|
186
|
+
for key in all_keys:
|
|
187
|
+
yield key
|
|
188
|
+
|
|
189
|
+
def count(self, pattern: str | None = None) -> int:
|
|
190
|
+
"""Count keys matching pattern.
|
|
191
|
+
"""
|
|
192
|
+
now = time.time()
|
|
193
|
+
|
|
194
|
+
with self._lock:
|
|
195
|
+
conn = self._get_connection()
|
|
196
|
+
try:
|
|
197
|
+
if pattern is None:
|
|
198
|
+
cursor = conn.execute(
|
|
199
|
+
'SELECT COUNT(*) FROM cache WHERE expires_at > ?',
|
|
200
|
+
(now,),
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
glob_pattern = self._fnmatch_to_glob(pattern)
|
|
204
|
+
cursor = conn.execute(
|
|
205
|
+
'SELECT COUNT(*) FROM cache WHERE key GLOB ? AND expires_at > ?',
|
|
206
|
+
(glob_pattern, now),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return cursor.fetchone()[0]
|
|
210
|
+
except Exception:
|
|
211
|
+
return 0
|
|
212
|
+
finally:
|
|
213
|
+
conn.close()
|
|
214
|
+
|
|
215
|
+
def _fnmatch_to_glob(self, pattern: str) -> str:
|
|
216
|
+
"""Convert fnmatch pattern to SQLite GLOB pattern.
|
|
217
|
+
|
|
218
|
+
fnmatch uses * and ? which are the same as SQLite GLOB.
|
|
219
|
+
The main difference is character classes [...] which we don't use.
|
|
220
|
+
"""
|
|
221
|
+
return pattern
|
|
222
|
+
|
|
223
|
+
def cleanup_expired(self) -> int:
|
|
224
|
+
"""Remove expired entries. Returns count of removed entries.
|
|
225
|
+
"""
|
|
226
|
+
now = time.time()
|
|
227
|
+
|
|
228
|
+
with self._lock:
|
|
229
|
+
conn = self._get_connection()
|
|
230
|
+
try:
|
|
231
|
+
cursor = conn.execute(
|
|
232
|
+
'SELECT COUNT(*) FROM cache WHERE expires_at <= ?',
|
|
233
|
+
(now,),
|
|
234
|
+
)
|
|
235
|
+
count = cursor.fetchone()[0]
|
|
236
|
+
conn.execute('DELETE FROM cache WHERE expires_at <= ?', (now,))
|
|
237
|
+
conn.commit()
|
|
238
|
+
return count
|
|
239
|
+
finally:
|
|
240
|
+
conn.close()
|
cachu/decorator.py
CHANGED
|
@@ -39,11 +39,11 @@ def _get_backend(package: str | None, backend_type: str, ttl: int) -> Backend:
|
|
|
39
39
|
backend = MemoryBackend()
|
|
40
40
|
elif backend_type == 'file':
|
|
41
41
|
if ttl < 60:
|
|
42
|
-
filename = f'cache{ttl}sec'
|
|
42
|
+
filename = f'cache{ttl}sec.db'
|
|
43
43
|
elif ttl < 3600:
|
|
44
|
-
filename = f'cache{ttl // 60}min'
|
|
44
|
+
filename = f'cache{ttl // 60}min.db'
|
|
45
45
|
else:
|
|
46
|
-
filename = f'cache{ttl // 3600}hour'
|
|
46
|
+
filename = f'cache{ttl // 3600}hour.db'
|
|
47
47
|
|
|
48
48
|
if package:
|
|
49
49
|
filename = f'{package}_{filename}'
|
|
@@ -220,12 +220,7 @@ def get_cache_info(fn: Callable[..., Any]) -> CacheInfo:
|
|
|
220
220
|
Returns
|
|
221
221
|
CacheInfo with hits, misses, and currsize
|
|
222
222
|
"""
|
|
223
|
-
|
|
224
|
-
actual_fn = fn
|
|
225
|
-
else:
|
|
226
|
-
actual_fn = fn
|
|
227
|
-
|
|
228
|
-
fn_id = id(actual_fn)
|
|
223
|
+
fn_id = id(fn)
|
|
229
224
|
|
|
230
225
|
with _stats_lock:
|
|
231
226
|
hits, misses = _stats.get(fn_id, (0, 0))
|
cachu/operations.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Cache CRUD operations.
|
|
2
2
|
"""
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any
|
|
5
4
|
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from .backends import NO_VALUE
|
|
8
8
|
from .config import _get_caller_package, get_config
|
|
@@ -124,8 +124,6 @@ def cache_clear(
|
|
|
124
124
|
if package is None:
|
|
125
125
|
package = _get_caller_package()
|
|
126
126
|
|
|
127
|
-
cfg = get_config(package)
|
|
128
|
-
|
|
129
127
|
if backend is not None:
|
|
130
128
|
backends_to_clear = [backend]
|
|
131
129
|
else:
|
|
@@ -141,19 +139,29 @@ def cache_clear(
|
|
|
141
139
|
|
|
142
140
|
from .decorator import _backends, _backends_lock
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
# When both backend and ttl are specified, directly get/create and clear that backend.
|
|
143
|
+
# This is essential for distributed caches (Redis) where cache_clear may be called
|
|
144
|
+
# from a different process than the one that populated the cache.
|
|
145
|
+
if backend is not None and ttl is not None:
|
|
146
|
+
backend_instance = _get_backend(package, backend, ttl)
|
|
147
|
+
cleared = backend_instance.clear(pattern)
|
|
148
|
+
if cleared > 0:
|
|
149
|
+
total_cleared += cleared
|
|
150
|
+
logger.debug(f'Cleared {cleared} entries from {backend} backend (ttl={ttl})')
|
|
151
|
+
else:
|
|
152
|
+
with _backends_lock:
|
|
153
|
+
for (pkg, btype, bttl), backend_instance in list(_backends.items()):
|
|
154
|
+
if pkg != package:
|
|
155
|
+
continue
|
|
156
|
+
if btype not in backends_to_clear:
|
|
157
|
+
continue
|
|
158
|
+
if ttl is not None and bttl != ttl:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
cleared = backend_instance.clear(pattern)
|
|
162
|
+
if cleared > 0:
|
|
163
|
+
total_cleared += cleared
|
|
164
|
+
logger.debug(f'Cleared {cleared} entries from {btype} backend (ttl={bttl})')
|
|
157
165
|
|
|
158
166
|
return total_cleared
|
|
159
167
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachu
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Flexible caching library
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
7
7
|
Project-URL: Repository, https://github.com/bissli/cachu.git
|
|
@@ -9,13 +9,17 @@ Requires-Python: >=3.10
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Requires-Dist: dogpile.cache
|
|
11
11
|
Requires-Dist: func-timeout
|
|
12
|
+
Provides-Extra: async
|
|
13
|
+
Requires-Dist: aiosqlite; extra == "async"
|
|
12
14
|
Provides-Extra: redis
|
|
13
|
-
Requires-Dist: redis; extra == "redis"
|
|
15
|
+
Requires-Dist: redis>=4.2.0; extra == "redis"
|
|
14
16
|
Provides-Extra: test
|
|
15
17
|
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
16
19
|
Requires-Dist: pytest-mock; extra == "test"
|
|
17
|
-
Requires-Dist: redis; extra == "test"
|
|
20
|
+
Requires-Dist: redis>=4.2.0; extra == "test"
|
|
18
21
|
Requires-Dist: testcontainers[redis]; extra == "test"
|
|
22
|
+
Requires-Dist: aiosqlite; extra == "test"
|
|
19
23
|
|
|
20
24
|
# cachu
|
|
21
25
|
|
|
@@ -71,17 +75,17 @@ cachu.configure(
|
|
|
71
75
|
|
|
72
76
|
### Configuration Options
|
|
73
77
|
|
|
74
|
-
| Option
|
|
75
|
-
|
|
76
|
-
| `backend`
|
|
77
|
-
| `key_prefix`
|
|
78
|
-
| `file_dir`
|
|
79
|
-
| `redis_url`
|
|
80
|
-
| `redis_distributed` | `False`
|
|
78
|
+
| Option | Default | Description |
|
|
79
|
+
| ------------------- | ---------------------------- | ------------------------------------------------- |
|
|
80
|
+
| `backend` | `'memory'` | Default backend type |
|
|
81
|
+
| `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
|
|
82
|
+
| `file_dir` | `'/tmp'` | Directory for file-based caches |
|
|
83
|
+
| `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
|
|
84
|
+
| `redis_distributed` | `False` | Enable distributed locks for Redis |
|
|
81
85
|
|
|
82
86
|
### Package Isolation
|
|
83
87
|
|
|
84
|
-
Each package automatically gets isolated configuration
|
|
88
|
+
Each package automatically gets isolated configuration, preventing conflicts when multiple libraries use cachu:
|
|
85
89
|
|
|
86
90
|
```python
|
|
87
91
|
# In library_a/config.py
|
|
@@ -92,7 +96,18 @@ cachu.configure(key_prefix='lib_a:', redis_url='redis://redis-a:6379/0')
|
|
|
92
96
|
import cachu
|
|
93
97
|
cachu.configure(key_prefix='lib_b:', redis_url='redis://redis-b:6379/0')
|
|
94
98
|
|
|
95
|
-
# Each library
|
|
99
|
+
# Each library's @cache calls use its own configuration automatically
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To override the automatic detection, specify the `package` parameter:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from cachu import cache
|
|
106
|
+
|
|
107
|
+
# This function will use library_a's configuration
|
|
108
|
+
@cache(ttl=300, package='library_a')
|
|
109
|
+
def get_shared_data(id: int) -> dict:
|
|
110
|
+
return fetch(id)
|
|
96
111
|
```
|
|
97
112
|
|
|
98
113
|
Retrieve configuration:
|
|
@@ -289,12 +304,12 @@ cache_clear()
|
|
|
289
304
|
|
|
290
305
|
**Clearing behavior:**
|
|
291
306
|
|
|
292
|
-
| `ttl`
|
|
293
|
-
|
|
294
|
-
| `300`
|
|
295
|
-
| `300`
|
|
296
|
-
| `None` | `None`
|
|
297
|
-
| `None` | `'users'` | `None`
|
|
307
|
+
| `ttl` | `tag` | `backend` | Behavior |
|
|
308
|
+
| ------ | --------- | ---------- | -------------------------------------- |
|
|
309
|
+
| `300` | `None` | `'memory'` | All keys in 300s memory region |
|
|
310
|
+
| `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
|
|
311
|
+
| `None` | `None` | `'memory'` | All memory regions |
|
|
312
|
+
| `None` | `'users'` | `None` | "users" tag across all backends |
|
|
298
313
|
|
|
299
314
|
### Cross-Module Clearing
|
|
300
315
|
|
|
@@ -352,6 +367,38 @@ if cachu.is_disabled():
|
|
|
352
367
|
print("Caching is disabled")
|
|
353
368
|
```
|
|
354
369
|
|
|
370
|
+
## Async Support
|
|
371
|
+
|
|
372
|
+
The library provides full async/await support with matching APIs:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
from cachu import async_cache, async_cache_get, async_cache_set, async_cache_delete
|
|
376
|
+
from cachu import async_cache_clear, async_cache_info
|
|
377
|
+
|
|
378
|
+
@async_cache(ttl=300, backend='memory')
|
|
379
|
+
async def get_user(user_id: int) -> dict:
|
|
380
|
+
return await fetch_from_database(user_id)
|
|
381
|
+
|
|
382
|
+
# Usage
|
|
383
|
+
user = await get_user(123) # Cache miss
|
|
384
|
+
user = await get_user(123) # Cache hit
|
|
385
|
+
|
|
386
|
+
# Per-call control works the same way
|
|
387
|
+
user = await get_user(123, _skip_cache=True)
|
|
388
|
+
user = await get_user(123, _overwrite_cache=True)
|
|
389
|
+
|
|
390
|
+
# CRUD operations
|
|
391
|
+
cached = await async_cache_get(get_user, user_id=123)
|
|
392
|
+
await async_cache_set(get_user, {'id': 123, 'name': 'Test'}, user_id=123)
|
|
393
|
+
await async_cache_delete(get_user, user_id=123)
|
|
394
|
+
await async_cache_clear(backend='memory', ttl=300)
|
|
395
|
+
|
|
396
|
+
# Statistics
|
|
397
|
+
info = await async_cache_info(get_user)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
All decorator options (`ttl`, `backend`, `tag`, `exclude`, `cache_if`, `validate`, `package`) work identically to the sync version.
|
|
401
|
+
|
|
355
402
|
## Advanced
|
|
356
403
|
|
|
357
404
|
### Direct Backend Access
|
|
@@ -386,25 +433,40 @@ from cachu import (
|
|
|
386
433
|
enable,
|
|
387
434
|
is_disabled,
|
|
388
435
|
|
|
389
|
-
# Decorator
|
|
436
|
+
# Sync Decorator
|
|
390
437
|
cache,
|
|
391
438
|
|
|
392
|
-
# CRUD Operations
|
|
439
|
+
# Sync CRUD Operations
|
|
393
440
|
cache_get,
|
|
394
441
|
cache_set,
|
|
395
442
|
cache_delete,
|
|
396
443
|
cache_clear,
|
|
397
444
|
cache_info,
|
|
398
445
|
|
|
446
|
+
# Async Decorator
|
|
447
|
+
async_cache,
|
|
448
|
+
|
|
449
|
+
# Async CRUD Operations
|
|
450
|
+
async_cache_get,
|
|
451
|
+
async_cache_set,
|
|
452
|
+
async_cache_delete,
|
|
453
|
+
async_cache_clear,
|
|
454
|
+
async_cache_info,
|
|
455
|
+
|
|
399
456
|
# Advanced
|
|
400
457
|
get_backend,
|
|
458
|
+
get_async_backend,
|
|
401
459
|
get_redis_client,
|
|
460
|
+
Backend,
|
|
461
|
+
AsyncBackend,
|
|
462
|
+
clear_async_backends,
|
|
402
463
|
)
|
|
403
464
|
```
|
|
404
465
|
|
|
405
466
|
## Features
|
|
406
467
|
|
|
407
|
-
- **Multiple backends**: Memory, file (
|
|
468
|
+
- **Multiple backends**: Memory, file (SQLite), and Redis
|
|
469
|
+
- **Async support**: Full async/await API with `@async_cache` decorator
|
|
408
470
|
- **Flexible TTL**: Configure different TTLs for different use cases
|
|
409
471
|
- **Tags**: Organize and selectively clear cache entries
|
|
410
472
|
- **Package isolation**: Each package gets isolated configuration
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
cachu/__init__.py,sha256=w2QlqHCWnPbnj_JL2zcW3Maa03PYuWU9IR3GABNiq8A,1286
|
|
2
|
+
cachu/async_decorator.py,sha256=NSCOrgKPRyqQz-VaN1iq3fqjwWuJHDFS-15Dw3aFRP8,8938
|
|
3
|
+
cachu/async_operations.py,sha256=eVqhZk3FVLNip_abjnCzG8AajzvJTtXbpL--dpMXBlc,5597
|
|
4
|
+
cachu/cache.py,sha256=UOh1hsvo5wqpf-quU0glGZi5bgjF8gkaSmYIkAwWfUA,23362
|
|
5
|
+
cachu/config.py,sha256=KtcDGpSTJmjRrcNLz9_Om3O814oJJ3p8gntB84Pd6Dk,5922
|
|
6
|
+
cachu/decorator.py,sha256=aNI7tSr74BeZAGa3Wti0I3M8MVtuFa5HSuvfL4jLqs4,8164
|
|
7
|
+
cachu/keys.py,sha256=fwwNOpnDJFCIWZoQ5UGJWhJa6xu36hsBsURI-n2NJKU,3557
|
|
8
|
+
cachu/operations.py,sha256=t42_Er-O59vrwFa5jdf4yq3Jr4li2l7php4yMVJnxPs,5588
|
|
9
|
+
cachu/types.py,sha256=FghBN5GhxnrpuT4WUL9iNnAfdoH__cw9_Ag4kHbIXq4,723
|
|
10
|
+
cachu/backends/__init__.py,sha256=Jn2yBAMmJ8d0J_NyjOtxRt7UTyMLf1rlY8QJ049hXE8,1318
|
|
11
|
+
cachu/backends/async_base.py,sha256=oZ3K3PhsYkbgZxFLFk3_NbxBxtNopqS90HZBizwg_q8,1394
|
|
12
|
+
cachu/backends/async_memory.py,sha256=SQvSHeWbySa52BnQLF75nhVXgsydubNu84a8hvSzQSc,3457
|
|
13
|
+
cachu/backends/async_redis.py,sha256=8kefPIoIJDAZ6C6HJCvHqKFMDS10sJYh8YcJMpXpQm8,4455
|
|
14
|
+
cachu/backends/async_sqlite.py,sha256=r-c1cNVl6JEApMGhw8Qw7843Vuj_LVRAM-MGgoIjah0,8423
|
|
15
|
+
cachu/backends/file.py,sha256=Pu01VtgHDgK6ev5hqyZXuJRCSB2VbNKHQ4w4nNKNyeI,298
|
|
16
|
+
cachu/backends/memory.py,sha256=kIgrVU8k_3Aquyj2PDf8IPbTjCITM_0V5GU47m3fJmo,3138
|
|
17
|
+
cachu/backends/redis.py,sha256=yE5rEBgOij9QOeC1VhWdIbGCgi442q-aWfmbbG4aNSE,3858
|
|
18
|
+
cachu/backends/sqlite.py,sha256=whduN5G_bN6ZJNuCBwbraDcadv_sg0j-OEiFnP8EEsk,7803
|
|
19
|
+
cachu-0.2.0.dist-info/METADATA,sha256=NTgzp45Dx6tQfD_xS_evTkYFAiiuKkSfinOsGGpocOI,11992
|
|
20
|
+
cachu-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
cachu-0.2.0.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
|
|
22
|
+
cachu-0.2.0.dist-info/RECORD,,
|
cachu-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
cachu/__init__.py,sha256=_-9rrpnkuhyTABEyh7lBRMZNb60AXzm3ZdHjBdaQDKc,676
|
|
2
|
-
cachu/cache.py,sha256=UOh1hsvo5wqpf-quU0glGZi5bgjF8gkaSmYIkAwWfUA,23362
|
|
3
|
-
cachu/config.py,sha256=KtcDGpSTJmjRrcNLz9_Om3O814oJJ3p8gntB84Pd6Dk,5922
|
|
4
|
-
cachu/decorator.py,sha256=FqD-On66WYYIAOWIe-umlILFc8XcuE8IQHTV6MnFA9o,8254
|
|
5
|
-
cachu/keys.py,sha256=fwwNOpnDJFCIWZoQ5UGJWhJa6xu36hsBsURI-n2NJKU,3557
|
|
6
|
-
cachu/operations.py,sha256=_hHFo9mBgsGT_45-08rprddxNhnGAezCFbQAR_CgI80,5001
|
|
7
|
-
cachu/types.py,sha256=FghBN5GhxnrpuT4WUL9iNnAfdoH__cw9_Ag4kHbIXq4,723
|
|
8
|
-
cachu/backends/__init__.py,sha256=dM6NfSRXMCOeTg9A9-scgiT_6r_BfzbmT1GVNqL6egU,1228
|
|
9
|
-
cachu/backends/file.py,sha256=2ssQmqvpLRDLX21joXRZKxfHekjAjb5gd_gHt52SgVA,5313
|
|
10
|
-
cachu/backends/memory.py,sha256=kIgrVU8k_3Aquyj2PDf8IPbTjCITM_0V5GU47m3fJmo,3138
|
|
11
|
-
cachu/backends/redis.py,sha256=yE5rEBgOij9QOeC1VhWdIbGCgi442q-aWfmbbG4aNSE,3858
|
|
12
|
-
cachu-0.1.2.dist-info/METADATA,sha256=nRI56YIazr3xLkbdGpg9Zq7g46blUpJvYvzBvMGUNN8,9676
|
|
13
|
-
cachu-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
cachu-0.1.2.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
|
|
15
|
-
cachu-0.1.2.dist-info/RECORD,,
|
|
File without changes
|