Skip to content

Commit 47a9edf

Browse files
committed
new template keys and clean up
1 parent 1ef663e commit 47a9edf

File tree

7 files changed

+506
-88
lines changed

7 files changed

+506
-88
lines changed

CHANGELOG.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Redis cluster support
1616
- DynamoDB backend example
1717

18+
## [0.1.2] - 2025-12-10
19+
20+
### Changed
21+
- Unified public decorator arguments to use consistent names:
22+
- `TTLCache.cached(key, ttl, cache=None)`
23+
- `SWRCache.cached(key, ttl, stale_ttl=0, cache=None, enable_lock=True)`
24+
- `BGCache.register_loader(key, interval_seconds, ttl=None, run_immediately=True, on_error=None, cache=None)`
25+
- Documented and clarified key template behavior across decorators:
26+
- Positional templates: `"user:{}"` → first positional argument
27+
- Named templates: `"user:{user_id}"`, `"i18n:{lang}"` → keyword arguments by name
28+
- Robust key lambdas for default arguments and complex keys.
29+
- Updated README API reference to match current behavior and naming, with:
30+
- New "Key templates & custom keys" section.
31+
- Richer examples for TTLCache, SWRCache, and BGCache (sync + async).
32+
- Clear explanation of how `key`, `ttl`, `stale_ttl`, and `interval_seconds` interact.
33+
34+
### Added
35+
- New edge-case tests for:
36+
- `InMemCache` (cleanup, lock property, `set_if_not_exists` with expired entries).
37+
- `HybridCache` (constructor validation, basic get/set/exists/delete behavior).
38+
- `validate_cache_storage()` failure path.
39+
- Decorator key-generation edge paths:
40+
- Static keys without placeholders.
41+
- No-arg functions with static keys.
42+
- Templates with positional placeholders but only kwargs passed.
43+
- Templates with missing named placeholders falling back to raw keys.
44+
- Additional key-template tests for TTLCache and SWRCache:
45+
- Positional vs named templates.
46+
- Extra kwargs with named templates.
47+
- Default-argument handling via `key=lambda *a, **k: ...`.
48+
49+
### Quality
50+
- Increased test coverage from ~70% to ~82%:
51+
- `decorators.py` coverage improved to ~87%.
52+
- `storage.py` coverage improved to ~74%.
53+
- Ensured all tests pass under the documented `pyproject.toml` configuration.
54+
55+
[Unreleased]: https://github.com/namshiv2/advanced_caching/compare/v0.1.2...HEAD
56+
[0.1.2]: https://github.com/namshiv2/advanced_caching/compare/v0.1.1...v0.1.2
57+
[0.1.1]: https://github.com/namshiv2/advanced_caching/releases/tag/v0.1.1
58+
1859
## [0.1.1] - 2025-12-10
1960

2061
### Added
@@ -48,4 +89,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4889
- **Production-Ready:** Comprehensive tests, benchmarks, and documentation
4990

5091
[0.1.1]: https://github.com/namshiv2/advanced_caching/releases/tag/v0.1.1
51-

README.md

Lines changed: 185 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,79 +102,233 @@ Full benchmarks available in `tests/benchmark.py`.
102102

103103
## API Reference
104104

105+
### Key templates & custom keys
106+
107+
All caching decorators share the same key concept:
108+
109+
- `key: str` – String template or literal
110+
- `key: Callable[..., str]` – Function that returns a string key
111+
112+
Supported patterns:
113+
114+
1. **Positional placeholder** – first positional argument:
115+
116+
```python
117+
@TTLCache.cached("user:{}", ttl=60)
118+
def get_user(user_id: int):
119+
...
120+
121+
get_user(42) # key -> "user:42"
122+
```
123+
124+
2. **Named placeholder** – keyword arguments by name:
125+
126+
```python
127+
@TTLCache.cached("user:{user_id}", ttl=60)
128+
def get_user(*, user_id: int):
129+
...
130+
131+
get_user(user_id=42) # key -> "user:42"
132+
```
133+
134+
3. **Named with extra kwargs** – only the named part is used for the key:
135+
136+
```python
137+
@SWRCache.cached("i18n:{lang}", ttl=60, stale_ttl=30)
138+
def load_i18n(lang: str, region: str | None = None):
139+
...
140+
141+
load_i18n(lang="en", region="US") # key -> "i18n:en"
142+
```
143+
144+
4. **Default arguments + robust key lambda** – recommended for complex/default cases:
145+
146+
```python
147+
@SWRCache.cached(
148+
key=lambda *a, **k: f"i18n:all:{k.get('lang', a[0] if a else 'en')}",
149+
ttl=60,
150+
stale_ttl=30,
151+
)
152+
def load_all(lang: str = "en") -> dict:
153+
print(f"Loading i18n for {lang}")
154+
return {"hello": f"Hello in {lang}"}
155+
156+
load_all() # key -> "i18n:all:en"
157+
load_all("en") # key -> "i18n:all:en"
158+
load_all(lang="en") # key -> "i18n:all:en"
159+
# Body runs once, subsequent calls are cached
160+
```
161+
162+
---
163+
105164
### TTLCache.cached(key, ttl, cache=None)
106165
Simple time-based cache with configurable TTL.
107166

