cachu 0.1.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/operations.py ADDED
@@ -0,0 +1,174 @@
1
+ """Cache CRUD operations.
2
+ """
3
+ import logging
4
+ from typing import Any
5
+ from collections.abc import Callable
6
+
7
+ from .backends import NO_VALUE
8
+ from .config import _get_caller_package, get_config
9
+ from .decorator import _get_backend, get_cache_info
10
+ from .keys import mangle_key
11
+ from .types import CacheInfo, CacheMeta
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _MISSING = object()
16
+
17
+
18
+ def _get_meta(fn: Callable[..., Any]) -> CacheMeta:
19
+ """Get CacheMeta from a decorated function.
20
+ """
21
+ meta = getattr(fn, '_cache_meta', None)
22
+ if meta is None:
23
+ raise ValueError(f'{fn.__name__} is not decorated with @cache')
24
+ return meta
25
+
26
+
27
+ def cache_get(fn: Callable[..., Any], default: Any = _MISSING, **kwargs: Any) -> Any:
28
+ """Get a cached value without calling the function.
29
+
30
+ Args:
31
+ fn: A function decorated with @cache
32
+ default: Value to return if not found (raises KeyError if not provided)
33
+ **kwargs: Function arguments to build the cache key
34
+
35
+ Returns
36
+ The cached value or default
37
+
38
+ Raises
39
+ KeyError: If not found and no default provided
40
+ ValueError: If function is not decorated with @cache
41
+ """
42
+ meta = _get_meta(fn)
43
+ cfg = get_config(meta.package)
44
+
45
+ key_generator = fn._cache_key_generator
46
+ base_key = key_generator(**kwargs)
47
+ cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
48
+
49
+ backend = _get_backend(meta.package, meta.backend, meta.ttl)
50
+ value = backend.get(cache_key)
51
+
52
+ if value is NO_VALUE:
53
+ if default is _MISSING:
54
+ raise KeyError(f'No cached value for {fn.__name__} with {kwargs}')
55
+ return default
56
+
57
+ return value
58
+
59
+
60
+ def cache_set(fn: Callable[..., Any], value: Any, **kwargs: Any) -> None:
61
+ """Set a cached value directly without calling the function.
62
+
63
+ Args:
64
+ fn: A function decorated with @cache
65
+ value: The value to cache
66
+ **kwargs: Function arguments to build the cache key
67
+
68
+ Raises
69
+ ValueError: If function is not decorated with @cache
70
+ """
71
+ meta = _get_meta(fn)
72
+ cfg = get_config(meta.package)
73
+
74
+ key_generator = fn._cache_key_generator
75
+ base_key = key_generator(**kwargs)
76
+ cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
77
+
78
+ backend = _get_backend(meta.package, meta.backend, meta.ttl)
79
+ backend.set(cache_key, value, meta.ttl)
80
+
81
+ logger.debug(f'Set cache for {fn.__name__} with key {cache_key}')
82
+
83
+
84
+ def cache_delete(fn: Callable[..., Any], **kwargs: Any) -> None:
85
+ """Delete a specific cached entry.
86
+
87
+ Args:
88
+ fn: A function decorated with @cache
89
+ **kwargs: Function arguments to build the cache key
90
+
91
+ Raises
92
+ ValueError: If function is not decorated with @cache
93
+ """
94
+ meta = _get_meta(fn)
95
+ cfg = get_config(meta.package)
96
+
97
+ key_generator = fn._cache_key_generator
98
+ base_key = key_generator(**kwargs)
99
+ cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
100
+
101
+ backend = _get_backend(meta.package, meta.backend, meta.ttl)
102
+ backend.delete(cache_key)
103
+
104
+ logger.debug(f'Deleted cache for {fn.__name__} with key {cache_key}')
105
+
106
+
107
+ def cache_clear(
108
+ tag: str | None = None,
109
+ backend: str | None = None,
110
+ ttl: int | None = None,
111
+ package: str | None = None,
112
+ ) -> int:
113
+ """Clear cache entries matching criteria.
114
+
115
+ Args:
116
+ tag: Clear only entries with this tag
117
+ backend: Backend type to clear ('memory', 'file', 'redis'). Clears all if None.
118
+ ttl: Specific TTL region to clear. Clears all TTLs if None.
119
+ package: Package to clear for. Auto-detected if None.
120
+
121
+ Returns
122
+ Number of entries cleared (may be approximate)
123
+ """
124
+ if package is None:
125
+ package = _get_caller_package()
126
+
127
+ cfg = get_config(package)
128
+
129
+ if backend is not None:
130
+ backends_to_clear = [backend]
131
+ else:
132
+ backends_to_clear = ['memory', 'file', 'redis']
133
+
134
+ if tag:
135
+ from .keys import _normalize_tag
136
+ pattern = f'*|{_normalize_tag(tag)}|*'
137
+ else:
138
+ pattern = None
139
+
140
+ total_cleared = 0
141
+
142
+ from .decorator import _backends, _backends_lock
143
+
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})')
157
+
158
+ return total_cleared
159
+
160
+
161
+ def cache_info(fn: Callable[..., Any]) -> CacheInfo:
162
+ """Get cache statistics for a decorated function.
163
+
164
+ Args:
165
+ fn: A function decorated with @cache
166
+
167
+ Returns
168
+ CacheInfo with hits, misses, and currsize
169
+
170
+ Raises
171
+ ValueError: If function is not decorated with @cache
172
+ """
173
+ _get_meta(fn) # Validate it's decorated
174
+ return get_cache_info(fn)
cachu/types.py ADDED
@@ -0,0 +1,37 @@
1
+ """Type definitions for the cache library.
2
+ """
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class CacheEntry:
10
+ """Cache entry metadata passed to validate callbacks.
11
+ """
12
+ value: Any
13
+ created_at: float
14
+ age: float
15
+
16
+
17
+ @dataclass
18
+ class CacheInfo:
19
+ """Cache statistics for a decorated function.
20
+ """
21
+ hits: int
22
+ misses: int
23
+ currsize: int
24
+
25
+
26
+ @dataclass
27
+ class CacheMeta:
28
+ """Metadata attached to cached functions.
29
+ """
30
+ ttl: int
31
+ backend: str
32
+ tag: str
33
+ exclude: set[str]
34
+ cache_if: Callable[[Any], bool] | None
35
+ validate: Callable[[CacheEntry], bool] | None
36
+ package: str
37
+ key_generator: Callable[..., str]
@@ -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
@@ -0,0 +1,15 @@
1
+ cachu/__init__.py,sha256=snSKE_XCc1SikF_FiyolTjTVc5E52QtXjvaXEQJqHs0,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.1.dist-info/METADATA,sha256=G8ue2kOMja2ER0KKUjKDDjVetNYS7OKJcQp1r-oWYY4,9629
13
+ cachu-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ cachu-0.1.1.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
15
+ cachu-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cachu