cachau 0.2.0__tar.gz
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.
- cachau-0.2.0/.gitignore +26 -0
- cachau-0.2.0/GUIDELINES.md +267 -0
- cachau-0.2.0/LICENSE +21 -0
- cachau-0.2.0/PKG-INFO +178 -0
- cachau-0.2.0/README.md +158 -0
- cachau-0.2.0/ROADMAP.md +107 -0
- cachau-0.2.0/VISION.md +150 -0
- cachau-0.2.0/pyproject.toml +35 -0
- cachau-0.2.0/src/cachau/__init__.py +25 -0
- cachau-0.2.0/src/cachau/backend.py +53 -0
- cachau-0.2.0/src/cachau/decorator.py +568 -0
- cachau-0.2.0/src/cachau/disk.py +193 -0
- cachau-0.2.0/src/cachau/durations.py +49 -0
- cachau-0.2.0/src/cachau/errors.py +25 -0
- cachau-0.2.0/src/cachau/explanation.py +98 -0
- cachau-0.2.0/src/cachau/fingerprint.py +169 -0
- cachau-0.2.0/src/cachau/flight.py +61 -0
- cachau-0.2.0/src/cachau/keys.py +216 -0
- cachau-0.2.0/src/cachau/memory.py +46 -0
- cachau-0.2.0/src/cachau/policy.py +62 -0
- cachau-0.2.0/src/cachau/sizes.py +101 -0
- cachau-0.2.0/src/cachau/stats.py +38 -0
- cachau-0.2.0/tests/test_backend.py +61 -0
- cachau-0.2.0/tests/test_cache.py +186 -0
- cachau-0.2.0/tests/test_data_hashing.py +118 -0
- cachau-0.2.0/tests/test_disk_backend.py +149 -0
- cachau-0.2.0/tests/test_durations.py +35 -0
- cachau-0.2.0/tests/test_explain.py +281 -0
- cachau-0.2.0/tests/test_fingerprint.py +105 -0
- cachau-0.2.0/tests/test_keying.py +119 -0
- cachau-0.2.0/tests/test_keys.py +125 -0
- cachau-0.2.0/tests/test_max_memory.py +226 -0
- cachau-0.2.0/tests/test_numba.py +448 -0
- cachau-0.2.0/tests/test_persistence.py +259 -0
- cachau-0.2.0/tests/test_single_flight.py +254 -0
- cachau-0.2.0/tests/test_sizes.py +86 -0
- cachau-0.2.0/tests/test_stats.py +341 -0
- cachau-0.2.0/tests/test_ttl.py +194 -0
cachau-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Internal design documents (source material, not part of the published repo)
|
|
2
|
+
*.docx
|
|
3
|
+
|
|
4
|
+
# Python
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Cachau's own default cache dir (used in docs/examples)
|
|
21
|
+
.cache/
|
|
22
|
+
.cachau/
|
|
23
|
+
|
|
24
|
+
# OS / editor noise
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Cachau — Design & Engineering Guidelines
|
|
2
|
+
|
|
3
|
+
These guidelines define how Cachau must behave and how contributions should be designed. They exist so that every feature stays consistent with the project's core promise: **correct, bounded, observable, explainable caching for data workloads.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. API design
|
|
8
|
+
|
|
9
|
+
- The common case must be minimal:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
@cache
|
|
13
|
+
def expensive(x):
|
|
14
|
+
...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Configuration is declarative and progressive:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
@cache(ttl="10m", max_memory="1GB", persist=True)
|
|
21
|
+
def expensive(x):
|
|
22
|
+
...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Complexity appears only as the user needs it. Backend objects, external configuration, serializers, and config files must **never** be required for basic usage.
|
|
26
|
+
|
|
27
|
+
- Per-function secondary interface:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
func.cache.stats()
|
|
31
|
+
func.cache.clear()
|
|
32
|
+
func.cache.invalidate(...)
|
|
33
|
+
func.cache.inspect()
|
|
34
|
+
func.cache.explain(...)
|
|
35
|
+
func.cache.profile(...)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Global operations:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
cache.clear()
|
|
42
|
+
cache.stats()
|
|
43
|
+
cache.configure(...)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 2. Cache identity and keys
|
|
47
|
+
|
|
48
|
+
A correct key conceptually represents:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
function identity + implementation/version + normalized arguments + relevant external dependencies
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Semantically equivalent calls must share a key. Normalize arguments through signature binding (kwargs vs. positional must not fragment the cache).
|
|
55
|
+
- Optimized, native hashing for: `numpy.ndarray`, `pandas.DataFrame`/`Series`, Polars, `pathlib.Path`, dataclasses, `dict`/`list`/`tuple`, and primitives.
|
|
56
|
+
- **Never blindly serialize a huge object just to compute a key.**
|
|
57
|
+
- Explicit keying must be supported:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
@cache(key=lambda dataset, version: version)
|
|
61
|
+
def process(dataset, version):
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- Explicit exclusion of irrelevant arguments:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
@cache(ignore=["logger", "progress_callback"])
|
|
69
|
+
def run(data, logger=None, progress_callback=None):
|
|
70
|
+
...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **Never silently ignore unhashable arguments.** Fail loudly or require explicit handling.
|
|
74
|
+
|
|
75
|
+
## 3. Namespaces
|
|
76
|
+
|
|
77
|
+
- Different functions must never collide, even with identical arguments. Conceptual identity: `package.module.function + function fingerprint + arguments`.
|
|
78
|
+
- Stable namespaces allow refactors without unintended invalidation:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
@cache(namespace="features.v2")
|
|
82
|
+
def build_features(...):
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 4. TTL
|
|
87
|
+
|
|
88
|
+
- Optional; accepts simple readable values: `ttl=60`, `"30s"`, `"10m"`, `"2h"`, `"7d"`.
|
|
89
|
+
- TTL starts when the entry was **computed and committed**, not when execution began.
|
|
90
|
+
- Expiration is **lazy** by default: check on access; if expired, treat as miss and remove/mark stale. No background workers unless a real need is demonstrated.
|
|
91
|
+
|
|
92
|
+
## 5. Memory limits and eviction
|
|
93
|
+
|
|
94
|
+
- Bounded resources are a core feature: `@cache(max_memory="1GB")` or `cache.configure(max_memory="4GB")`.
|
|
95
|
+
- Document precisely what "size" means (serialized size, approximate in-memory size, deep size, or physical backend size).
|
|
96
|
+
- **LRU** is the initial policy — understandable and predictable. Do not add more policies before demonstrated need.
|
|
97
|
+
- If a single entry exceeds total capacity: compute the result, return it, **do not cache it**, and record `cache_skip_oversized`. Never flush the whole cache to fit one pathological entry.
|
|
98
|
+
|
|
99
|
+
## 6. Persistence
|
|
100
|
+
|
|
101
|
+
- Trivial to enable: `@cache(persist=True)` or `@cache(persist="./.cache")`.
|
|
102
|
+
- Must survive interpreter, notebook, and process restarts.
|
|
103
|
+
- Writes are **atomic**: serialize to temp file → sync where appropriate → atomic rename.
|
|
104
|
+
- Per-entry metadata: key, `created_at`, `expires_at`, `last_access`, size, serializer, function identity, function fingerprint, format version.
|
|
105
|
+
- The persistent format has **explicit versioning**. Incompatibility degrades to a miss or controlled invalidation — never mysterious errors.
|
|
106
|
+
|
|
107
|
+
## 7. Invalidation
|
|
108
|
+
|
|
109
|
+
Invalidation is a first-class concept:
|
|
110
|
+
|
|
111
|
+
- Per function: `expensive.cache.clear()`
|
|
112
|
+
- Per invocation: `expensive.cache.invalidate(x=1)`
|
|
113
|
+
- Global: `cache.clear()`
|
|
114
|
+
- **Changing a function's implementation invalidates previous results by default.** A result produced by `return x * 2` must not be reused after the code becomes `return x * 3`.
|
|
115
|
+
- External dependency invalidation:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@cache(depends_on=["data.csv"])
|
|
119
|
+
def load_data():
|
|
120
|
+
...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Possible fingerprints: mtime, size, content hash, environment variable, package version, user-defined token.
|
|
124
|
+
|
|
125
|
+
- Automatic detection is **conservative**. Explicitness is preferable to unreliable magic.
|
|
126
|
+
|
|
127
|
+
## 8. Observability and metrics
|
|
128
|
+
|
|
129
|
+
A cache that is hard to observe is hard to trust.
|
|
130
|
+
|
|
131
|
+
- `func.cache.stats()` reports: hits, misses, hit rate, writes, skipped writes, expirations, invalidations, evictions, serialization/deserialization errors, entry count, current bytes, computation time, estimated time saved.
|
|
132
|
+
- Misses must distinguish causes: `miss_not_found`, `miss_expired`, `miss_invalidated`, `miss_code_changed`, `miss_dependency_changed`, `miss_corrupt`.
|
|
133
|
+
- `func.cache.explain(args)` answers: HIT or MISS? Why? Which dependency changed? Did the code change? How big is it? How much TTL remains? What was invalidated or evicted?
|
|
134
|
+
- Metrics observe without changing behavior.
|
|
135
|
+
|
|
136
|
+
## 9. Serialization
|
|
137
|
+
|
|
138
|
+
- Extensible but invisible in normal cases. Priorities: Python types → NumPy → pandas → common objects.
|
|
139
|
+
- Strategies: pickle/cloudpickle, native NumPy formats, Arrow/Parquet, custom serializers.
|
|
140
|
+
- **Correctness beats micro-optimization.**
|
|
141
|
+
- If the function computes correctly but serialization fails: return the result, record `cache_write_error`, optionally warn. The cache is an optimization, not a correctness dependency.
|
|
142
|
+
|
|
143
|
+
## 10. Concurrency: single-flight
|
|
144
|
+
|
|
145
|
+
- Multiple concurrent callers for the same absent key should not all compute:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Caller A → MISS → computes
|
|
149
|
+
Caller B → same key → waits
|
|
150
|
+
Caller C → same key → waits
|
|
151
|
+
A commits the result
|
|
152
|
+
B/C reuse the result
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- Synchronization is **per key**. Never a global lock that serializes independent work.
|
|
156
|
+
|
|
157
|
+
## 11. Purity, exceptions, and mutability
|
|
158
|
+
|
|
159
|
+
- Exceptions are **not cached** by default.
|
|
160
|
+
- Correctness assumes the result is determined by the key and declared dependencies. Do not try to magically detect nondeterminism; random seeds should be explicit arguments.
|
|
161
|
+
- Functions that mutate inputs in-place cannot be treated as conventional memoization. Default contract: functions are pure with respect to their key. Mutation: reject when detectable, require opt-in, or document as outside the standard guarantee.
|
|
162
|
+
- Define what happens when a caller mutates a mutable result retrieved from the cache (especially for the in-memory backend).
|
|
163
|
+
|
|
164
|
+
## 12. Notebook experience
|
|
165
|
+
|
|
166
|
+
Notebooks are a primary target:
|
|
167
|
+
|
|
168
|
+
- `func.cache.stats() / clear() / inspect() / explain(...)` must feel natural in a cell.
|
|
169
|
+
- Re-running a cell must not unexpectedly destroy a useful persistent cache.
|
|
170
|
+
- Changing the code **should** invalidate stale results, in an understandable way.
|
|
171
|
+
|
|
172
|
+
## 13. Architecture
|
|
173
|
+
|
|
174
|
+
Conceptual layers:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Decorator / Public API
|
|
178
|
+
→ Invocation Normalization
|
|
179
|
+
→ Key Builder
|
|
180
|
+
→ Dependency Fingerprinting
|
|
181
|
+
→ Cache Policy
|
|
182
|
+
→ Storage Backend
|
|
183
|
+
→ Serializer
|
|
184
|
+
→ Metrics / Events
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Separation of responsibilities:
|
|
188
|
+
|
|
189
|
+
- The decorator does not implement storage.
|
|
190
|
+
- The backend does not decide function semantics.
|
|
191
|
+
- The serializer does not decide invalidation.
|
|
192
|
+
- Metrics observe without changing behavior.
|
|
193
|
+
|
|
194
|
+
Minimal backend interface:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
class CacheBackend:
|
|
198
|
+
def get(key): ...
|
|
199
|
+
def set(key, value, metadata): ...
|
|
200
|
+
def delete(key): ...
|
|
201
|
+
def clear(namespace=None): ...
|
|
202
|
+
def iter_entries(...): ...
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
V1 ships `MemoryBackend` + `DiskBackend`. Redis, S3, databases, and distributed caching stay out until the local model is consolidated.
|
|
206
|
+
|
|
207
|
+
## 14. Numba support
|
|
208
|
+
|
|
209
|
+
Numba is a **first-class workload**:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
@cache(ttl="1h", max_memory="4GB", persist=True)
|
|
213
|
+
@njit
|
|
214
|
+
def simulate(values, iterations):
|
|
215
|
+
...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- Cachau operates at the **Python → Numba dispatcher boundary**. The initial goal is *not* making `@cache` callable from nopython mode — it is caching Numba workloads correctly, fast, and transparently.
|
|
219
|
+
- **Dispatcher identity**: derived from the original Python function, semantically relevant compile options (`fastmath`, `parallel`, `boundscheck`, `error_model`), defaults, and namespace/version — never `repr()` or memory identity. Changing semantically relevant configuration invalidates stale persisted results.
|
|
220
|
+
- **Compilation cache vs. result cache** are independent: Numba `cache=True` caches machine code; Cachau caches computed results. They coexist — a Cachau HIT skips execution; a MISS still benefits from Numba's compilation cache.
|
|
221
|
+
- **Honest metrics**: distinguish cold JIT vs. warm JIT. Measure lookup, key generation, compile/dispatch, execution, serialization, and deserialization separately. Never repeatedly count one-time compilation cost as normal execution cost. Standard benchmark: compile → warm up → benchmark; report cold JIT separately.
|
|
222
|
+
- **ndarray identity** includes at minimum dtype, shape, and content; strides/memory order only when semantically relevant. Equal bytes with different dtype or shape must not collide. C/F/non-contiguous layout must not fragment the cache unless it affects observable semantics — identity follows semantics, not compiler internals.
|
|
223
|
+
- **Advanced types** (`numba.typed.List`, `typed.Dict`, jitclass, extension types): support is explicit and progressive. Do not declare support without deterministic hashing, serialization round-trip, stability tests, and benchmarks. For jitclass, prefer explicit keys or field-based adapters over compiler internals.
|
|
224
|
+
- `parallel=True` works at the Python boundary without Cachau altering thread configuration. Do not needlessly bind persisted results to CPU/LLVM/machine code — prefer semantic identity over compilation-artifact identity.
|
|
225
|
+
|
|
226
|
+
## 15. Cache economics
|
|
227
|
+
|
|
228
|
+
The fundamental rule:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
T_key + T_lookup + T_deserialize < T_recompute
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Example: a Python function takes 10 s; with Numba it takes 50 ms; hashing a 2 GB array takes 800 ms — caching makes it *worse*. Cachau should measure (or make it easy to measure): keying cost, lookup, deserialize, warm recomputation, net time saved. `cache.profile()` embodies this philosophy: not just caching, but explaining when caching is a bad decision.
|
|
235
|
+
|
|
236
|
+
## 16. Testing requirements
|
|
237
|
+
|
|
238
|
+
Mandatory semantic tests:
|
|
239
|
+
|
|
240
|
+
- HIT after first computation; MISS on different arguments
|
|
241
|
+
- kwargs/positional normalization
|
|
242
|
+
- TTL expiration; TTL starts after computation
|
|
243
|
+
- Memory eviction; oversized-entry skip
|
|
244
|
+
- Persistence across processes; corruption recovery
|
|
245
|
+
- Invalidation: code change, dependency change, manual
|
|
246
|
+
- Same-key concurrency (single-flight)
|
|
247
|
+
- Serialization failure fallback
|
|
248
|
+
- `stats()`, `clear()`, namespace isolation
|
|
249
|
+
|
|
250
|
+
Numba-specific tests:
|
|
251
|
+
|
|
252
|
+
- Warm/cold JIT distinction
|
|
253
|
+
- dtype/shape/layout identity
|
|
254
|
+
- `fastmath` and `parallel` variants
|
|
255
|
+
- Large arrays
|
|
256
|
+
- Typed containers (once supported)
|
|
257
|
+
- Cross-process persistence
|
|
258
|
+
- Keying cost vs. recomputation
|
|
259
|
+
|
|
260
|
+
## 17. Feature acceptance bar
|
|
261
|
+
|
|
262
|
+
Every new feature must justify its complexity through real use cases. When evaluating a proposal, ask:
|
|
263
|
+
|
|
264
|
+
1. Does it preserve **correctness before hit rate**?
|
|
265
|
+
2. Can the system still **explain** what happened?
|
|
266
|
+
3. Is the behavior **predictable and bounded**?
|
|
267
|
+
4. Is it needed for the narrow mission — *"a pleasant, robust function cache for expensive Python data workloads"* — or is it scope creep toward an anti-goal (distributed cache, workflow engine, artifact registry)?
|
cachau-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolás Seijas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cachau-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cachau
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Delightful, observable, bounded, and persistent function caching for Python data workloads.
|
|
5
|
+
Project-URL: Repository, https://github.com/nicoseijas/Cachau
|
|
6
|
+
Author-email: Nicolás Seijas <nicoseijas@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cache,caching,data-science,memoization,numba,numpy,pandas
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Cachau
|
|
22
|
+
|
|
23
|
+
**Delightful, observable, bounded, and persistent function caching for Python data workloads.**
|
|
24
|
+
|
|
25
|
+
Cachau is a function cache designed around the real problems of data science: large arguments, expensive computations, notebooks that restart, voluminous results, invalidation when code or data changes, and explicit memory and disk limits.
|
|
26
|
+
|
|
27
|
+
> Say ciao to recomputation.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from cachau import cache
|
|
31
|
+
|
|
32
|
+
@cache(ttl="1h", persist=True, max_memory="2GB")
|
|
33
|
+
def expensive_analysis(df, config):
|
|
34
|
+
...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Status: v0.2.0 — the core engine plus validated Numba Level A support** (236 tests): normalized keys with type-tagged hashing (incl. closure captures), native NumPy/pandas identity, `key=`/`ignore=` escape hatches, code-change invalidation, TTL, LRU memory bounds, atomic corruption-safe persistence, same-key single-flight, `stats()` with miss reasons and cold/warm JIT accounting, and `explain()`. Pre-1.0, so the API may still evolve. Next up: `depends_on=` dependency invalidation and `profile()` (see [ROADMAP](ROADMAP.md)).
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install cachau
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why not just `functools.lru_cache` / joblib / diskcache?
|
|
46
|
+
|
|
47
|
+
Plenty of libraries offer TTL, persistence, or LRU. None of them combine what data workloads actually need:
|
|
48
|
+
|
|
49
|
+
| Problem | Cachau's answer |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Hashing a 2 GB DataFrame just to build a key | Native, optimized hashing for NumPy, pandas, and Polars — plus explicit `key=` / `ignore=` escape hatches |
|
|
52
|
+
| Stale results after you edit the function | Code-fingerprint invalidation by default: change `x * 2` to `x * 3`, the old result dies |
|
|
53
|
+
| Results that outlive the data they came from | `depends_on=["data.csv"]` — invalidate on file, env var, or package changes |
|
|
54
|
+
| Caches that eat all your RAM or disk | First-class `max_memory` bounds with predictable LRU eviction |
|
|
55
|
+
| Notebook restarts throwing work away | `persist=True` — atomic, versioned on-disk format that survives restarts |
|
|
56
|
+
| "Why was that a miss?!" | `func.cache.explain(...)` tells you exactly what happened and why |
|
|
57
|
+
| Caching that silently makes things *slower* | `func.cache.profile(...)` measures whether caching is even worth it |
|
|
58
|
+
| Numba treated as an afterthought | First-class support at the dispatcher boundary — `fastmath`/`parallel`-aware identity, honest cold/warm JIT metrics |
|
|
59
|
+
|
|
60
|
+
## A taste of the API
|
|
61
|
+
|
|
62
|
+
The common case is one decorator, zero configuration:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
@cache
|
|
66
|
+
def load_dataset(path):
|
|
67
|
+
return pd.read_parquet(path)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Configuration is declarative and progressive — no backend objects, no config files:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
@cache(ttl="1h")
|
|
74
|
+
def build_features(df, config):
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
@cache(persist=True)
|
|
78
|
+
def train_embedding(dataset_hash, params):
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
@cache(max_memory="2GB")
|
|
82
|
+
def expensive_simulation(seed, params):
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
@cache(ignore=["logger", "progress_callback"])
|
|
86
|
+
def run(data, logger=None, progress_callback=None):
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
@cache(key=lambda dataset, version: version)
|
|
90
|
+
def process(dataset, version):
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Coming in V1.1: `@cache(depends_on=["data/train.parquet"])` — invalidation when files, environment variables, or package versions change.
|
|
95
|
+
|
|
96
|
+
Every cached function carries its own control surface:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
build_features.cache.stats() # hits, misses, hit rate, bytes, time saved...
|
|
100
|
+
build_features.cache.clear()
|
|
101
|
+
build_features.cache.invalidate(df, config)
|
|
102
|
+
build_features.cache.explain(df, config)
|
|
103
|
+
build_features.cache.profile(df, config)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `explain()` — transparency on demand
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
MISS
|
|
110
|
+
Reason: expired
|
|
111
|
+
Namespace: features.build_features
|
|
112
|
+
Created: 2026-07-19 14:03:11 UTC
|
|
113
|
+
Expired: 3m 2s ago (at 2026-07-19 15:03:11 UTC)
|
|
114
|
+
Size: 1.2 MB
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
(V1.1 adds dependency answers: *which file changed, previous vs. current fingerprint*.)
|
|
118
|
+
|
|
119
|
+
### `profile()` — is caching even worth it? *(planned — V1.1)*
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Cache suitability
|
|
123
|
+
|
|
124
|
+
Computation (warm Numba): 182 ms
|
|
125
|
+
Key generation: 347 ms
|
|
126
|
+
Cache lookup: 2 ms
|
|
127
|
+
Deserialization: 41 ms
|
|
128
|
+
--------------------------------------
|
|
129
|
+
Cache hit total: 390 ms
|
|
130
|
+
|
|
131
|
+
Caching is slower than recomputation by ~2.1x.
|
|
132
|
+
Primary cause: hashing ndarray[float64, 480 MB]
|
|
133
|
+
Recommendation: provide an explicit stable key or dataset version.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Cachau doesn't just cache — it tells you when caching is a bad decision.
|
|
137
|
+
|
|
138
|
+
## First-class Numba support
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from numba import njit
|
|
142
|
+
from cachau import cache
|
|
143
|
+
|
|
144
|
+
@cache(ttl="1h", max_memory="4GB", persist=True)
|
|
145
|
+
@njit
|
|
146
|
+
def simulate(values, iterations):
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Cachau caches **results** at the Python → dispatcher boundary (`@cache` goes below `@njit`); Numba's `cache=True` caches **machine code**. They compose: a Cachau HIT skips execution entirely, and a MISS still benefits from Numba's compilation cache. Dispatcher identity covers the Python function, closure captures, and semantically relevant compile options (`fastmath`, `parallel`, `boundscheck`, `error_model`, `locals=` type forcing) — changing any of them invalidates stale results. Metrics are honest about JIT: each specialization's first compile is reported as `cold_compute_seconds` and never counted as normal execution cost. Validated by a 26-test matrix.
|
|
151
|
+
|
|
152
|
+
## Design principles
|
|
153
|
+
|
|
154
|
+
1. **Correctness before hit rate.** A false HIT is worse than a MISS. When in doubt, recompute.
|
|
155
|
+
2. **Safe by default.** Exceptions aren't cached; serialization failure never loses your result; corruption degrades to a miss, never a mysterious error.
|
|
156
|
+
3. **Observable before clever.** Every hit, miss, eviction, and skip has an inspectable reason code.
|
|
157
|
+
4. **No hidden magic.** Automatic detection is conservative; explicitness beats unreliable cleverness.
|
|
158
|
+
5. **Bounded by design.** Memory and disk limits are core features, not afterthoughts.
|
|
159
|
+
|
|
160
|
+
## What Cachau is *not*
|
|
161
|
+
|
|
162
|
+
Not Redis, not a distributed cache, not a workflow engine, not an artifact registry, not an experiment tracker, not a joblib/Dask replacement. The scope stays narrow on purpose:
|
|
163
|
+
|
|
164
|
+
> *A pleasant, robust function cache for expensive Python data workloads.*
|
|
165
|
+
|
|
166
|
+
## Documentation
|
|
167
|
+
|
|
168
|
+
- **[VISION.md](VISION.md)** — why Cachau exists, positioning, and guiding maxims
|
|
169
|
+
- **[ROADMAP.md](ROADMAP.md)** — phased plan from foundations to Numba Level B
|
|
170
|
+
- **[GUIDELINES.md](GUIDELINES.md)** — the full design & engineering spec (API, cache identity, TTL, eviction, persistence, invalidation, observability, concurrency, Numba, testing)
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
The core engine is young and feedback is the most valuable contribution: try it on a real workload and open an issue with what surprised you. Bug reports with a failing test are gold. Before proposing features, read [GUIDELINES.md](GUIDELINES.md), especially the feature acceptance bar: every addition must preserve correctness, explainability, and the narrow mission.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
cachau-0.2.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Cachau
|
|
2
|
+
|
|
3
|
+
**Delightful, observable, bounded, and persistent function caching for Python data workloads.**
|
|
4
|
+
|
|
5
|
+
Cachau is a function cache designed around the real problems of data science: large arguments, expensive computations, notebooks that restart, voluminous results, invalidation when code or data changes, and explicit memory and disk limits.
|
|
6
|
+
|
|
7
|
+
> Say ciao to recomputation.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from cachau import cache
|
|
11
|
+
|
|
12
|
+
@cache(ttl="1h", persist=True, max_memory="2GB")
|
|
13
|
+
def expensive_analysis(df, config):
|
|
14
|
+
...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Status: v0.2.0 — the core engine plus validated Numba Level A support** (236 tests): normalized keys with type-tagged hashing (incl. closure captures), native NumPy/pandas identity, `key=`/`ignore=` escape hatches, code-change invalidation, TTL, LRU memory bounds, atomic corruption-safe persistence, same-key single-flight, `stats()` with miss reasons and cold/warm JIT accounting, and `explain()`. Pre-1.0, so the API may still evolve. Next up: `depends_on=` dependency invalidation and `profile()` (see [ROADMAP](ROADMAP.md)).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install cachau
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why not just `functools.lru_cache` / joblib / diskcache?
|
|
26
|
+
|
|
27
|
+
Plenty of libraries offer TTL, persistence, or LRU. None of them combine what data workloads actually need:
|
|
28
|
+
|
|
29
|
+
| Problem | Cachau's answer |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Hashing a 2 GB DataFrame just to build a key | Native, optimized hashing for NumPy, pandas, and Polars — plus explicit `key=` / `ignore=` escape hatches |
|
|
32
|
+
| Stale results after you edit the function | Code-fingerprint invalidation by default: change `x * 2` to `x * 3`, the old result dies |
|
|
33
|
+
| Results that outlive the data they came from | `depends_on=["data.csv"]` — invalidate on file, env var, or package changes |
|
|
34
|
+
| Caches that eat all your RAM or disk | First-class `max_memory` bounds with predictable LRU eviction |
|
|
35
|
+
| Notebook restarts throwing work away | `persist=True` — atomic, versioned on-disk format that survives restarts |
|
|
36
|
+
| "Why was that a miss?!" | `func.cache.explain(...)` tells you exactly what happened and why |
|
|
37
|
+
| Caching that silently makes things *slower* | `func.cache.profile(...)` measures whether caching is even worth it |
|
|
38
|
+
| Numba treated as an afterthought | First-class support at the dispatcher boundary — `fastmath`/`parallel`-aware identity, honest cold/warm JIT metrics |
|
|
39
|
+
|
|
40
|
+
## A taste of the API
|
|
41
|
+
|
|
42
|
+
The common case is one decorator, zero configuration:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
@cache
|
|
46
|
+
def load_dataset(path):
|
|
47
|
+
return pd.read_parquet(path)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Configuration is declarative and progressive — no backend objects, no config files:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
@cache(ttl="1h")
|
|
54
|
+
def build_features(df, config):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@cache(persist=True)
|
|
58
|
+
def train_embedding(dataset_hash, params):
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@cache(max_memory="2GB")
|
|
62
|
+
def expensive_simulation(seed, params):
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@cache(ignore=["logger", "progress_callback"])
|
|
66
|
+
def run(data, logger=None, progress_callback=None):
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
@cache(key=lambda dataset, version: version)
|
|
70
|
+
def process(dataset, version):
|
|
71
|
+
...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Coming in V1.1: `@cache(depends_on=["data/train.parquet"])` — invalidation when files, environment variables, or package versions change.
|
|
75
|
+
|
|
76
|
+
Every cached function carries its own control surface:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
build_features.cache.stats() # hits, misses, hit rate, bytes, time saved...
|
|
80
|
+
build_features.cache.clear()
|
|
81
|
+
build_features.cache.invalidate(df, config)
|
|
82
|
+
build_features.cache.explain(df, config)
|
|
83
|
+
build_features.cache.profile(df, config)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `explain()` — transparency on demand
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
MISS
|
|
90
|
+
Reason: expired
|
|
91
|
+
Namespace: features.build_features
|
|
92
|
+
Created: 2026-07-19 14:03:11 UTC
|
|
93
|
+
Expired: 3m 2s ago (at 2026-07-19 15:03:11 UTC)
|
|
94
|
+
Size: 1.2 MB
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
(V1.1 adds dependency answers: *which file changed, previous vs. current fingerprint*.)
|
|
98
|
+
|
|
99
|
+
### `profile()` — is caching even worth it? *(planned — V1.1)*
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Cache suitability
|
|
103
|
+
|
|
104
|
+
Computation (warm Numba): 182 ms
|
|
105
|
+
Key generation: 347 ms
|
|
106
|
+
Cache lookup: 2 ms
|
|
107
|
+
Deserialization: 41 ms
|
|
108
|
+
--------------------------------------
|
|
109
|
+
Cache hit total: 390 ms
|
|
110
|
+
|
|
111
|
+
Caching is slower than recomputation by ~2.1x.
|
|
112
|
+
Primary cause: hashing ndarray[float64, 480 MB]
|
|
113
|
+
Recommendation: provide an explicit stable key or dataset version.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Cachau doesn't just cache — it tells you when caching is a bad decision.
|
|
117
|
+
|
|
118
|
+
## First-class Numba support
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from numba import njit
|
|
122
|
+
from cachau import cache
|
|
123
|
+
|
|
124
|
+
@cache(ttl="1h", max_memory="4GB", persist=True)
|
|
125
|
+
@njit
|
|
126
|
+
def simulate(values, iterations):
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Cachau caches **results** at the Python → dispatcher boundary (`@cache` goes below `@njit`); Numba's `cache=True` caches **machine code**. They compose: a Cachau HIT skips execution entirely, and a MISS still benefits from Numba's compilation cache. Dispatcher identity covers the Python function, closure captures, and semantically relevant compile options (`fastmath`, `parallel`, `boundscheck`, `error_model`, `locals=` type forcing) — changing any of them invalidates stale results. Metrics are honest about JIT: each specialization's first compile is reported as `cold_compute_seconds` and never counted as normal execution cost. Validated by a 26-test matrix.
|
|
131
|
+
|
|
132
|
+
## Design principles
|
|
133
|
+
|
|
134
|
+
1. **Correctness before hit rate.** A false HIT is worse than a MISS. When in doubt, recompute.
|
|
135
|
+
2. **Safe by default.** Exceptions aren't cached; serialization failure never loses your result; corruption degrades to a miss, never a mysterious error.
|
|
136
|
+
3. **Observable before clever.** Every hit, miss, eviction, and skip has an inspectable reason code.
|
|
137
|
+
4. **No hidden magic.** Automatic detection is conservative; explicitness beats unreliable cleverness.
|
|
138
|
+
5. **Bounded by design.** Memory and disk limits are core features, not afterthoughts.
|
|
139
|
+
|
|
140
|
+
## What Cachau is *not*
|
|
141
|
+
|
|
142
|
+
Not Redis, not a distributed cache, not a workflow engine, not an artifact registry, not an experiment tracker, not a joblib/Dask replacement. The scope stays narrow on purpose:
|
|
143
|
+
|
|
144
|
+
> *A pleasant, robust function cache for expensive Python data workloads.*
|
|
145
|
+
|
|
146
|
+
## Documentation
|
|
147
|
+
|
|
148
|
+
- **[VISION.md](VISION.md)** — why Cachau exists, positioning, and guiding maxims
|
|
149
|
+
- **[ROADMAP.md](ROADMAP.md)** — phased plan from foundations to Numba Level B
|
|
150
|
+
- **[GUIDELINES.md](GUIDELINES.md)** — the full design & engineering spec (API, cache identity, TTL, eviction, persistence, invalidation, observability, concurrency, Numba, testing)
|
|
151
|
+
|
|
152
|
+
## Contributing
|
|
153
|
+
|
|
154
|
+
The core engine is young and feedback is the most valuable contribution: try it on a real workload and open an issue with what surprised you. Bug reports with a failing test are gold. Before proposing features, read [GUIDELINES.md](GUIDELINES.md), especially the feature acceptance bar: every addition must preserve correctness, explainability, and the narrow mission.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|