Metadata-Version: 2.3
Name: belljar
Version: 0.4.0
Summary: Extremely simple to use mid-execution memoization library.
Keywords: cache,memoization,decorator,dill,persistence,hashing
Author: Wannes Vantorre
Author-email: Wannes Vantorre <vantorrewannes@gmail.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: blake3>=1.0.9
Requires-Dist: dill>=0.4.1
Requires-Python: >=3.14
Project-URL: Homepage, https://codeberg.org/ProductionCode/belljar/belljar
Project-URL: Repository, https://codeberg.org/ProductionCode/belljar/belljar
Project-URL: Issues, https://codeberg.org/ProductionCode/belljar/issues
Description-Content-Type: text/markdown

# belljar 🫙

**Mid-execution memoization for dynamic state.**

Standard caching fails when your function relies on hidden or changing state (like a database cursor or an open file). `belljar` solves this by letting you build the cache key _while_ your code runs — and keep it, as a value, for later.

```bash
uv add belljar
```

## The Difference

| Concept                    | `functools.lru_cache`            | `belljar`                                        |
| :------------------------- | :------------------------------- | :----------------------------------------------- |
| **When is it checked?**    | _Before_ the function runs.      | _Mid-execution_, exactly when you tell it to.    |
| **What defines identity?** | Static function arguments.       | Whatever runtime state you fold in, in order.    |
| **Handling Mutable State** | Fails. Returns stale/wrong data. | Succeeds. Hashes current state dynamically.      |
| **Where can you retrieve?**| Only by calling again.           | Anywhere that folds the same trace.              |

## Usage

A `Jar` is a persistent store plus a cursor: it opens at a fresh identity, and `include()` advances it by folding runtime state in. `get()` and `set()` read and write the value at the current position (`get()` returns `None` when nothing is sealed there).

```python
from belljar import Jar
import io

# A simulated file. The object stays the same, but its internal cursor moves.
log_file = io.StringIO("chunk1 chunk2 chunk3")

def process_chunk(file_handle):
    jar = Jar()  # caches under ./.jar by default; pass any path to move it
    jar.include(process_chunk.__code__)    # fold this function's code: edits invalidate
    jar.include(file_handle.tell())        # fold the file's exact runtime cursor position

    # If we've processed from this exact position before, skip the work
    if (cached := jar.get()) is not None:
        return cached

    # Otherwise, do the heavy processing and seal the result
    print("Doing heavy work...")
    return jar.set(file_handle.read(6))

process_chunk(log_file)  # Reads "chunk1", saves to disk. (Takes time)
process_chunk(log_file)  # Reads "chunk2", saves to disk. (Takes time)

log_file.seek(0)
process_chunk(log_file)  # Cursor is back at 0 → instantly returns cached "chunk1"
```

No decorators, no changed return types — a cache hit is a plain `if`.

## Identity Is Reached, Not Given

Identities are deterministic: folding the same state in the same order always arrives at the same position. So the place that seals a value and the place that retrieves it don't have to share anything but the trace — factor it into a function and both sides reach the same entry:

```python
class KVStore:
    def _entry(self, key):
        jar = Jar("./cache")
        jar.include(KVStore._entry.__code__)
        jar.include(key)
        return jar

    def add(self, key, value):
        self._entry(key).set(value)

    def get(self, key):
        return self._entry(key).get()
```

## Core Mechanics

- **Identity is a trace.** The cursor is a running hash: every `include` chains onto everything folded before it, in order. `include(a); include(b)` and `include(b); include(a)` are different identities — the key mirrors the path your execution took.
- **Invalidation is inclusion.** Fold whatever governs a value's lifetime: `func.__code__` to invalidate on edit, a schema version, a mtime. If it's part of the identity, changing it is invalidation.
- **Disk Persistent:** Caches survive restarts. Identities are deterministic, so a fresh process folding the same state finds the same entries.
- **Deep Serialization:** Powered by `dill` (not `pickle`), meaning it safely handles lambdas, nested classes, and complex closures — both as folded state and as sealed values.
- **Concurrency:** A `Jar` is a lightweight cursor over a shared store — construction is just a path and a hash seed, so make one per call or task instead of sharing a cursor.
