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.
Files changed (38) hide show
  1. cachau-0.2.0/.gitignore +26 -0
  2. cachau-0.2.0/GUIDELINES.md +267 -0
  3. cachau-0.2.0/LICENSE +21 -0
  4. cachau-0.2.0/PKG-INFO +178 -0
  5. cachau-0.2.0/README.md +158 -0
  6. cachau-0.2.0/ROADMAP.md +107 -0
  7. cachau-0.2.0/VISION.md +150 -0
  8. cachau-0.2.0/pyproject.toml +35 -0
  9. cachau-0.2.0/src/cachau/__init__.py +25 -0
  10. cachau-0.2.0/src/cachau/backend.py +53 -0
  11. cachau-0.2.0/src/cachau/decorator.py +568 -0
  12. cachau-0.2.0/src/cachau/disk.py +193 -0
  13. cachau-0.2.0/src/cachau/durations.py +49 -0
  14. cachau-0.2.0/src/cachau/errors.py +25 -0
  15. cachau-0.2.0/src/cachau/explanation.py +98 -0
  16. cachau-0.2.0/src/cachau/fingerprint.py +169 -0
  17. cachau-0.2.0/src/cachau/flight.py +61 -0
  18. cachau-0.2.0/src/cachau/keys.py +216 -0
  19. cachau-0.2.0/src/cachau/memory.py +46 -0
  20. cachau-0.2.0/src/cachau/policy.py +62 -0
  21. cachau-0.2.0/src/cachau/sizes.py +101 -0
  22. cachau-0.2.0/src/cachau/stats.py +38 -0
  23. cachau-0.2.0/tests/test_backend.py +61 -0
  24. cachau-0.2.0/tests/test_cache.py +186 -0
  25. cachau-0.2.0/tests/test_data_hashing.py +118 -0
  26. cachau-0.2.0/tests/test_disk_backend.py +149 -0
  27. cachau-0.2.0/tests/test_durations.py +35 -0
  28. cachau-0.2.0/tests/test_explain.py +281 -0
  29. cachau-0.2.0/tests/test_fingerprint.py +105 -0
  30. cachau-0.2.0/tests/test_keying.py +119 -0
  31. cachau-0.2.0/tests/test_keys.py +125 -0
  32. cachau-0.2.0/tests/test_max_memory.py +226 -0
  33. cachau-0.2.0/tests/test_numba.py +448 -0
  34. cachau-0.2.0/tests/test_persistence.py +259 -0
  35. cachau-0.2.0/tests/test_single_flight.py +254 -0
  36. cachau-0.2.0/tests/test_sizes.py +86 -0
  37. cachau-0.2.0/tests/test_stats.py +341 -0
  38. cachau-0.2.0/tests/test_ttl.py +194 -0
@@ -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