1111- [ Installation] ( #installation ) – Get started in 30 seconds
1212- [ Quick Examples] ( #quick-start ) – Copy-paste ready code
1313- [ API Reference] ( #api-reference ) – Full decorator & backend docs
14+ - [ Storage & Redis] ( #storage--redis ) – Redis/Hybrid/custom storage examples
1415- [ Custom Storage] ( #custom-storage ) – Implement your own backend
1516- [ Benchmarks] ( #benchmarks ) – See the performance gains
1617- [ Use Cases] ( #use-cases ) – Real-world examples
@@ -37,6 +38,8 @@ pip install advanced-caching
3738uv pip install advanced-caching
3839# with Redis support
3940pip install " advanced-caching[redis]"
41+ # with Redis support (uv)
42+ uv pip install " advanced-caching[redis]"
4043```
4144
4245## Quick Start
@@ -90,6 +93,10 @@ user = await get_user_async(42)
9093## Benchmarks
9194Full benchmarks available in ` tests/benchmark.py ` .
9295
96+ Step-by-step benchmarking + profiling guide: ` docs/benchmarking-and-profiling.md ` .
97+
98+ Storage & Redis usage is documented below.
99+
93100## API Reference
94101
95102### Key templates & custom keys
@@ -159,7 +166,7 @@ Simple time-based cache with configurable TTL.
159166TTLCache.cached(
160167 key: str | Callable[... , str ],
161168 ttl: int ,
162- cache: CacheStorage | None = None ,
169+ cache: CacheStorage | Callable[[], CacheStorage] | None = None ,
163170) -> Callable
164171```
165172
@@ -172,7 +179,7 @@ TTLCache.cached(
172179
173180Positional key:
174181``` python
175- @TTLCache.cached (" user:{} , " , ttl = 300 )
182+ @TTLCache.cached (" user:{} " , ttl = 300 )
176183def get_user (user_id : int ):
177184 return db.fetch(user_id)
178185
@@ -206,7 +213,7 @@ SWRCache.cached(
206213 key: str | Callable[... , str ],
207214 ttl: int ,
208215 stale_ttl: int = 0 ,
209- cache: CacheStorage | None = None ,
216+ cache: CacheStorage | Callable[[], CacheStorage] | None = None ,
210217 enable_lock: bool = True ,
211218) -> Callable
212219```
@@ -262,7 +269,7 @@ BGCache.register_loader(
262269 ttl: int | None = None ,
263270 run_immediately: bool = True ,
264271 on_error: Callable[[Exception ], None ] | None = None ,
265- cache: CacheStorage | None = None ,
272+ cache: CacheStorage | Callable[[], CacheStorage] | None = None ,
266273) -> Callable
267274```
268275
@@ -325,6 +332,97 @@ BGCache.shutdown(wait=True)
325332
326333### Storage Backends
327334
335+ ## Storage & Redis
336+
337+ ### Install (uv)
338+
339+ ``` bash
340+ uv pip install advanced-caching
341+ uv pip install " advanced-caching[redis]" # for RedisCache / HybridCache
342+ ```
343+
344+ ### How storage is chosen
345+
346+ - If you don’t pass ` cache=... ` , each decorated function lazily creates its own ` InMemCache ` instance.
347+ - You can pass either a cache instance (` cache=my_cache ` ) or a cache factory (` cache=lambda: my_cache ` ).
348+
349+ ### Share one storage instance
350+
351+ ``` python
352+ from advanced_caching import InMemCache, TTLCache
353+
354+ shared = InMemCache()
355+
356+ @TTLCache.cached (" user:{} " , ttl = 60 , cache = shared)
357+ def get_user (user_id : int ) -> dict :
358+ return {" id" : user_id}
359+
360+ @TTLCache.cached (" org:{} " , ttl = 60 , cache = shared)
361+ def get_org (org_id : int ) -> dict :
362+ return {" id" : org_id}
363+ ```
364+
365+ ### Use RedisCache (distributed)
366+
367+ ` RedisCache ` stores values in Redis using ` pickle ` .
368+
369+ ``` python
370+ import redis
371+ from advanced_caching import RedisCache, TTLCache
372+
373+ client = redis.Redis(host = " localhost" , port = 6379 )
374+ cache = RedisCache(client, prefix = " app:" )
375+
376+ @TTLCache.cached (" user:{} " , ttl = 300 , cache = cache)
377+ def get_user (user_id : int ) -> dict :
378+ return {" id" : user_id}
379+ ```
380+
381+ ### Use SWRCache with RedisCache (recommended)
382+
383+ ` SWRCache ` uses ` get_entry ` /` set_entry ` so it can store freshness metadata.
384+
385+ ``` python
386+ import redis
387+ from advanced_caching import RedisCache, SWRCache
388+
389+ client = redis.Redis(host = " localhost" , port = 6379 )
390+ cache = RedisCache(client, prefix = " products:" )
391+
392+ @SWRCache.cached (" product:{} " , ttl = 60 , stale_ttl = 30 , cache = cache)
393+ def get_product (product_id : int ) -> dict :
394+ return {" id" : product_id}
395+ ```
396+
397+ ### Use HybridCache (L1 memory + L2 Redis)
398+
399+ ` HybridCache ` is a two-level cache:
400+ - ** L1** : fast in-memory (` InMemCache ` )
401+ - ** L2** : Redis-backed (` RedisCache ` )
402+
403+ Reads go to L1 first; on L1 miss it tries L2; on L2 hit it warms L1.
404+
405+ ``` python
406+ import redis
407+ from advanced_caching import HybridCache, InMemCache, RedisCache, TTLCache
408+
409+ client = redis.Redis(host = " localhost" , port = 6379 )
410+
411+ hybrid = HybridCache(
412+ l1_cache = InMemCache(),
413+ l2_cache = RedisCache(client, prefix = " app:" ),
414+ l1_ttl = 60 ,
415+ )
416+
417+ @TTLCache.cached (" user:{} " , ttl = 300 , cache = hybrid)
418+ def get_user (user_id : int ) -> dict :
419+ return {" id" : user_id}
420+ ```
421+
422+ Notes:
423+ - ` ttl ` on the decorator controls how long values are considered valid.
424+ - ` l1_ttl ` controls how long HybridCache keeps values in memory after an L2 hit.
425+
328426#### InMemCache()
329427Thread-safe in-memory cache with TTL.
330428
@@ -386,18 +484,16 @@ if entry and entry.is_fresh():
386484
387485Implement the ` CacheStorage ` protocol for custom backends (DynamoDB, file-based, encrypted storage, etc.).
388486
389- ### File-Based Cache Example
487+ ### File-based example
390488
391489``` python
392490import json
393491import time
394492from pathlib import Path
395- from advanced_caching import CacheStorage, TTLCache, validate_cache_storage
493+ from advanced_caching import CacheEntry, CacheStorage, TTLCache, validate_cache_storage
396494
397495
398496class FileCache (CacheStorage ):
399- """ File-based cache storage."""
400-
401497 def __init__ (self , directory : str = " /tmp/cache" ):
402498 self .directory = Path(directory)
403499 self .directory.mkdir(parents = True , exist_ok = True )
@@ -406,26 +502,45 @@ class FileCache(CacheStorage):
406502 safe_key = key.replace(" /" , " _" ).replace(" :" , " _" )
407503 return self .directory / f " { safe_key} .json "
408504
409- def get (self , key : str ):
505+ def get_entry (self , key : str ) -> CacheEntry | None :
410506 path = self ._get_path(key)
411507 if not path.exists():
412508 return None
413509 try :
414510 with open (path) as f:
415511 data = json.load(f)
416- if data[" fresh_until" ] < time.time():
417- path.unlink()
418- return None
419- return data[" value" ]
420- except (json.JSONDecodeError, KeyError , OSError ):
512+ return CacheEntry(
513+ value = data[" value" ],
514+ fresh_until = float (data[" fresh_until" ]),
515+ created_at = float (data[" created_at" ]),
516+ )
517+ except Exception :
518+ return None
519+
520+ def set_entry (self , key : str , entry : CacheEntry, ttl : int | None = None ) -> None :
521+ now = time.time()
522+ if ttl is not None :
523+ fresh_until = now + ttl if ttl > 0 else float (" inf" )
524+ entry = CacheEntry(value = entry.value, fresh_until = fresh_until, created_at = now)
525+ with open (self ._get_path(key), " w" ) as f:
526+ json.dump(
527+ {" value" : entry.value, " fresh_until" : entry.fresh_until, " created_at" : entry.created_at},
528+ f,
529+ )
530+
531+ def get (self , key : str ):
532+ entry = self .get_entry(key)
533+ if entry is None :
421534 return None
535+ if not entry.is_fresh():
536+ self .delete(key)
537+ return None
538+ return entry.value
422539
423540 def set (self , key : str , value , ttl : int = 0 ) -> None :
424541 now = time.time()
425542 fresh_until = now + ttl if ttl > 0 else float (" inf" )
426- data = {" value" : value, " fresh_until" : fresh_until, " created_at" : now}
427- with open (self ._get_path(key), " w" ) as f:
428- json.dump(data, f)
543+ self .set_entry(key, CacheEntry(value = value, fresh_until = fresh_until, created_at = now))
429544
430545 def delete (self , key : str ) -> None :
431546 self ._get_path(key).unlink(missing_ok = True )
@@ -440,15 +555,12 @@ class FileCache(CacheStorage):
440555 return True
441556
442557
443- # Use it
444558cache = FileCache(" /tmp/app_cache" )
445559assert validate_cache_storage(cache)
446560
447561@TTLCache.cached (" user:{} " , ttl = 300 , cache = cache)
448562def get_user (user_id : int ):
449- return {" id" : user_id, " name" : f " User { user_id} " }
450-
451- user = get_user(42 ) # Stores in /tmp/app_cache/user_42.json
563+ return {" id" : user_id}
452564```
453565
454566### Best Practices
@@ -463,12 +575,12 @@ user = get_user(42) # Stores in /tmp/app_cache/user_42.json
463575
464576### Run Tests
465577``` bash
466- pytest tests/test_correctness.py -v
578+ uv run pytest tests/test_correctness.py -v
467579```
468580
469581### Run Benchmarks
470582``` bash
471- python tests/benchmark.py
583+ uv run python tests/benchmark.py
472584```
473585
474586
@@ -564,39 +676,11 @@ Contributions welcome! Please:
5646761 . Fork the repository
5656772 . Create a feature branch (` git checkout -b feature/my-feature ` )
5666783 . Add tests for new functionality
567- 4 . Ensure all tests pass (` pytest ` )
679+ 4 . Ensure all tests pass (` uv run pytest` )
5686805 . Submit a pull request
569681
570682---
571683
572684## License
573685
574686MIT License – See [ LICENSE] ( LICENSE ) for details.
575-
576- ---
577-
578- ## Changelog
579-
580- ### 0.1.0 (Initial Release)
581- - ✅ TTL Cache decorator
582- - ✅ SWR Cache decorator
583- - ✅ Background Cache with APScheduler
584- - ✅ InMemCache, RedisCache, HybridCache storage backends
585- - ✅ Full async/sync support
586- - ✅ Custom storage protocol
587- - ✅ Comprehensive test suite
588- - ✅ Benchmark suite
589-
590- ---
591-
592- ## Roadmap
593-
594- - [ ] Distributed tracing/observability
595- - [ ] Metrics export (Prometheus)
596- - [ ] Cache warming strategies
597- - [ ] Serialization plugins (msgpack, protobuf)
598- - [ ] Redis cluster support
599- - [ ] DynamoDB backend example
600-
601- ---
602-
0 commit comments