Metadata-Version: 2.3
Name: belljar
Version: 0.2.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 — identities are storable strings.      |

## Usage

A `Jar` is a persistent store plus a cursor. `include()` advances the cursor by folding runtime state into the identity; `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

jar = Jar()  # caches under ./.jar by default; pass any path to move it

# 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.seek()                             # start a fresh identity for this call
    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`.

## Identities Are Values

`jar.identity` exports the cursor as a string; `jar.seek(identity)` jumps back to it. That means the place that seals a value and the place that retrieves it don't have to share any code — store the identity wherever you like:

```python
jar = Jar("./cache")

class KVStore:
    def __init__(self):
        self.index: dict[str, str] = {}

    def add(self, key, value):
        jar.seek()
        jar.include(KVStore.add.__code__)
        jar.include(key)
        jar.set(value)
        self.index[key] = jar.identity   # keep the hash

    def get(self, key):
        jar.seek(self.index[key])        # jump straight to it
        return jar.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 — give each concurrent task its own (`Jar` construction is just a path and a hash seed).
