wallet_helper.ledger module
Content-addressed ledger, so a heavy call runs once and is remembered.
The storage half of wallet-helper. A call is identified by a key built from a namespace plus a payload (the arguments, file, or bytes that determine the result). The ledger stores the result under that key, so an identical call is served from disk instead of running again, across process restarts.
The default store is a directory with one JSON file per entry: local, no server, easy to inspect and to delete. It is content-addressed, so a renamed input file still hits and two different inputs never collide.
- class wallet_helper.ledger.Ledger(cache_dir=None, max_entries=None)[source]
Bases:
objectA directory-backed store of results, one JSON file per entry.
- Parameters:
cache_dir (str or pathlib.Path, optional) – Where entries live. Defaults to
$WALLET_HELPER_DIRthen~/.cache/wallet-helper.max_entries (int, optional) – A size cap. When set, each
put()also evicts down to the newestmax_entriesentries, so the store cannot grow without bound.None(default) means no automatic bound; callevict()yourself.
Examples
>>> import os_helper as osh >>> with osh.temporary_folder() as tmp: ... Ledger(tmp).has("demo_x") False
- clear(namespace=None)[source]
Delete entries, all or just one
namespace(irreversible).With a namespace, only its entries are removed (this is what a memoized function’s
cache_clear()calls). Without one, the whole directory is removed and recreated lazily on the nextput().- Parameters:
namespace (str | None)
- Return type:
None
- evict(*, max_entries=None, older_than=None)[source]
Prune entries and return how many were removed.
Expired entries (past their
ttl) are always removed. In addition:
- put(key, result, *, ttl=None)[source]
Store
resultforkey(overwrites any previous entry).- Parameters:
key (str) – The ledger key (see
make_key()).result (Any) – The JSON-serialisable result to store.
ttl (float, optional) – Seconds until the entry is considered stale.
None(default) means it never expires. A stale entry is treated as a miss on the next call and is removed byevict().
- Return type:
None
- register_hit(key)[source]
Count one reuse of the cached result for
key(no-op if absent).The read-modify-write runs under a lock so concurrent hits do not lose a count: threads in this process take turns, and separate processes take turns too where advisory file locks are available (POSIX). On Windows the cross-process count is best-effort; use
SqliteLedgerfor an exact count under heavy multi-process concurrency.- Parameters:
key (str)
- Return type:
None
- register_miss(key=None)[source]
Count one call that had to be computed because the cache had nothing.
Counted store-wide rather than per entry, because a miss by definition has no entry to count it against. Pairs with the lifetime hit tally so a caller can compute a hit rate – which is the number that actually answers “is this cache earning its keep”. A raw hit count cannot: 500 hits is excellent against 520 calls and dismal against 50 000.
- Parameters:
key (str | None)
- Return type:
None
- stats(namespace=None)[source]
Count stored entries, their reuses, and the store’s lifetime tallies.
- Parameters:
namespace (str, optional) – Restrict
entriesandhitsto this namespace.None(default) counts the whole ledger. The lifetime tallies are always store-wide – they are not attached to entries, so there is nothing to filter them by.- Returns:
entriesandhitsdescribe the entries that exist now;lifetime_hitsandlifetime_missesdescribe everything this store has ever done.- Return type:
Notes
The distinction is not pedantry.
hitsis stored inside each record, so it leaves with the record: evicting or expiring an entry destroys the evidence of every call it ever saved. Measured on a store with five entries and a hundred reuses each,evict(max_entries=2)took the reported total from 500 to 200 – sixty per cent of the saved calls, gone, with nothing raised. Since eviction and TTL are this package’s own features, any long-running cache under-reports its own value, and it does so more the better it is housekeeping.lifetime_hitsis the number that honours the original claim, “how many real calls were saved”. It survives eviction and expiry. It does not surviveclear(), which removes the store directory outright – that is an explicit instruction to forget, not automatic housekeeping.
- class wallet_helper.ledger.LedgerLike(*args, **kwargs)[source]
Bases:
ProtocolThe storage contract every ledger backend fulfils.
The default
Ledgerkeeps one JSON file per entry, which is ideal for a single process.wallet_helper.sqlite_ledger.SqliteLedgerkeeps everything in one SQLite file for a shared, concurrency-safe store. Awallet_helper.guard.Walletand the command-line tools accept either.- clear(namespace=None)[source]
Remove entries, all of them or just one namespace (irreversible).
- Parameters:
namespace (str | None)
- Return type:
None
- evict(*, max_entries=None, older_than=None)[source]
Prune entries and return how many were removed (see
Ledger.evict()).
- put(key, result, *, ttl=None)[source]
Store
resultforkey(overwrites), expiring afterttlseconds.
- register_hit(key)[source]
Count one reuse of the cached result for
key(no-op if absent).- Parameters:
key (str)
- Return type:
None
- wallet_helper.ledger.is_fresh(record, *, now=None)[source]
Return
Trueif a record has not expired.- Parameters:
record (dict) – A stored record, which may carry an
expires_attimestamp.now (float, optional) – The current time; defaults to
time.time(). Passing it lets a caller judge many records against one instant.
- Returns:
Truewhen there is no expiry, or the expiry is still in the future.- Return type:
- wallet_helper.ledger.make_key(namespace, payload)[source]
Build a ledger key from a
namespaceand a contentpayload.- Parameters:
namespace (str) – A scope for the call, for example
"transcribe"or"openai.chat", so unrelated calls do not collide even if their payloads hash alike.payload (Any) – See
_digest().
- Returns:
"<namespace>_<hash>", safe to use as a filename.- Return type:
Notes
A
stroros.PathLikeargument that names an existing file is keyed by the file’s content, by design, so the same file reached by a different path still hits (this applies to a path used as an argument or as a value; a plainstrused as a dict key keeps its text). Two consequences worth knowing:A string you meant as plain data (a title, an id) that happens to match an existing file in the working directory is content-addressed too. If that is not what you want, wrap the value so it is not a bare path, or exclude the argument with
ignore=/ akey=builder.If a file is deleted in the brief moment between detection and hashing, the key falls back to the path text for that call. The wrapped function would fail to read the missing file anyway, so no stale result is served.
Examples
>>> make_key("demo", {"b": 2, "a": 1}) == make_key("demo", {"a": 1, "b": 2}) True