fleche.storage
Storage subpackage public API.
This module re-exports the primary storage interfaces and implementations for backward compatibility with from fleche.storage import … imports.
Submodules
Exceptions
Common base class for all non-exit exceptions. |
|
Inappropriate argument value (of correct type). |
Classes
Describes the kind of operation being performed on storage. |
|
Minimal base that exposes the |
|
Abstract base providing key-management helpers for any keyed storage. |
|
Primitive backend interface for key-value storage. |
|
Abstract domain interface for value storage. |
|
Bridges |
|
Abstract domain interface for call storage. |
|
Bridges |
|
Mixin that recursively destructures collections on save/load. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
Bridges |
|
Bridges |
|
File-based storage backend using pickle. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
|
SQLAlchemy-backed CallStorage with JSON metadata and DB-backed expand(). |
|
Mixin that serializes all storage operations behind a single reentrant lock. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
Functions
|
Register a custom container destructurer. |
Package Contents
- exception fleche.storage.SaveError[source]
Bases:
ExceptionCommon base class for all non-exit exceptions.
- exception fleche.storage.AmbiguousDigestError[source]
Bases:
ValueErrorInappropriate argument value (of correct type).
- class fleche.storage.Intent[source]
Bases:
enum.StrEnumDescribes the kind of operation being performed on storage.
Mixins may use this to choose between exclusive and shared locks.
- WRITE = 'write'
- class fleche.storage.OperationContext[source]
Bases:
abc.ABCMinimal base that exposes the
_operation_context()hook.Both
KeyManagement(storage layer) andBaseCache(cache layer) inherit from this class so that the same thread-safety mixins (SerializingMixin,PerKeyLockMixin) can attach to either layer without duplication.- _operation_context(key: fleche.digest.Digest | str, *, intent: Intent = Intent.WRITE)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).intentdescribes the kind of operation being performed. Mixins may use it to choose between exclusive and shared locks. Currently the only defined value isIntent.WRITE(the default).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key, *, intent=Intent.WRITE): with self._lock: # this mixin's resource with super()._operation_context(key, intent=intent): yield
- class fleche.storage.KeyManagement[source]
Bases:
OperationContextAbstract base providing key-management helpers for any keyed storage.
Subclasses must implement
list,_evict, and_contains. The concrete helpersevict,contains,expand, andshrinkare implemented here once and inherited by all storage classes.Every public operation enters
_operation_context()around the compound work it performs, so mixins can inject an operation-scoped resource (e.g. a threading lock, a SQLAlchemy session, a file handle) without overriding every method individually.- abstractmethod list() Iterable[fleche.digest.Digest][source]
- abstractmethod _evict(key: fleche.digest.Digest) None[source]
- abstractmethod _contains(key: fleche.digest.Digest) bool[source]
- evict(key: fleche.digest.Digest | str) None[source]
Removes the entry corresponding to the key from the storage.
- contains(key: fleche.digest.Digest | str) bool[source]
- expand(key: fleche.digest.Digest | str) fleche.digest.Digest[source]
Expands a short-hand digest to the full length one.
- shrink(key: fleche.digest.Digest | str, /) fleche.digest.Digest[source]
- shrink(key: fleche.digest.Digest | str, /, *keys: fleche.digest.Digest | str) tuple[Digest, ...]
Find the shortest substring(s) that unambiguously reference each key.
With a single key, returns one
Digest. With multiple keys, returns a tuple ofDigestin the same order as the inputs; the batched form fetcheslist()once instead of per-key, which matters on backends where listing is expensive (e.g. SQL, filesystem).
- _shrink_one(key: Digest | str, sorted_all: Sequence[str]) fleche.digest.Digest[source]
- _normalize_key(key: fleche.digest.Digest | str) fleche.digest.Digest[source]
Expand a short digest prefix to a full key, or wrap a full key as Digest.
- class fleche.storage.StorageBackend[source]
Bases:
KeyManagementPrimitive backend interface for key-value storage.
Backends implement the low-level
put/get/_evict/listoperations. Higher-level classes (ValueMixin,CallMixin) add domain-specific logic on top.- abstractmethod put(value: Any, key: fleche.digest.Digest) fleche.digest.Digest[source]
- abstractmethod get(key: fleche.digest.Digest) Any[source]
- _contains(key: fleche.digest.Digest) bool[source]
- class fleche.storage.ValueStorage[source]
Bases:
KeyManagementAbstract domain interface for value storage.
- abstractmethod save(value: Any, key: fleche.digest.Digest | None = None) fleche.digest.Digest[source]
- abstractmethod load(key: fleche.digest.Digest | str) Any[source]
- class fleche.storage.ValueMixin[source]
Bases:
ValueStorage,StorageBackendBridges
ValueStoragewithStorageBackendprimitives.Implements
saveandloadusingputandget. Concrete classes inherit from this and aStorageBackendimplementation to get a fully functional value storage.- save(value: Any, key: fleche.digest.Digest | None = None) fleche.digest.Digest[source]
- load(key: fleche.digest.Digest | str) Any[source]
- class fleche.storage.CallStorage[source]
Bases:
KeyManagementAbstract domain interface for call storage.
- abstractmethod save(call: fleche.call.DigestedCall) fleche.digest.Digest[source]
- abstractmethod load(key: fleche.digest.Digest | str) fleche.call.DigestedCall[source]
- abstractmethod query(template: fleche.call.QueryCall) Iterable[fleche.call.DigestedCall][source]
- transform(func: Callable[[fleche.call.DigestedCall], fleche.call.DigestedCall] | None = None) None[source]
Applies a transformation function to all DigestedCall objects in the storage.
- Parameters:
func – A function that takes a
DigestedCalland returns a transformed one. IfNone, the identity is used (useful for re-calculating keys).
- class fleche.storage.CallMixin[source]
Bases:
CallStorage,StorageBackendBridges
CallStoragewithStorageBackendprimitives.Implements
save,load, andqueryusingputandget, deriving the storage key from the call’s lookup key.transformis inherited fromCallStorage.Concrete classes inherit from this and a
StorageBackendimplementation to get a fully functional call storage.- load(key: fleche.digest.Digest | str) fleche.call.DigestedCall[source]
- query(template: fleche.call.QueryCall) Iterable[fleche.call.DigestedCall][source]
Find cached calls that ‘match’ the template.
Returns all calls where the given arguments, results or metadata match exactly the stored ones. Values may be given either as they are or as
Digest.- Parameters:
template (Call) – specification for calls to return; use None as wildcard.
- Returns:
an iterable over all matching digested call objects
- Return type:
Iterable[DigestedCall]
- class fleche.storage.DestructuringMixin[source]
Bases:
fleche.storage.base.ValueStorageMixin that recursively destructures collections on save/load.
Place before a
ValueMixinin the MRO to add destructuring behavior. Lists, tuples, and dicts are broken apart so each element is stored independently; on load the original structure is reassembled.Example
>>> from fleche.storage.base import ValueMixin >>> from fleche.storage.memory import MemoryBackend >>> @dataclass(frozen=True) ... class MyValueStorage(DestructuringMixin, ValueMixin, MemoryBackend): ... >>> vm = MyValueStorage(storage={}) >>> key = vm.save([1, [2, 3]]) >>> vm.load(key) == [1, [2, 3]] True
- remaining_depth: int = 0
- _intern_rec(value: Any, key: fleche.digest.Digest | None = None) tuple[Any, int | float][source]
Post-order traversal: recurse to leaves, decide inline-vs-store on the way back up.
Returns
(result, depth)where result is the plain value whendepth < remaining_depth(the element is inlined in its parent’sDigestedwrapper) or aDigestwhen the element was written to storage separately. Every node in the structure is visited exactly once (O(n)), unlike a separate depth-counting pass.
- save(value: Any, key: fleche.digest.Digest | None = None) fleche.digest.Digest[source]
- load(key: fleche.digest.Digest | str) Any[source]
- _raw_sub_digests(raw: Any) set[fleche.digest.Digest][source]
Direct digest children of a raw stored entry.
A raw entry is what
super().loadreturns — i.e. what was written to the underlying backend beforemend()rewires sub-digests back into their parent container. OnlyDigestedwrappers carry child references; scalars and plain (non-destructured) containers return an empty set.
- child_digests(key: fleche.digest.Digest | str) set[fleche.digest.Digest][source]
Direct digest children of the raw entry stored at key.
Bypasses
mend(), so destructured sub-references are returned as opaqueDigestkeys rather than being followed. Intended for reference-graph traversals (GC, debugging) where loading the mended value would flatten the structure we need to inspect.- Raises:
KeyError – if key is not present in the underlying backend.
- count_reuses() collections.Counter[fleche.digest.Digest][source]
Return a counter of how many times each stored key is referenced as a sub-component.
Scans every raw entry and tallies
Digestback-references found insideDigestedIterableandDigestedDictwrappers. A count of0means the key is not pointed to by any other stored value (i.e. a top-level entry). A count greater than1indicates a sub-value shared between multiple parent containers.- Returns:
A
Countermapping eachDigestkey to the number of times it is referenced by other stored entries.
Example
>>> from fleche.storage.memory import ValueMemory >>> ds = ValueMemory(storage={}) >>> shared = [2, 3] >>> _ = ds.save([1, shared]) >>> _ = ds.save([4, shared]) >>> hits = ds.count_reuses() >>> hits[ds.save(shared)] # [2, 3] is referenced by both outer lists 2
- fleche.storage.register_destructurer(pred: Callable[[Any], bool], fn: Callable) None[source]
Register a custom container destructurer.
pred(value) should return
Truefor values this destructurer handles. fn must accept(intern, value)where intern isDestructuringMixin._intern_rec(). Entries are appended after the built-in ones; first match wins, so registering a handler for an entirely new container type is safe without displacing list/dict/dataclass/attrs. Call before anyDestructuringMixininstance is used.
- class fleche.storage.ValueMemory[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.destructuring.DestructuringMixin,fleche.storage.base.ValueMixin,MemoryBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- __hash__
- class fleche.storage.CallMemory[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.base.CallMixin,MemoryBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- __hash__
- class fleche.storage.ValueVoid[source]
Bases:
fleche.storage.base.ValueMixin,VoidBackendBridges
ValueStoragewithStorageBackendprimitives.Implements
saveandloadusingputandget. Concrete classes inherit from this and aStorageBackendimplementation to get a fully functional value storage.
- class fleche.storage.CallVoid[source]
Bases:
fleche.storage.base.CallMixin,VoidBackendBridges
CallStoragewithStorageBackendprimitives.Implements
save,load, andqueryusingputandget, deriving the storage key from the call’s lookup key.transformis inherited fromCallStorage.Concrete classes inherit from this and a
StorageBackendimplementation to get a fully functional call storage.
- class fleche.storage.FileStorage[source]
Bases:
fleche.storage.base.StorageBackendFile-based storage backend using pickle.
Stores objects on the filesystem.
- root: pathlib.Path
- lock_timeout: float = 1.0
- list() Iterable[fleche.digest.Digest][source]
- _evict(key: fleche.digest.Digest) None[source]
- put(value: Any, key: fleche.digest.Digest) fleche.digest.Digest[source]
- get(key: fleche.digest.Digest) Any[source]
- _contains(key: fleche.digest.Digest) bool[source]
- class fleche.storage.ValuePickleFile[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.destructuring.DestructuringMixin,fleche.storage.base.ValueMixin,PickleFileBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- class fleche.storage.CallPickleFile[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.base.CallMixin,PickleFileBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- class fleche.storage.ValueBagOfHoldingH5File[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.destructuring.DestructuringMixin,fleche.storage.base.ValueMixin,BagOfHoldingH5FileBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- class fleche.storage.CallBagOfHoldingH5File[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.base.CallMixin,BagOfHoldingH5FileBackendMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- class fleche.storage.Sql[source]
Bases:
fleche.storage.thread_safe.PerKeyLockMixin,fleche.storage.base.CallStorageSQLAlchemy-backed CallStorage with JSON metadata and DB-backed expand().
- url: str | None = None
- echo: bool = False
- engine: Any
- session: Any
- _local: threading.local
- _operation_context(key, *, intent: fleche.storage.base.Intent = Intent.WRITE)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).intentdescribes the kind of operation being performed. Mixins may use it to choose between exclusive and shared locks. Currently the only defined value isIntent.WRITE(the default).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key, *, intent=Intent.WRITE): with self._lock: # this mixin's resource with super()._operation_context(key, intent=intent): yield
- _persist_call(call: fleche.call.DigestedCall, key: fleche.digest.Digest) fleche.digest.Digest[source]
- _fetch_call(key: fleche.digest.Digest) fleche.call.DigestedCall[source]
- _contains(key: fleche.digest.Digest) bool[source]
- list() Iterable[fleche.digest.Digest][source]
- expand(key: fleche.digest.Digest | str) fleche.digest.Digest[source]
Expands a short-hand digest to the full length one.
- _evict(key: fleche.digest.Digest) None[source]
- load(key: fleche.digest.Digest | str) fleche.call.DigestedCall[source]
- _normalize_value(v: Any) str[source]
Return the stored form used in SQL for argument/result matching.
We must match the generic CallStorage.query semantics which compare digest(template_value) == digest(stored_call_value). In this backend, stored argument/result values are hex-digest strings, and digest(Digest(x)) == x. Therefore we should always compare Arg.value/CallModel.result to str(digest(template_value)).
- _build_call_conditions(template: fleche.call.QueryCall) List[Any][source]
- query(template: fleche.call.QueryCall) Iterable[fleche.call.DigestedCall][source]
Find cached calls matching a template using SQL-side filtering.
Semantics match CallStorage.query: - Fields set to None are wildcards. - Arguments and result are compared by digest(template_value) == digest(stored_value). - Metadata can be filtered by providing template.metadata as a mapping of
metadata name -> dict of key/value filters. An empty dict for a given name means “presence of that metadata name”. Filters with simple types (str, bool, int, float) are pushed down to SQL via JSON-extract expressions; other types (e.g., lists) or None values fall back to client-side checks after loading.
This method builds a SELECT over calls, joining the arguments table and metadata table as needed to reduce candidate rows, then loads the resulting calls and performs any remaining client-side validation.
- Parameters:
template – A Call used as a template. None-valued fields are wildcards.
- Yields:
Call – Matching calls including their decoded metadata.
- class fleche.storage.SerializingMixin[source]
Bases:
fleche.storage.base.OperationContextMixin that serializes all storage operations behind a single reentrant lock.
Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class SerializingValueMemory(SerializingMixin, ValueMemory): ...
- _lock: _PicklableRLock
- _operation_context(key, *, intent: fleche.storage.base.Intent = Intent.WRITE)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).intentdescribes the kind of operation being performed. Mixins may use it to choose between exclusive and shared locks. Currently the only defined value isIntent.WRITE(the default).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key, *, intent=Intent.WRITE): with self._lock: # this mixin's resource with super()._operation_context(key, intent=intent): yield
- class fleche.storage.PerKeyLockMixin[source]
Bases:
fleche.storage.base.OperationContextMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Instances must be hashable. Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValuePickle(PerKeyLockMixin, ValuePickleFile): ...
- _get_key_lock(key: fleche.digest.Digest | str) threading.RLock[source]
- _operation_context(key, *, intent: fleche.storage.base.Intent = Intent.WRITE)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).intentdescribes the kind of operation being performed. Mixins may use it to choose between exclusive and shared locks. Currently the only defined value isIntent.WRITE(the default).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key, *, intent=Intent.WRITE): with self._lock: # this mixin's resource with super()._operation_context(key, intent=intent): yield