167+
**Signature:**
168+
```python
169+
TTLCache.cached(
170+
key: str | Callable[..., str],
171+
ttl: int,
172+
cache: CacheStorage | None = None,
173+
) -> Callable
174+
```
175+
108176
**Parameters:**
109-
- `key` (str | callable): Cache key or function. String `"user:{}"` formats with first arg
177+
- `key` (str | callable): Cache key template or generator function
110178
- `ttl` (int): Time-to-live in seconds
111179
- `cache` (CacheStorage): Optional custom backend (defaults to InMemCache)
112180

113-
**Example:**
181+
**Examples:**
182+
183+
Positional key:
114184
```python
115-
@TTLCache.cached("user:{}", ttl=300)
116-
def get_user(user_id):
185+
@TTLCache.cached("user:{},", ttl=300)
186+
def get_user(user_id: int):
117187
return db.fetch(user_id)
118188

119-
# Custom backend
120-
@TTLCache.cached("data:{}", ttl=60, cache=redis_cache)
121-
def get_data(data_id):
122-
return api.fetch(data_id)
189+
get_user(42) # key -> "user:42"
190+
```
191+
192+
Named key:
193+
```python
194+
@TTLCache.cached("user:{user_id}", ttl=300)
195+
def get_user(*, user_id: int):
196+
return db.fetch(user_id)
197+
198+
get_user(user_id=42) # key -> "user:42"
199+
```
200+
201+
Custom key function:
202+
```python
203+
@TTLCache.cached(key=lambda *a, **k: f"user:{k.get('user_id', a[0])}", ttl=300)
204+
def get_user(user_id: int = 0):
205+
return db.fetch(user_id)
123206
```
124207

125208
---
126209

127210
### SWRCache.cached(key, ttl, stale_ttl=0, cache=None, enable_lock=True)
128211
Serve stale data instantly while refreshing in background.
129212

213+
**Signature:**
214+
```python
215+
SWRCache.cached(
216+
key: str | Callable[..., str],
217+
ttl: int,
218+
stale_ttl: int = 0,
219+
cache: CacheStorage | None = None,
220+
enable_lock: bool = True,
221+
) -> Callable
222+
```
223+
130224
**Parameters:**
131-
- `key` (str | callable): Cache key (same format as TTLCache)
225+
- `key` (str | callable): Cache key (same patterns as TTLCache)
132226
- `ttl` (int): Fresh data TTL in seconds
133227
- `stale_ttl` (int): Grace period to serve stale data while refreshing
134228
- `cache` (CacheStorage): Optional custom backend
135229
- `enable_lock` (bool): Prevent thundering herd (default: True)
136230

