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. |
|
Bridges |
|
Bridges |
|
Bridges |
|
Bridges |
|
File-based storage backend using pickle. |
|
Bridges |
|
Bridges |
|
Bridges |
|
Bridges |
|
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.
- 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.Call) fleche.digest.Digest[source]
- abstractmethod load(key: fleche.digest.Digest | str) fleche.call.Call[source]
- abstractmethod query(template: fleche.call.QueryCall) Iterable[fleche.call.Call][source]
- transform(func: Callable[[fleche.call.Call], fleche.call.Call] | None = None) None[source]
Applies a transformation function to all Call objects in the storage.
- 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.- save(call: fleche.call.Call) fleche.digest.Digest[source]
- load(key: fleche.digest.Digest | str) fleche.call.Call[source]
- query(template: fleche.call.QueryCall) Iterable[fleche.call.Call][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.
- class fleche.storage.DestructuringMixin[source]
Bases:
fleche.storage.base.StorageBackendMixin that recursively destructures collections on save/load.
Place before a concrete
StorageBackendin 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:
@dataclass(frozen=True) class ValueMemory(ValueMixin, DestructuringMixin, MemoryBackend): ... vm = ValueMemory(storage={}) key = vm.save([1, [2, 3]]) assert vm.load(key) == [1, [2, 3]]
- 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.
- put(value: Any, key: fleche.digest.Digest) fleche.digest.Digest[source]
- get(key: fleche.digest.Digest | Any) Any[source]
- class fleche.storage.ValueMemory[source]
Bases:
fleche.storage.base.ValueMixin,fleche.storage.destructuring.DestructuringMixin,MemoryBackendBridges
ValueStoragewithStorageBackendprimitives.Implements
saveandloadusingputandget. Concrete classes inherit from this and aStorageBackendimplementation to get a fully functional value storage.
- 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.base.ValueMixin,fleche.storage.destructuring.DestructuringMixin,PickleFileBackendBridges
ValueStoragewithStorageBackendprimitives.Implements
saveandloadusingputandget. Concrete classes inherit from this and aStorageBackendimplementation to get a fully functional value storage.
- class fleche.storage.CallPickleFile[source]
Bases:
fleche.storage.base.CallMixin,PickleFileBackendBridges
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.ValueBagOfHoldingH5File[source]
Bases:
fleche.storage.base.ValueMixin,fleche.storage.destructuring.DestructuringMixin,BagOfHoldingH5FileBackendBridges
ValueStoragewithStorageBackendprimitives.Implements
saveandloadusingputandget. Concrete classes inherit from this and aStorageBackendimplementation to get a fully functional value storage.
- class fleche.storage.CallBagOfHoldingH5File[source]
Bases:
fleche.storage.base.CallMixin,BagOfHoldingH5FileBackendBridges
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.Sql[source]
Bases:
fleche.storage.base.CallStorageSQLAlchemy-backed CallStorage with JSON metadata and DB-backed expand().
- url: str | None = None
- echo: bool = False
- engine: Any
- session: Any
- put(call: Any, 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]
- save(call: fleche.call.Call) fleche.digest.Digest[source]
- load(key: fleche.digest.Digest | str) fleche.call.Call[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.Call][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: threading.RLock
- _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).Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValueMemory(PerKeyLockMixin, ValueMemory): ...
- _key_locks: weakref.WeakValueDictionary[fleche.digest.Digest | str, threading.RLock]
- _meta_lock: threading.Lock
- _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