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.
@@ -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
- if hasattr(fn, '__wrapped__'):
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
- with _backends_lock:
145
- for (pkg, btype, bttl), backend_instance in list(_backends.items()):
146
- if pkg != package:
147
- continue
148
- if btype not in backends_to_clear:
149
- continue
150
- if ttl is not None and bttl != ttl:
151
- continue
152
-
153
- cleared = backend_instance.clear(pattern)
154
- if cleared > 0:
155
- total_cleared += cleared
156
- logger.debug(f'Cleared {cleared} entries from {btype} backend (ttl={bttl})')
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.1.2
4
- Summary: Flexible caching library built on dogpile.cache
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 | Default | Description |
75
- |--------|---------|-------------|
76
- | `backend` | `'memory'` | Default backend type |
77
- | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
78
- | `file_dir` | `'/tmp'` | Directory for file-based caches |
79
- | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
80
- | `redis_distributed` | `False` | Enable distributed locks for Redis |
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. This prevents conflicts when multiple libraries use the cachu package:
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 uses its own configuration automatically
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` | `tag` | `backend` | Behavior |
293
- |-------|-------|-----------|----------|
294
- | `300` | `None` | `'memory'` | All keys in 300s memory region |
295
- | `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
296
- | `None` | `None` | `'memory'` | All memory regions |
297
- | `None` | `'users'` | `None` | "users" tag across all backends |
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 (DBM), and Redis
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,