137-
**Example:**
231+
**Examples:**
232+
233+
Basic SWR with positional key:
138234
```python
139235
@SWRCache.cached("product:{}", ttl=60, stale_ttl=30)
140-
def get_product(product_id):
236+
def get_product(product_id: int):
141237
return api.fetch_product(product_id)
142238

143-
# Returns immediately with fresh or stale data
144-
# Never blocks on refresh
239+
get_product(1) # key -> "product:1"
240+
```
241+
242+
Named key with kwargs:
243+
```python
244+
@SWRCache.cached("i18n:{lang}", ttl=60, stale_ttl=30)
245+
def load_i18n(*, lang: str = "en") -> dict:
246+
return {"hello": f"Hello in {lang}"}
247+
248+
load_i18n(lang="en") # key -> "i18n:en"
249+
```
250+
251+
Default arg + key lambda (robust):
252+
```python
253+
@SWRCache.cached(
254+
key=lambda *a, **k: f"i18n:all:{k.get('lang', a[0] if a else 'en')}",
255+
ttl=60,
256+
stale_ttl=30,
257+
)
258+
def load_all(lang: str = "en") -> dict:
259+
return {"hello": f"Hello in {lang}"}
145260
```
146261

147262
---
148263

149-
### BGCache.register_loader(cache_key, interval_seconds, ttl_seconds=None, run_immediately=True, on_error=None, cache=None)
264+
### BGCache.register_loader(key, interval_seconds, ttl=None, run_immediately=True, on_error=None, cache=None)
150265
Pre-load expensive data with periodic refresh.
151266

267+
**Signature:**
268+
```python
269+
BGCache.register_loader(
270+
key: str,
271+
interval_seconds: int,
272+
ttl: int | None = None,
273+
run_immediately: bool = True,
274+
on_error: Callable[[Exception], None] | None = None,
275+
cache: CacheStorage | None = None,
276+
) -> Callable
277+
```
278+
152279
**Parameters:**
153-
- `cache_key` (str): Unique cache key (no formatting)
280+
- `key` (str): Unique cache key (no formatting, fixed string)
154281
- `interval_seconds` (int): Refresh interval in seconds
155-
- `ttl_seconds` (int): Cache TTL (defaults to interval_seconds × 2)
156-
- `run_immediately` (bool): Load on registration (default: True)
157-
- `on_error` (callable): Error handler function
282+
- `ttl` (int | None): Cache TTL (defaults to 2 × interval_seconds when None)
283+
- `run_immediately` (bool): Load once at registration (default: True)
284+
- `on_error` (callable): Error handler function `(Exception) -> None`
158285
- `cache` (CacheStorage): Optional custom backend
159286

160-
**Example:**
287+
**Examples:**
288+
289+
Sync loader:
161290
```python
162-
@BGCache.register_loader("inventory", interval_seconds=300)
163-
def load_inventory():
164-
return warehouse_api.get_items()
291+
from advanced_caching import BGCache
165292

166-
# Async support
167-
@BGCache.register_loader("products", interval_seconds=300)
168-
async def load_products():
293+
@BGCache.register_loader(key="inventory", interval_seconds=300, ttl=900)
294+
def load_inventory() -> list[dict]:
295+
return warehouse_api.get_all_items()
296+
297+
# Later
298+
items = load_inventory() # instant access to cached data
299+
```
300+
301+
Async loader:
302+
```python
303+
@BGCache.register_loader(key="products", interval_seconds=300, ttl=900)
304+
async def load_products() -> list[dict]:
169305
return await api.fetch_products()
170306

171-
# With error handling
172-
@BGCache.register_loader("config", interval_seconds=60, on_error=logger.error)
173-
def load_config():
174-
return settings.fetch()
307+
products = await load_products() # returns cached list
308+
```
175309

176-
# Shutdown scheduler when done
177-
BGCache.shutdown()
310+
With error handling:
311+
```python
312+
errors: list[Exception] = []
313+
314+
def on_error(exc: Exception) -> None:
315+
errors.append(exc)
316+
317+
@BGCache.register_loader(
318+
key="unstable",
319+
interval_seconds=60,
320+
run_immediately=True,
321+
on_error=on_error,
322+
)
323+
def maybe_fails() -> dict:
324+
raise RuntimeError("boom")
325+
326+
# errors list will contain the exception from background job
327+
```
328+
329+
Shutdown scheduler when done:
330+
```python
331+
BGCache.shutdown(wait=True)
178332
```
179333

180334
---
@@ -456,4 +610,3 @@ MIT License – See [LICENSE](LICENSE) for details.
456610

457611
---
458612

459-

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "advanced-caching"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "Production-ready composable caching with TTL, SWR, and background refresh patterns for Python."
99
readme = "README.md"
1010
requires-python = ">=3.10"

0 commit comments

Comments
 (0)