Cache
cache/ — Your caching Swiss Army knife 🔪
A collection of decorators and utilities to make caching just work when you need it, whether you're storing pickles, numpy arrays, FAISS indices, or serving from Redis. All decorators respect BFG["cache_dir"] from config and log their actions via the YumBox logger. You no longer need to redefine getters/setters on each project, with a standard interface you can cache the valuable AI model inferences and outputs very easily.
🎯 Quick Start
from yumbox.cache import cache, np_cache, retry
@cache # Auto-saves/loads result to cache_dir/{func_name}.pkl
def expensive_computation(x):
return heavy_ml_pipeline(x)
@np_cache # For functions returning dict[str, np.ndarray]
def extract_features(data, cache=None, cache_file=None):
if cache: # Partial cache hit!
# merge cached + new results
...
return {"embeddings": embeddings, "labels": labels}
@retry(max_tries=3, backoff_factor=1) # Auto-retry on failure
def flaky_api_call(url):
return requests.get(url).json()
🧰 Decorators Reference
Basic Caching
| Decorator | Format | Key Strategy | Best For |
|---|---|---|---|
@cache |
.pkl |
function name only | Simple, deterministic functions |
@cache_kwargs |
.pkl |
func_{k1-v1,k2-v2}.pkl |
Functions where kwargs change output |
@cache_kwargs_hash |
.pkl |
MD5 hash of sorted kwargs | When kwargs are long/complex |
@timed_cache(hours) |
.pkl |
function name + mtime check | Stale-while-revalidate patterns |
@async_cache |
.pkl |
function name | async def functions |
NumPy-Aware Caching
| Decorator | Format | Key Strategy | Notes |
|---|---|---|---|
@np_cache |
.npz |
function name | Expects cache/cache_file params in func |
@np_cache_lazy |
.npz |
function name | Loads only requested keys via mmap |
@np_cache_kwargs |
.npz |
func_{kwargs}.npz |
kwargs affect cache key |
@np_cache_kwargs_hash |
.npz |
MD5 of kwargs | Safer for long/unordered kwargs |
@np_cache_kwargs_list_hash |
.npz |
MD5 of cache_kwargs subset |
Fine-grained control over what affects cache |
@unsafe_np_cache |
.npz |
function name | Enables allow_pickle=True (⚠️ security) |
Specialized Formats
| Decorator | Backend | Use Case |
|---|---|---|
@index |
FAISS .bin |
Caching vector indices |
@safe_cache |
safetensors .safetensors |
Secure, framework-agnostic tensor storage |
@kv_cache |
pickle .pkl |
Generic key-value caching pattern |
@hd5_cache |
HDF5 .h5 |
Large numeric datasets with key-based access |
@lmdb_cache |
LMDB (vector) | High-performance local key-value store for arrays |
@redis_cache |
Redis | Distributed caching across workers/nodes |
Retry & Pagination Helpers
| Decorator | Purpose |
|---|---|
@retry(...) |
Retry on exceptions with fixed/exponential backoff, validator support, and graceful error returns |
@last_offset |
Cache paginated results + persist last offset (integer) for resumable iteration |
@last_str_offset |
Same as above, but offset is a string (e.g., cursor tokens) |
@retry Configuration
@retry(
max_tries=5, # Total attempts (1 initial + 4 retries)
wait=2, # Fixed 2s between retries
backoff_factor=1, # OR: exponential: 1s, 2s, 4s, 8s...
max_wait=30, # Cap exponential backoff at 30s
validator=my_check, # Optional: validate response before accepting
return_exception=True, # Return {"status":"error",...} instead of raising
retry_on400=False # Don't retry on client errors by default
)
def call_service(...): ...
🗄️ Storage Backends (cache/*.py)
fs.py — Safe File I/O Utilities
Atomic writes, lock retries, and format-agnostic helpers:
from yumbox.cache.fs import safe_wpickle, safe_rpickle, FSImage
# Safe pickle (writes to temp, then atomic move)
safe_wpickle("result.pkl", my_obj)
data = safe_rpickle("result.pkl")
# Browse & sample images from directories
fs_img = FSImage(
data_dirs=["/data/imgs", "/backup/imgs"],
exts=[".jpg", ".png"], # or "image" for MIME prefix
depth=2 # max subdirectory depth (0 = unlimited)
)
sample = fs_img.get_random(seed=42) # reproducible sampling
| Utility | Purpose |
|---|---|
safe_save* / safe_load* |
Atomic write/read via temp file + shutil.move |
retry_on_lock |
Decorator to retry on EACCES/EAGAIN |
safe_wpickle / safe_rpickle |
Pickle helpers with atomicity + lock retries |
FSImage |
Lightweight image dataset loader with MIME/extension filtering |
hdf.py — HDF5 Caching
Store dict[str, np.ndarray] with base64-encoded keys (HDF5 doesn't like special chars):
@hd5_cache
def compute_embeddings(texts, keys=None, cache=None, cache_file=None):
# cache comes pre-loaded for requested `keys`
# return dict of new/updated arrays
return {"emb_001": arr1, "emb_002": arr2}
- Keys are base64-encoded internally → safe for HDF5
- Supports partial loads via
keys=kwarg - Compression:
gzipby default
kv.py — LMDB Stores
Three flavors for different needs:
| Class | Use Case | Serialization |
|---|---|---|
LMDB |
Simple key → dict | msgpack |
LMDBMultiIndex |
Multiple keys → same data (dedup) | msgpack + indirection |
VectorLMDB |
key → np.ndarray |
gzip-compressed numpy |
# VectorLMDB example
with VectorLMDB("embeddings", "/cache/dir") as db:
db.upsert("doc_123", embedding_array)
vec = db.get("doc_123")
meta = db.get_info("doc_123") # shape, dtype, size without loading
Bulk operations (bulk_upsert, bulk_get) use LMDB's putmulti for speed.
redis.py — Distributed Vector Cache
VectorRedis mirrors VectorLMDB API but over Redis:
# Connect
vredis = VectorRedis(
db_name="prod_embeddings",
host="redis.internal",
port=6379,
password=os.getenv("REDIS_PASS")
)
# Use
vredis.upsert("user_42", embedding, ttl=3600) # 1h expiry
batch = vredis.bulk_get(["user_42", "user_43"])
- Arrays compressed with gzip before storage
- Supports TTL, pipelines, and
scan_iterfor key listing - Decorators
@redis_cache/@redis_cache_kwargs_list_hashwork like their local counterparts
🔑 Common Patterns
Partial Cache Hits (NumPy/HDF5/LMDB)
Functions decorated with *_cache receive cache (pre-loaded results) and cache_file (path/identifier). Use them to avoid recomputing:
@np_cache
def process_batch(items, keys=None, cache=None, cache_file=None):
results = {}
for key in keys:
if cache and key in cache:
results[key] = cache[key] # reuse
else:
results[key] = heavy_compute(key) # compute new
return results # decorator saves merged results
Hash-Based Keys for Complex kwargs
When kwargs are dicts/lists/unhashable, use the _hash variants:
@cache_kwargs_hash # or np_cache_kwargs_hash
def train_model(config: dict, data_path: str):
# cache key = MD5(json.dumps(sorted(config)))
...
Resumable Pagination
Combine @last_offset with limit-based fetching:
@last_offset
def fetch_page(source, offset=0, limit=100, cached_output=None):
if cached_output:
# merge new page with cached results
...
new_items, next_offset = api.get_batch(offset, limit)
return cached_output + new_items, next_offset
⚙️ Configuration
All caching respects BFG["cache_dir"] from yumbox.config. Set it once at startup:
# config.yaml
cache_dir: "/tmp/yumbox_cache"
# or in code
from yumbox.config import BFG
BFG["cache_dir"] = Path.home() / ".cache" / "yumbox"
No cache_dir? Decorators become no-ops (functions execute normally, no I/O).
💡 Pro tip: Mix and match! Use
@np_cache_lazyfor local dev, swap to@redis_cachefor distributed training — same function signature, zero logic changes.
Happy caching! 🚀 If you find a decorator that doesn't quite fit your use case, the code is intentionally modular — extend away.