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 ADDED
@@ -0,0 +1,13 @@
1
+ from .core import cacheable, invalidate, version, clear, configure, prune, _reset
2
+ from .context import add_deps
3
+
4
+ __all__ = [
5
+ "cacheable",
6
+ "invalidate",
7
+ "version",
8
+ "add_deps",
9
+ "clear",
10
+ "configure",
11
+ "prune",
12
+ "_reset",
13
+ ]
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp310-abi3-macosx_11_0_arm64