cachu 0.1.1__tar.gz
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-0.1.1/PKG-INFO +410 -0
- cachu-0.1.1/README.md +392 -0
- cachu-0.1.1/pyproject.toml +40 -0
- cachu-0.1.1/setup.cfg +17 -0
- cachu-0.1.1/src/cachu/__init__.py +27 -0
- cachu-0.1.1/src/cachu/backends/__init__.py +47 -0
- cachu-0.1.1/src/cachu/backends/file.py +158 -0
- cachu-0.1.1/src/cachu/backends/memory.py +102 -0
- cachu-0.1.1/src/cachu/backends/redis.py +131 -0
- cachu-0.1.1/src/cachu/cache.py +636 -0
- cachu-0.1.1/src/cachu/config.py +193 -0
- cachu-0.1.1/src/cachu/decorator.py +257 -0
- cachu-0.1.1/src/cachu/keys.py +122 -0
- cachu-0.1.1/src/cachu/operations.py +174 -0
- cachu-0.1.1/src/cachu/types.py +37 -0
- cachu-0.1.1/src/cachu.egg-info/PKG-INFO +410 -0
- cachu-0.1.1/src/cachu.egg-info/SOURCES.txt +32 -0
- cachu-0.1.1/src/cachu.egg-info/dependency_links.txt +1 -0
- cachu-0.1.1/src/cachu.egg-info/requires.txt +10 -0
- cachu-0.1.1/src/cachu.egg-info/top_level.txt +1 -0
- cachu-0.1.1/tests/test_clearing.py +113 -0
- cachu-0.1.1/tests/test_config.py +58 -0
- cachu-0.1.1/tests/test_defaultcache.py +128 -0
- cachu-0.1.1/tests/test_delete_keys.py +156 -0
- cachu-0.1.1/tests/test_disable.py +79 -0
- cachu-0.1.1/tests/test_exclude_params.py +274 -0
- cachu-0.1.1/tests/test_file_cache.py +86 -0
- cachu-0.1.1/tests/test_integration.py +287 -0
- cachu-0.1.1/tests/test_memory_cache.py +148 -0
- cachu-0.1.1/tests/test_namespace.py +124 -0
- cachu-0.1.1/tests/test_namespace_isolation.py +138 -0
- cachu-0.1.1/tests/test_redis_cache.py +89 -0
- cachu-0.1.1/tests/test_set_keys.py +130 -0
cachu-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cachu
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Flexible caching library built on dogpile.cache
|
|
5
|
+
Author: bissli
|
|
6
|
+
License-Expression: 0BSD
|
|
7
|
+
Project-URL: Repository, https://github.com/bissli/cachu.git
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: dogpile.cache
|
|
11
|
+
Requires-Dist: func-timeout
|
|
12
|
+
Provides-Extra: redis
|
|
13
|
+
Requires-Dist: redis; extra == "redis"
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest; extra == "test"
|
|
16
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
17
|
+
Requires-Dist: testcontainers[redis]; extra == "test"
|
|
18
|
+
|
|
19
|
+
# cachu
|
|
20
|
+
|
|
21
|
+
Flexible caching library with support for memory, file, and Redis backends.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
**Basic installation:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install git+https://github.com/bissli/cachu.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**With Redis support:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git+https://github.com/bissli/cachu.git#egg=cachu[redis]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import cachu
|
|
41
|
+
|
|
42
|
+
# Configure once at startup
|
|
43
|
+
cachu.configure(backend='memory', key_prefix='v1:')
|
|
44
|
+
|
|
45
|
+
# Use the @cache decorator
|
|
46
|
+
@cachu.cache(ttl=300)
|
|
47
|
+
def get_user(user_id: int) -> dict:
|
|
48
|
+
return fetch_from_database(user_id)
|
|
49
|
+
|
|
50
|
+
# Cached automatically
|
|
51
|
+
user = get_user(123) # Cache miss - fetches from DB
|
|
52
|
+
user = get_user(123) # Cache hit - returns cached value
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Configure cache settings at application startup:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import cachu
|
|
61
|
+
|
|
62
|
+
cachu.configure(
|
|
63
|
+
backend='memory', # Default backend: 'memory', 'file', or 'redis'
|
|
64
|
+
key_prefix='v1:', # Prefix for all cache keys
|
|
65
|
+
file_dir='/var/cache/app', # Directory for file cache
|
|
66
|
+
redis_url='redis://localhost:6379/0', # Redis connection URL
|
|
67
|
+
redis_distributed=False, # Use distributed locks for Redis
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Configuration Options
|
|
72
|
+
|
|
73
|
+
| Option | Default | Description |
|
|
74
|
+
|--------|---------|-------------|
|
|
75
|
+
| `backend` | `'memory'` | Default backend type |
|
|
76
|
+
| `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
|
|
77
|
+
| `file_dir` | `'/tmp'` | Directory for file-based caches |
|
|
78
|
+
| `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
|
|
79
|
+
| `redis_distributed` | `False` | Enable distributed locks for Redis |
|
|
80
|
+
|
|
81
|
+
### Package Isolation
|
|
82
|
+
|
|
83
|
+
Each package automatically gets isolated configuration. This prevents conflicts when multiple libraries use the cachu package:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# In library_a/config.py
|
|
87
|
+
import cachu
|
|
88
|
+
cachu.configure(key_prefix='lib_a:', redis_url='redis://redis-a:6379/0')
|
|
89
|
+
|
|
90
|
+
# In library_b/config.py
|
|
91
|
+
import cachu
|
|
92
|
+
cachu.configure(key_prefix='lib_b:', redis_url='redis://redis-b:6379/0')
|
|
93
|
+
|
|
94
|
+
# Each library uses its own configuration automatically
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Retrieve configuration:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
cfg = cachu.get_config() # Current package's config
|
|
101
|
+
cfg = cachu.get_config(package='mylib') # Specific package's config
|
|
102
|
+
all_configs = cachu.get_all_configs() # All configurations
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
### Basic Caching
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from cachu import cachu
|
|
111
|
+
|
|
112
|
+
@cache(ttl=300, backend='memory')
|
|
113
|
+
def expensive_operation(param: str) -> dict:
|
|
114
|
+
return compute_result(param)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Backend Types
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Memory cache (default)
|
|
121
|
+
@cache(ttl=300, backend='memory')
|
|
122
|
+
def fast_lookup(key: str) -> str:
|
|
123
|
+
return fetch(key)
|
|
124
|
+
|
|
125
|
+
# File cache (persists across restarts)
|
|
126
|
+
@cache(ttl=3600, backend='file')
|
|
127
|
+
def load_config(name: str) -> dict:
|
|
128
|
+
return parse_config_file(name)
|
|
129
|
+
|
|
130
|
+
# Redis cache (shared across processes)
|
|
131
|
+
@cache(ttl=86400, backend='redis')
|
|
132
|
+
def fetch_external_data(api_key: str) -> dict:
|
|
133
|
+
return call_external_api(api_key)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Tags for Grouping
|
|
137
|
+
|
|
138
|
+
Tags organize cache entries into logical groups for selective clearing:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
@cache(ttl=300, tag='users')
|
|
142
|
+
def get_user(user_id: int) -> dict:
|
|
143
|
+
return fetch_user(user_id)
|
|
144
|
+
|
|
145
|
+
@cache(ttl=300, tag='products')
|
|
146
|
+
def get_product(product_id: int) -> dict:
|
|
147
|
+
return fetch_product(product_id)
|
|
148
|
+
|
|
149
|
+
# Clear only user caches
|
|
150
|
+
cachu.cache_clear(tag='users', backend='memory', ttl=300)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Conditional Caching
|
|
154
|
+
|
|
155
|
+
Cache results only when a condition is met:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Don't cache None results
|
|
159
|
+
@cache(ttl=300, cache_if=lambda result: result is not None)
|
|
160
|
+
def find_user(email: str) -> dict | None:
|
|
161
|
+
return db.find_by_email(email)
|
|
162
|
+
|
|
163
|
+
# Don't cache empty lists
|
|
164
|
+
@cache(ttl=300, cache_if=lambda result: len(result) > 0)
|
|
165
|
+
def search(query: str) -> list:
|
|
166
|
+
return db.search(query)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Validation Callbacks
|
|
170
|
+
|
|
171
|
+
Validate cached entries before returning:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
@cache(ttl=3600, validate=lambda entry: entry.age < 1800)
|
|
175
|
+
def get_price(symbol: str) -> float:
|
|
176
|
+
# TTL is 1 hour, but recompute after 30 minutes
|
|
177
|
+
return fetch_live_price(symbol)
|
|
178
|
+
|
|
179
|
+
# Validate based on value
|
|
180
|
+
def check_version(entry):
|
|
181
|
+
return entry.value.get('version') == CURRENT_VERSION
|
|
182
|
+
|
|
183
|
+
@cache(ttl=86400, validate=check_version)
|
|
184
|
+
def get_config() -> dict:
|
|
185
|
+
return load_config()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The `entry` parameter is a `CacheEntry` with:
|
|
189
|
+
- `value`: The cached value
|
|
190
|
+
- `created_at`: Unix timestamp when cached
|
|
191
|
+
- `age`: Seconds since creation
|
|
192
|
+
|
|
193
|
+
### Per-Call Control
|
|
194
|
+
|
|
195
|
+
Control caching behavior for individual calls:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
@cache(ttl=300)
|
|
199
|
+
def get_data(id: int) -> dict:
|
|
200
|
+
return fetch(id)
|
|
201
|
+
|
|
202
|
+
# Normal call - uses cache
|
|
203
|
+
result = get_data(123)
|
|
204
|
+
|
|
205
|
+
# Skip cache for this call only (don't read or write cache)
|
|
206
|
+
result = get_data(123, _skip_cache=True)
|
|
207
|
+
|
|
208
|
+
# Force refresh - execute and overwrite cached value
|
|
209
|
+
result = get_data(123, _overwrite_cache=True)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Cache Statistics
|
|
213
|
+
|
|
214
|
+
Track hits and misses:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
@cache(ttl=300)
|
|
218
|
+
def get_user(user_id: int) -> dict:
|
|
219
|
+
return fetch_user(user_id)
|
|
220
|
+
|
|
221
|
+
# After some usage
|
|
222
|
+
info = cachu.cache_info(get_user)
|
|
223
|
+
print(f"Hits: {info.hits}, Misses: {info.misses}, Size: {info.currsize}")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Excluding Parameters
|
|
227
|
+
|
|
228
|
+
Exclude parameters from the cache key:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
@cache(ttl=300, exclude={'logger', 'context'})
|
|
232
|
+
def process_data(logger, context, user_id: int, data: str) -> dict:
|
|
233
|
+
logger.info(f"Processing for user {user_id}")
|
|
234
|
+
return compute(data)
|
|
235
|
+
|
|
236
|
+
# Different logger/context values use the same cache entry
|
|
237
|
+
process_data(logger1, ctx1, 123, 'test') # Cache miss
|
|
238
|
+
process_data(logger2, ctx2, 123, 'test') # Cache hit
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Automatic filtering**: The library automatically excludes:
|
|
242
|
+
- `self` and `cls` parameters
|
|
243
|
+
- Parameters starting with underscore (`_`)
|
|
244
|
+
- Database connection objects
|
|
245
|
+
|
|
246
|
+
## CRUD Operations
|
|
247
|
+
|
|
248
|
+
### Direct Cache Manipulation
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from cachu import cache_get, cache_set, cache_delete, cache_clear
|
|
252
|
+
|
|
253
|
+
@cache(ttl=300, tag='users')
|
|
254
|
+
def get_user(user_id: int) -> dict:
|
|
255
|
+
return fetch_user(user_id)
|
|
256
|
+
|
|
257
|
+
# Get cached value without calling function
|
|
258
|
+
user = cache_get(get_user, user_id=123, default=None)
|
|
259
|
+
|
|
260
|
+
# Set cache value directly
|
|
261
|
+
cache_set(get_user, {'id': 123, 'name': 'Updated'}, user_id=123)
|
|
262
|
+
|
|
263
|
+
# Delete specific cache entry
|
|
264
|
+
cache_delete(get_user, user_id=123)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Clearing Caches
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from cachu import cachu_clear
|
|
271
|
+
|
|
272
|
+
# Clear specific region
|
|
273
|
+
cache_clear(backend='memory', ttl=300)
|
|
274
|
+
|
|
275
|
+
# Clear by tag
|
|
276
|
+
cache_clear(tag='users', backend='memory', ttl=300)
|
|
277
|
+
|
|
278
|
+
# Clear all TTLs for a backend
|
|
279
|
+
cache_clear(backend='memory')
|
|
280
|
+
|
|
281
|
+
# Clear everything
|
|
282
|
+
cache_clear()
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Clearing behavior:**
|
|
286
|
+
|
|
287
|
+
| `ttl` | `tag` | `backend` | Behavior |
|
|
288
|
+
|-------|-------|-----------|----------|
|
|
289
|
+
| `300` | `None` | `'memory'` | All keys in 300s memory region |
|
|
290
|
+
| `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
|
|
291
|
+
| `None` | `None` | `'memory'` | All memory regions |
|
|
292
|
+
| `None` | `'users'` | `None` | "users" tag across all backends |
|
|
293
|
+
|
|
294
|
+
### Cross-Module Clearing
|
|
295
|
+
|
|
296
|
+
When clearing from a different module, use the `package` parameter:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
# In myapp/service.py
|
|
300
|
+
@cache(ttl=300)
|
|
301
|
+
def get_data(id: int) -> dict:
|
|
302
|
+
return fetch(id)
|
|
303
|
+
|
|
304
|
+
# In tests/conftest.py
|
|
305
|
+
cachu.cache_clear(backend='memory', ttl=300, package='myapp')
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Instance and Class Methods
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
class UserRepository:
|
|
312
|
+
def __init__(self, db):
|
|
313
|
+
self.db = db
|
|
314
|
+
|
|
315
|
+
@cache(ttl=300)
|
|
316
|
+
def get_user(self, user_id: int) -> dict:
|
|
317
|
+
return self.db.fetch(user_id)
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
@cache(ttl=300)
|
|
321
|
+
def get_default_user(cls) -> dict:
|
|
322
|
+
return cls.DEFAULT_USER
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
@cache(ttl=300)
|
|
326
|
+
def get_guest() -> dict:
|
|
327
|
+
return {'id': 0, 'name': 'Guest'}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
Disable caching globally for tests:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
import cachu
|
|
336
|
+
import pytest
|
|
337
|
+
|
|
338
|
+
@pytest.fixture(autouse=True)
|
|
339
|
+
def disable_caching():
|
|
340
|
+
cachu.disable()
|
|
341
|
+
yield
|
|
342
|
+
cachu.enable()
|
|
343
|
+
|
|
344
|
+
# Check state
|
|
345
|
+
if cachu.is_disabled():
|
|
346
|
+
print("Caching is disabled")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Advanced
|
|
350
|
+
|
|
351
|
+
### Direct Backend Access
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from cachu import get_backend
|
|
355
|
+
|
|
356
|
+
backend = get_backend('memory', ttl=300)
|
|
357
|
+
backend.set('my_key', {'data': 'value'}, ttl=300)
|
|
358
|
+
value = backend.get('my_key')
|
|
359
|
+
backend.delete('my_key')
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Redis Client Access
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
from cachu import get_redis_client
|
|
366
|
+
|
|
367
|
+
client = get_redis_client()
|
|
368
|
+
client.set('direct_key', 'value')
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Public API
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from cachu import (
|
|
375
|
+
# Configuration
|
|
376
|
+
configure,
|
|
377
|
+
get_config,
|
|
378
|
+
get_all_configs,
|
|
379
|
+
disable,
|
|
380
|
+
enable,
|
|
381
|
+
is_disabled,
|
|
382
|
+
|
|
383
|
+
# Decorator
|
|
384
|
+
cache,
|
|
385
|
+
|
|
386
|
+
# CRUD Operations
|
|
387
|
+
cache_get,
|
|
388
|
+
cache_set,
|
|
389
|
+
cache_delete,
|
|
390
|
+
cache_clear,
|
|
391
|
+
cache_info,
|
|
392
|
+
|
|
393
|
+
# Advanced
|
|
394
|
+
get_backend,
|
|
395
|
+
get_redis_client,
|
|
396
|
+
)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Features
|
|
400
|
+
|
|
401
|
+
- **Multiple backends**: Memory, file (DBM), and Redis
|
|
402
|
+
- **Flexible TTL**: Configure different TTLs for different use cases
|
|
403
|
+
- **Tags**: Organize and selectively clear cache entries
|
|
404
|
+
- **Package isolation**: Each package gets isolated configuration
|
|
405
|
+
- **Conditional caching**: Cache based on result value
|
|
406
|
+
- **Validation callbacks**: Validate entries before returning
|
|
407
|
+
- **Per-call control**: Skip or overwrite cache per call
|
|
408
|
+
- **Statistics**: Track hits, misses, and cache size
|
|
409
|
+
- **Intelligent filtering**: Auto-excludes `self`, `cls`, connections, and `_` params
|
|
410
|
+
- **Global disable**: Bypass all caching for testing
|