cachu 0.1.3__py3-none-any.whl → 0.2.1__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 +262 -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 +5 -5
- {cachu-0.1.3.dist-info → cachu-0.2.1.dist-info}/METADATA +71 -20
- cachu-0.2.1.dist-info/RECORD +21 -0
- {cachu-0.1.3.dist-info → cachu-0.2.1.dist-info}/WHEEL +1 -1
- cachu/cache.py +0 -636
- cachu-0.1.3.dist-info/RECORD +0 -15
- {cachu-0.1.3.dist-info → cachu-0.2.1.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
|
@@ -4,9 +4,9 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from functools import wraps
|
|
8
9
|
from typing import Any
|
|
9
|
-
from collections.abc import Callable
|
|
10
10
|
|
|
11
11
|
from .backends import NO_VALUE, Backend
|
|
12
12
|
from .backends.file import FileBackend
|
|
@@ -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}'
|
|
@@ -61,7 +61,7 @@ def _get_backend(package: str | None, backend_type: str, ttl: int) -> Backend:
|
|
|
61
61
|
return backend
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def get_backend(backend_type: str | None = None, package: str | None = None, ttl: int
|
|
64
|
+
def get_backend(backend_type: str | None = None, package: str | None = None, *, ttl: int) -> Backend:
|
|
65
65
|
"""Get a backend instance.
|
|
66
66
|
|
|
67
67
|
Args:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachu
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: Flexible caching library
|
|
3
|
+
Version: 0.2.1
|
|
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,13 +75,13 @@ 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
|
|
|
@@ -300,12 +304,12 @@ cache_clear()
|
|
|
300
304
|
|
|
301
305
|
**Clearing behavior:**
|
|
302
306
|
|
|
303
|
-
| `ttl`
|
|
304
|
-
|
|
305
|
-
| `300`
|
|
306
|
-
| `300`
|
|
307
|
-
| `None` | `None`
|
|
308
|
-
| `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 |
|
|
309
313
|
|
|
310
314
|
### Cross-Module Clearing
|
|
311
315
|
|
|
@@ -363,6 +367,38 @@ if cachu.is_disabled():
|
|
|
363
367
|
print("Caching is disabled")
|
|
364
368
|
```
|
|
365
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
|
+
|
|
366
402
|
## Advanced
|
|
367
403
|
|
|
368
404
|
### Direct Backend Access
|
|
@@ -397,25 +433,40 @@ from cachu import (
|
|
|
397
433
|
enable,
|
|
398
434
|
is_disabled,
|
|
399
435
|
|
|
400
|
-
# Decorator
|
|
436
|
+
# Sync Decorator
|
|
401
437
|
cache,
|
|
402
438
|
|
|
403
|
-
# CRUD Operations
|
|
439
|
+
# Sync CRUD Operations
|
|
404
440
|
cache_get,
|
|
405
441
|
cache_set,
|
|
406
442
|
cache_delete,
|
|
407
443
|
cache_clear,
|
|
408
444
|
cache_info,
|
|
409
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
|
+
|
|
410
456
|
# Advanced
|
|
411
457
|
get_backend,
|
|
458
|
+
get_async_backend,
|
|
412
459
|
get_redis_client,
|
|
460
|
+
Backend,
|
|
461
|
+
AsyncBackend,
|
|
462
|
+
clear_async_backends,
|
|
413
463
|
)
|
|
414
464
|
```
|
|
415
465
|
|
|
416
466
|
## Features
|
|
417
467
|
|
|
418
|
-
- **Multiple backends**: Memory, file (
|
|
468
|
+
- **Multiple backends**: Memory, file (SQLite), and Redis
|
|
469
|
+
- **Async support**: Full async/await API with `@async_cache` decorator
|
|
419
470
|
- **Flexible TTL**: Configure different TTLs for different use cases
|
|
420
471
|
- **Tags**: Organize and selectively clear cache entries
|
|
421
472
|
- **Package isolation**: Each package gets isolated configuration
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cachu/__init__.py,sha256=-eqMY3cCuepixdZ-FefQsRXPufVSgrEMlYkYwylSlTM,1286
|
|
2
|
+
cachu/async_decorator.py,sha256=Jx2fHESLlld7NZiD2-6kcozukJtp5efnt4cMhntDDRA,8939
|
|
3
|
+
cachu/async_operations.py,sha256=eVqhZk3FVLNip_abjnCzG8AajzvJTtXbpL--dpMXBlc,5597
|
|
4
|
+
cachu/config.py,sha256=KtcDGpSTJmjRrcNLz9_Om3O814oJJ3p8gntB84Pd6Dk,5922
|
|
5
|
+
cachu/decorator.py,sha256=RHwDRZxZfOkBgEK1XgRyis22bxQ0ba0X4NtHBd9FTb4,8161
|
|
6
|
+
cachu/keys.py,sha256=fwwNOpnDJFCIWZoQ5UGJWhJa6xu36hsBsURI-n2NJKU,3557
|
|
7
|
+
cachu/operations.py,sha256=t42_Er-O59vrwFa5jdf4yq3Jr4li2l7php4yMVJnxPs,5588
|
|
8
|
+
cachu/types.py,sha256=FghBN5GhxnrpuT4WUL9iNnAfdoH__cw9_Ag4kHbIXq4,723
|
|
9
|
+
cachu/backends/__init__.py,sha256=Jn2yBAMmJ8d0J_NyjOtxRt7UTyMLf1rlY8QJ049hXE8,1318
|
|
10
|
+
cachu/backends/async_base.py,sha256=oZ3K3PhsYkbgZxFLFk3_NbxBxtNopqS90HZBizwg_q8,1394
|
|
11
|
+
cachu/backends/async_memory.py,sha256=SQvSHeWbySa52BnQLF75nhVXgsydubNu84a8hvSzQSc,3457
|
|
12
|
+
cachu/backends/async_redis.py,sha256=8kefPIoIJDAZ6C6HJCvHqKFMDS10sJYh8YcJMpXpQm8,4455
|
|
13
|
+
cachu/backends/async_sqlite.py,sha256=r-c1cNVl6JEApMGhw8Qw7843Vuj_LVRAM-MGgoIjah0,8423
|
|
14
|
+
cachu/backends/file.py,sha256=Pu01VtgHDgK6ev5hqyZXuJRCSB2VbNKHQ4w4nNKNyeI,298
|
|
15
|
+
cachu/backends/memory.py,sha256=kIgrVU8k_3Aquyj2PDf8IPbTjCITM_0V5GU47m3fJmo,3138
|
|
16
|
+
cachu/backends/redis.py,sha256=yE5rEBgOij9QOeC1VhWdIbGCgi442q-aWfmbbG4aNSE,3858
|
|
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,,
|