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
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 recursively destructures collections on save/load. |
|
Bridges |
|
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. |
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.KeyManagement[source]
Bases:
abc.ABCAbstract 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.- _operation_context(key: fleche.digest.Digest | str)[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).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): with self._lock: # this mixin's resource with super()._operation_context(key): yield
- 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]
Find the shortest substring that is still an unambiguous reference to the same value.
- _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]
- 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
- class fleche.storage.ValueMemory[source]
Bases:
fleche.storage.destructuring.DestructuringMixin,fleche.storage.base.ValueMixin,MemoryBackendMixin 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
- class fleche.storage.CallMemory[source]
Bases:
fleche.storage.base.CallMixin,MemoryBackendBridges
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.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
- lock_wait_start: float = 0.001
- 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)[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).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): with self._lock: # this mixin's resource with super()._operation_context(key): yield
- put(call: fleche.call.DigestedCall, key: fleche.digest.Digest) fleche.digest.Digest[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.KeyManagementMixin 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)[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).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): with self._lock: # this mixin's resource with super()._operation_context(key): yield
- class fleche.storage.PerKeyLockMixin[source]
Bases:
fleche.storage.base.KeyManagementMixin 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)[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).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): with self._lock: # this mixin's resource with super()._operation_context(key): yield