zoocache 2026.1.20__cp310-abi3-macosx_11_0_arm64.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.
- zoocache/__init__.py +13 -0
- zoocache/_zoocache.abi3.so +0 -0
- zoocache/context.py +33 -0
- zoocache/core.py +158 -0
- zoocache-2026.1.20.dist-info/METADATA +135 -0
- zoocache-2026.1.20.dist-info/RECORD +7 -0
- zoocache-2026.1.20.dist-info/WHEEL +4 -0
zoocache/__init__.py
ADDED
|
Binary file
|
zoocache/context.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import Set, Optional
|
|
3
|
+
|
|
4
|
+
_DEPS_CONTEXT: ContextVar[Optional[Set[str]]] = ContextVar(
|
|
5
|
+
"_DEPS_CONTEXT", default=None
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_deps(deps: list[str]) -> None:
|
|
10
|
+
"""Register dynamic dependencies for the current @cacheable call."""
|
|
11
|
+
ctx = _DEPS_CONTEXT.get()
|
|
12
|
+
if ctx is not None:
|
|
13
|
+
ctx.update(deps)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_current_deps() -> Optional[Set[str]]:
|
|
17
|
+
"""Get the dependency set for the current context."""
|
|
18
|
+
return _DEPS_CONTEXT.get()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DepsTracker:
|
|
22
|
+
"""Context manager to track dynamic dependencies."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.deps: Set[str] = set()
|
|
26
|
+
self.token = None
|
|
27
|
+
|
|
28
|
+
def __enter__(self):
|
|
29
|
+
self.token = _DEPS_CONTEXT.set(self.deps)
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
33
|
+
_DEPS_CONTEXT.reset(self.token)
|
zoocache/core.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Callable, Optional, Dict
|
|
5
|
+
from ._zoocache import Core
|
|
6
|
+
from .context import DepsTracker, get_current_deps
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_core: Optional[Core] = None
|
|
10
|
+
_config: Dict[str, Any] = {}
|
|
11
|
+
_op_counter: int = 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _reset() -> None:
|
|
15
|
+
"""Internal use only: reset the global state for testing."""
|
|
16
|
+
global _core, _config, _op_counter
|
|
17
|
+
_core = None
|
|
18
|
+
_config = {}
|
|
19
|
+
_op_counter = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def configure(
|
|
23
|
+
storage_url: Optional[str] = None,
|
|
24
|
+
bus_url: Optional[str] = None,
|
|
25
|
+
prefix: Optional[str] = None,
|
|
26
|
+
prune_after: Optional[int] = None,
|
|
27
|
+
default_ttl: Optional[int] = None,
|
|
28
|
+
read_extend_ttl: bool = True,
|
|
29
|
+
max_entries: Optional[int] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
global _core, _config
|
|
32
|
+
if _core is not None:
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
"zoocache already initialized, call configure() before any cache operation"
|
|
35
|
+
)
|
|
36
|
+
_config = {
|
|
37
|
+
"storage_url": storage_url,
|
|
38
|
+
"bus_url": bus_url,
|
|
39
|
+
"prefix": prefix,
|
|
40
|
+
"prune_after": prune_after,
|
|
41
|
+
"default_ttl": default_ttl,
|
|
42
|
+
"read_extend_ttl": read_extend_ttl,
|
|
43
|
+
"max_entries": max_entries,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_core() -> Core:
|
|
48
|
+
global _core
|
|
49
|
+
if _core is None:
|
|
50
|
+
# Filter config for Rust Core.__init__
|
|
51
|
+
core_args = {k: v for k, v in _config.items() if k != "prune_after"}
|
|
52
|
+
_core = Core(**core_args)
|
|
53
|
+
return _core
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _maybe_prune() -> None:
|
|
57
|
+
global _op_counter
|
|
58
|
+
_op_counter += 1
|
|
59
|
+
if _op_counter >= 1000:
|
|
60
|
+
_op_counter = 0
|
|
61
|
+
if age := _config.get("prune_after"):
|
|
62
|
+
prune(age)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def prune(max_age_secs: int = 3600) -> None:
|
|
66
|
+
"""Manually trigger pruning of the PrefixTrie."""
|
|
67
|
+
_get_core().prune(max_age_secs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _generate_key(
|
|
71
|
+
func: Callable, namespace: Optional[str], args: tuple, kwargs: dict
|
|
72
|
+
) -> str:
|
|
73
|
+
from ._zoocache import hash_key
|
|
74
|
+
|
|
75
|
+
obj = (func.__module__, func.__qualname__, args, sorted(kwargs.items()))
|
|
76
|
+
prefix = f"{namespace}:{func.__name__}" if namespace else func.__name__
|
|
77
|
+
return hash_key(obj, prefix)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def clear() -> None:
|
|
81
|
+
_get_core().clear()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _collect_deps(deps: Any, args: tuple, kwargs: dict) -> list[str]:
|
|
85
|
+
base = list(get_current_deps() or [])
|
|
86
|
+
extra = (deps(*args, **kwargs) if callable(deps) else deps) if deps else []
|
|
87
|
+
return list(set(base + list(extra)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def invalidate(tag: str) -> None:
|
|
91
|
+
_get_core().invalidate(tag)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cacheable(
|
|
95
|
+
namespace: Optional[str] = None, deps: Any = None, ttl: Optional[int] = None
|
|
96
|
+
):
|
|
97
|
+
def decorator(func: Callable):
|
|
98
|
+
@functools.wraps(func)
|
|
99
|
+
async def async_wrapper(*args, **kwargs):
|
|
100
|
+
key = _generate_key(func, namespace, args, kwargs)
|
|
101
|
+
_maybe_prune()
|
|
102
|
+
|
|
103
|
+
val, is_leader, fut = _get_core().get_or_entry_async(key)
|
|
104
|
+
if val is not None:
|
|
105
|
+
return val
|
|
106
|
+
|
|
107
|
+
if is_leader:
|
|
108
|
+
leader_fut = asyncio.get_running_loop().create_future()
|
|
109
|
+
_get_core().register_flight_future(key, leader_fut)
|
|
110
|
+
try:
|
|
111
|
+
res = await execute(key, args, kwargs)
|
|
112
|
+
_get_core().finish_flight(key, False, res)
|
|
113
|
+
leader_fut.set_result(res)
|
|
114
|
+
return res
|
|
115
|
+
except Exception as e:
|
|
116
|
+
_get_core().finish_flight(key, True, None)
|
|
117
|
+
leader_fut.set_exception(e)
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
if fut is not None:
|
|
121
|
+
return await fut
|
|
122
|
+
|
|
123
|
+
# Fallback if flight was already finished before we could wait
|
|
124
|
+
return await execute(key, args, kwargs)
|
|
125
|
+
|
|
126
|
+
async def execute(key, args, kwargs):
|
|
127
|
+
with DepsTracker():
|
|
128
|
+
res = await func(*args, **kwargs)
|
|
129
|
+
_get_core().set(key, res, _collect_deps(deps, args, kwargs), ttl=ttl)
|
|
130
|
+
return res
|
|
131
|
+
|
|
132
|
+
@functools.wraps(func)
|
|
133
|
+
def sync_wrapper(*args, **kwargs):
|
|
134
|
+
key = _generate_key(func, namespace, args, kwargs)
|
|
135
|
+
_maybe_prune()
|
|
136
|
+
val, is_leader = _get_core().get_or_entry(key)
|
|
137
|
+
if not is_leader:
|
|
138
|
+
return val
|
|
139
|
+
try:
|
|
140
|
+
with DepsTracker():
|
|
141
|
+
res = func(*args, **kwargs)
|
|
142
|
+
_get_core().set(
|
|
143
|
+
key, res, _collect_deps(deps, args, kwargs), ttl=ttl
|
|
144
|
+
)
|
|
145
|
+
_get_core().finish_flight(key, False, res)
|
|
146
|
+
return res
|
|
147
|
+
except Exception:
|
|
148
|
+
_get_core().finish_flight(key, True, None)
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
return async_wrapper if inspect.iscoroutinefunction(func) else sync_wrapper
|
|
152
|
+
|
|
153
|
+
return decorator
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def version() -> str:
|
|
157
|
+
"""Return the version of the Rust core."""
|
|
158
|
+
return _get_core().version()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zoocache
|
|
3
|
+
Version: 2026.1.20
|
|
4
|
+
Summary: Cache that invalidates when your data changes, not when a timer expires. Rust-powered semantic invalidation for Python.
|
|
5
|
+
Author-email: Alberto Daniel Badia <alberto_badia@enlacepatagonia.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<picture>
|
|
12
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
|
|
13
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-light.svg">
|
|
14
|
+
<img alt="ZooCache Logo" src="docs/assets/logo-light.svg" width="600">
|
|
15
|
+
</picture>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
Zoocache is a high-performance caching library with a Rust core, designed for applications where data consistency and read performance are critical.
|
|
20
|
+
</p>
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://www.python.org/downloads/"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue.svg"></a>
|
|
23
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
|
|
24
|
+
<img alt="PyPI" src="https://img.shields.io/pypi/v/zoocache">
|
|
25
|
+
<img alt="Downloads" src="https://img.shields.io/pypi/dm/zoocache">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ✨ Key Features
|
|
31
|
+
|
|
32
|
+
- 🚀 **Rust-Powered Performance**: Core logic implemented in Rust for ultra-low latency and safe concurrency.
|
|
33
|
+
- 🧠 **Semantic Invalidation**: Use a `PrefixTrie` for hierarchical invalidation. Clear "user:*" to invalidate all keys related to a specific user instantly.
|
|
34
|
+
- 🛡️ **Causal Consistency**: Built-in support for Hybrid Logical Clocks (HLC) ensures consistency even in distributed systems.
|
|
35
|
+
- ⚡ **Anti-Avalanche (SingleFlight)**: Protects your backend from "thundering herd" effects by coalescing concurrent identical requests.
|
|
36
|
+
- 📦 **Smart Serialization**: Transparently handles MsgPack and LZ4 compression for maximum throughput and minimum storage.
|
|
37
|
+
- 🔄 **Self-Healing Distributed Cache**: Automatic synchronization via Redis Bus with robust error recovery.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## ⚡ Quick Start
|
|
42
|
+
|
|
43
|
+
### Installation
|
|
44
|
+
|
|
45
|
+
Using `pip`:
|
|
46
|
+
```bash
|
|
47
|
+
pip install zoocache
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using `uv` (recommended):
|
|
51
|
+
```bash
|
|
52
|
+
uv add zoocache
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Simple Usage
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from zoocache import cacheable, invalidate
|
|
59
|
+
|
|
60
|
+
@cacheable(deps=lambda user_id: [f"user:{user_id}"])
|
|
61
|
+
def get_user(user_id: int):
|
|
62
|
+
return db.fetch_user(user_id)
|
|
63
|
+
|
|
64
|
+
def update_user(user_id: int, data: dict):
|
|
65
|
+
db.save(user_id, data)
|
|
66
|
+
invalidate(f"user:{user_id}") # All cached 'get_user' calls for this ID die instantly
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Complex Dependencies
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from zoocache import cacheable, add_deps
|
|
73
|
+
|
|
74
|
+
@cacheable
|
|
75
|
+
def get_product_page(product_id: int, store_id: int):
|
|
76
|
+
# This page stays cached as long as none of these change:
|
|
77
|
+
add_deps([
|
|
78
|
+
f"prod:{product_id}",
|
|
79
|
+
f"store:{store_id}:inv",
|
|
80
|
+
f"region:eu:pricing",
|
|
81
|
+
"campaign:blackfriday"
|
|
82
|
+
])
|
|
83
|
+
return render_page(product_id, store_id)
|
|
84
|
+
|
|
85
|
+
# Any of these will invalidate the page:
|
|
86
|
+
# invalidate("prod:42")
|
|
87
|
+
# invalidate("store:1:inv")
|
|
88
|
+
# invalidate("region:eu") -> Clears ALL prices in that region
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📖 Documentation
|
|
94
|
+
|
|
95
|
+
Explore the deep dives into Zoocache's architecture and features:
|
|
96
|
+
|
|
97
|
+
- [**Architecture Overview**](docs/architecture.md) - How the Rust core and Python wrapper interact.
|
|
98
|
+
- [**Hierarchical Invalidation**](docs/invalidation.md) - Deep dive into the PrefixTrie and O(D) invalidation.
|
|
99
|
+
- [**Serialization Pipeline**](docs/serialization.md) - Efficient data handling with MsgPack and LZ4.
|
|
100
|
+
- [**Concurrency & SingleFlight**](docs/concurrency.md) - Shielding your database from traffic spikes.
|
|
101
|
+
- [**Distributed Consistency**](docs/consistency.md) - HLC, Redis Bus, and robust consistency models.
|
|
102
|
+
- [**Reliability & Edge Cases**](docs/reliability.md) - Fail-fast mechanisms and memory management.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## ⚖️ Comparison
|
|
107
|
+
|
|
108
|
+
| Feature | **🐾 Zoocache** | **🔴 Redis (Raw)** | **🐶 Dogpile** | **diskcache** |
|
|
109
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
110
|
+
| **Invalidation** | 🧠 **Semantic (Trie)** | 🔧 Manual | 🔧 Manual | ⏳ TTL |
|
|
111
|
+
| **Consistency** | 🛡️ **Causal (HLC)** | ❌ Eventual | ❌ No | ❌ No |
|
|
112
|
+
| **Anti-Avalanche** | ✅ **Native** | ❌ No | ✅ Yes (Locks) | ❌ No |
|
|
113
|
+
| **Performance** | 🚀 **Very High** | 🏎️ High | 🐢 Medium | 🐢 Medium |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ❓ When to Use Zoocache
|
|
118
|
+
|
|
119
|
+
### ✅ Good Fit
|
|
120
|
+
- **Complex Data Relationships:** Use dependencies to invalidate groups of data.
|
|
121
|
+
- **High Read/Write Ratio:** Where TTL causes stale data or unnecessary cache churn.
|
|
122
|
+
- **Distributed Systems:** Native Redis Pub/Sub invalidation and HLC consistency.
|
|
123
|
+
- **Strict Consistency:** When users must see updates immediately (e.g., pricing, inventory).
|
|
124
|
+
|
|
125
|
+
### ❌ Not Ideal
|
|
126
|
+
- **Pure Time-Based Expiry:** If you only need simple TTL for session tokens.
|
|
127
|
+
- **Simple Key-Value:** If you don't need dependencies or hierarchical invalidation.
|
|
128
|
+
- **Minimal Dependencies:** For small, local-only apps where basic `lru_cache` suffices.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 📄 License
|
|
133
|
+
|
|
134
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
135
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
zoocache/__init__.py,sha256=sK8m3_oTFNhBgqNDH2Scoo3aNoPtKVQHPRdONCKa5OQ,250
|
|
2
|
+
zoocache/_zoocache.abi3.so,sha256=whMickXZxrELSHzYxkmtwSp9OIzb7TQE1-KtBJf8X9s,1952976
|
|
3
|
+
zoocache/context.py,sha256=qQakwfhwSauP9ZKTRy1oQnj-fPFjZZ34sIaeQgCuo6Q,848
|
|
4
|
+
zoocache/core.py,sha256=uQvoqn2jicXOkFb-ZfPmUVqmibr4nk_15rXUSvNVgyI,4775
|
|
5
|
+
zoocache-2026.1.20.dist-info/METADATA,sha256=3AXOXytuOtwkBWrZFo8XeMdXFCSz1q0MbF5JRSne6WI,5100
|
|
6
|
+
zoocache-2026.1.20.dist-info/WHEEL,sha256=vZ12AMAE5CVtd8oYbYGrz3omfHuIZCNO_3P50V00s00,104
|
|
7
|
+
zoocache-2026.1.20.dist-info/RECORD,,
|