fleche.caches ============= .. py:module:: fleche.caches Attributes ---------- .. autoapisummary:: fleche.caches.logger fleche.caches.DigestedIterable fleche.caches.DigestedDict Exceptions ---------- .. autoapisummary:: fleche.caches.Rejected Classes ------- .. autoapisummary:: fleche.caches.BaseCache fleche.caches.Cache fleche.caches.CacheWrapper fleche.caches.ReadOnlyMixin fleche.caches.ReadOnlyCache fleche.caches.FilteringMixin fleche.caches.FilteredCache fleche.caches.RefreshingCache fleche.caches.CacheStack fleche.caches.SizeLimitedMixin fleche.caches.SizeLimitedCache Functions --------- .. autoapisummary:: fleche.caches._combine_expand fleche.caches._combine_shrink Module Contents --------------- .. py:data:: logger .. py:exception:: Rejected Bases: :py:obj:`Exception` Cache refused to cache the call for some reason or other. .. py:data:: DigestedIterable .. py:data:: DigestedDict .. py:class:: BaseCache Bases: :py:obj:`abc.ABC` Helper class that provides a standard way to create an ABC using inheritance. .. py:method:: save(call: fleche.call.Call) -> str :abstractmethod: .. py:method:: load(key: str) -> fleche.call.LazyCall :abstractmethod: .. py:method:: load_value(key: str) -> Any :abstractmethod: .. py:method:: evict(key: str | fleche.digest.Digest) -> None :abstractmethod: .. py:method:: contains(key: str) -> bool .. py:method:: transfer(other: BaseCache, pop: bool = False, overwrite: bool = False) -> None Transfer all calls from this cache to another cache. :param other: The destination cache. :param pop: If True, evict transferred keys from the source cache after moving. :param overwrite: If True, overwrite existing entries in the target cache. If False (default), skip entries that already exist in the target. .. py:method:: readonly() -> ReadOnlyCache Return a read-only view of this cache. .. py:method:: push(cache: BaseCache) -> CacheStack .. py:method:: expand(key: fleche.digest.Digest | str) -> fleche.digest.Digest :abstractmethod: Expand a short digest prefix to its full-length digest. :param key: the short digest prefix to expand :type key: str or :class:`Digest` :returns: the full-length digest :rtype: :class:`Digest` :raises KeyError: if the key is not found :raises AmbiguousDigestError: if the prefix matches more than one entry .. py:method:: shrink(key: fleche.digest.Digest | str, /) -> fleche.digest.Digest shrink(key: fleche.digest.Digest | str, /, *keys: fleche.digest.Digest | str) -> tuple[Digest, ...] Find the shortest substring(s) that unambiguously reference each call. With a single key, returns one :class:`Digest`. With multiple keys, returns a tuple of :class:`Digest` in the same order as the inputs; the batched form lets sub-storages list their keys once instead of per-key, which matters on backends where listing is expensive (e.g. SQL, filesystem). Each input key must belong to *one* of the sub-storages (call or value). Mixing call keys and value keys in a single call is undefined behaviour — the result depends on internal partitioning order and may change without notice. .. warning:: This is a property of how many values there are in your storage! A key returned from this function may become ambigious in the future when more values are added. Do not rely on this function in your programs, it is provided as a convenience for users only! :param \*keys: one or more keys to shorten :type \*keys: str or :class:`Digest` :returns: :class:`Digest` (single key) or tuple of :class:`Digest` (multiple) :raises AmbiguousDigestError: if no shorter key is possible for any input .. py:method:: _shrink(*keys: fleche.digest.Digest | str) -> tuple[Digest, ...] :abstractmethod: Partition and shrink all keys; always returns a same-length tuple of short digests. .. py:method:: _query(call: BaseCache._query.call) -> Iterable[fleche.call.LazyCall] :abstractmethod: .. py:method:: query(template: fleche.call.QueryCall | None = None, **kwargs) -> fleche.query.QueryIterator Query the cache for matching calls. Accepts either a :class:`~fleche.call.QueryCall` as the first positional argument, or the same keyword arguments that :class:`~fleche.call.QueryCall` accepts. Omitted fields default to ``None`` (wildcard). Passing both a template and keyword arguments raises :class:`TypeError`. Examples:: cache.query(name="my_func") cache.query(name="my_func", arguments={"x": 1}) cache.query(QueryCall(name="my_func")) # existing form still works cache.query() # all calls :returns: :class:`~fleche.query.QueryIterator` .. py:method:: table(arguments: Iterable[str] | str | Literal[True] = (), results=False, shrink_keys: bool = True) -> pandas.DataFrame Return a pandas DataFrame summarizing cached calls via query(). This implementation uses a fully-wildcard Call template to retrieve all calls through ``self.query`` and then flattens metadata keys into top-level columns for convenience. By default, arguments and results are elided. The DataFrame index will be the lookup key (digest) of each call. Columns are: - `name`: the function name - `module`: the module name - 'result`: if `results` argument is `True` - metadata fields are flattened and added as columns directly If given argument names collide with any of the above columns, they are prefixed by 'a_'. Only requested arguments are loaded from cache. :param arguments: add the given arguments (of the queried calls) as columns to the table. Pass ``True`` to add all arguments, or a single string as a shortcut for a one-element tuple. :param results: if True, add results of queried calls to table :type results: bool :param shrink_keys: if True (default), shrink each index entry to its shortest unambiguous prefix. Set to ``False`` to keep full-length digests. :type shrink_keys: bool :returns: table of all calls on cache :rtype: :class:`pandas.DataFrame` .. py:method:: filter(predicate: Callable[[fleche.call.Call | fleche.call.LazyCall], bool] | fleche.call.QueryCall) -> FilteredCache Create a read-only view of this cache that only exposes calls matching the predicate. :param predicate: A function that takes a Call or LazyCall and returns True if it should be included in the new cache, or a QueryCall object to use as a template. :returns: A read-only view of the cache. :rtype: FilteredCache .. py:function:: _combine_expand(key: Digest | str, results: Iterable[Digest]) -> fleche.digest.Digest Reduce sub-storage expand results to a single resolved digest. :raises KeyError: if no results were found. :raises AmbiguousDigestError: if results disagree on the full digest. .. py:function:: _combine_shrink(key: Digest | str, results: Iterable[Digest]) -> fleche.digest.Digest Reduce sub-storage shrink results to the longest (safest) prefix. :raises KeyError: if no results were found. .. py:class:: Cache Bases: :py:obj:`BaseCache` Helper class that provides a standard way to create an ABC using inheritance. .. py:attribute:: values :type: fleche.storage.ValueStorage .. py:attribute:: calls :type: fleche.storage.CallStorage .. py:method:: load_value(key) .. py:method:: save(call: fleche.call.Call) -> str .. py:method:: load(key: str) -> fleche.call.LazyCall .. py:method:: contains(key: str) -> bool .. py:method:: expand(key: fleche.digest.Digest | str) -> fleche.digest.Digest Expand a short digest prefix to its full-length digest. :param key: the short digest prefix to expand :type key: str or :class:`Digest` :returns: the full-length digest :rtype: :class:`Digest` :raises KeyError: if the key is not found :raises AmbiguousDigestError: if the prefix matches more than one entry .. py:method:: _shrink(*keys: fleche.digest.Digest | str) -> tuple[Digest, ...] Partition and shrink all keys; always returns a same-length tuple of short digests. .. py:method:: _query(call: Cache._query.call) -> Iterable[fleche.call.LazyCall] Query for cached calls that match a template and return decoded results. This delegates to the underlying :meth:`CallStorage.query` using the provided template ``call``. Any digested argument values and the result are decoded via this cache's value storage before yielding. :param call: A ``Call`` instance used as a template; fields set to ``None`` act as wildcards. For arguments and result, comparisons follow digest semantics (i.e., values are matched by their digest). :Yields: *Call | LazyCall* -- Matching calls with arguments and result decoded from digests where possible. .. py:method:: evict(key: str | fleche.digest.Digest) -> None .. py:method:: redigest() -> None Ensures consistent cache keys in case digest function changed. This may take time depending on cache size. .. py:method:: gc() -> set[fleche.digest.Digest] Evict value entries not reachable from any stored call. Brute-force mark-and-sweep: walks every call record to build the set of directly-referenced value digests, then transitively follows destructured sub-references (via :meth:`DestructuringMixin.child_digests` on storages that satisfy :class:`HasChildDigests`), and evicts every ``values`` key outside the reachable set. Call records are left untouched. :returns: The set of digests that were evicted from value storage. .. py:class:: CacheWrapper Bases: :py:obj:`BaseCache` Forwarding base class: all BaseCache methods delegate to ``self.cache``. Combine with behaviour mixins (ReadOnlyMixin, FilteringMixin) to build concrete wrapper classes without redeclaring ``cache``. .. py:attribute:: cache :type: BaseCache .. py:method:: save(call: fleche.call.Call) -> str .. py:method:: load(key: str) -> fleche.call.LazyCall .. py:method:: load_value(key: str) -> Any .. py:method:: contains(key: str) -> bool .. py:method:: evict(key: str | fleche.digest.Digest) -> None .. py:method:: expand(key: fleche.digest.Digest | str) -> fleche.digest.Digest Expand a short digest prefix to its full-length digest. :param key: the short digest prefix to expand :type key: str or :class:`Digest` :returns: the full-length digest :rtype: :class:`Digest` :raises KeyError: if the key is not found :raises AmbiguousDigestError: if the prefix matches more than one entry .. py:method:: _shrink(*keys: fleche.digest.Digest | str) -> tuple[Digest, ...] Partition and shrink all keys; always returns a same-length tuple of short digests. .. py:method:: _query(call: CacheWrapper._query.call) -> Iterable[fleche.call.LazyCall] .. py:class:: ReadOnlyMixin Bases: :py:obj:`CacheWrapper` Raises :class:`Rejected` for ``save`` and ``evict``. .. py:method:: save(call: fleche.call.Call) .. py:method:: evict(key: str | fleche.digest.Digest) -> None .. py:class:: ReadOnlyCache Bases: :py:obj:`ReadOnlyMixin` A cache that can only be read from. .. py:class:: FilteringMixin Bases: :py:obj:`CacheWrapper` Filters ``load`` and ``_query`` results by a predicate. .. py:attribute:: predicate :type: Callable[[fleche.call.Call | fleche.call.LazyCall], bool] .. py:method:: load(key: str) -> fleche.call.LazyCall .. py:method:: _query(call: FilteringMixin._query.call) -> Iterable[fleche.call.LazyCall] .. py:class:: FilteredCache Bases: :py:obj:`FilteringMixin`, :py:obj:`ReadOnlyMixin` A read-only view of a cache that only exposes calls matching a predicate. .. py:class:: RefreshingCache Bases: :py:obj:`CacheWrapper` A cache that forces re-execution by always missing on load. It forwards saves and value loads to an underlying cache, allowing new results to be stored while ensuring that existing ones are ignored for the duration of its use. This is necessary to handle nested fleche calls during a rerun, otherwise forcing them to re-execute would be awkward. .. py:method:: load(key: str) -> fleche.call.LazyCall .. py:method:: contains(key: str) -> bool .. py:class:: CacheStack Bases: :py:obj:`BaseCache` A combination of caches with a shared traversal policy. Saving always targets the lowest level (``stack[0]``); loading traverses from ``stack[0]`` upward and back-fills any hit into ``stack[0]``. All multi-cache fan-out is handled by three private traversal helpers, each implementing one of the recurring patterns across the stack: - :meth:`_first_hit` — return on the first success; raise if all miss. - :meth:`_collect` — gather every success; caller combines the results. - :meth:`_foreach` — apply to every cache; swallow expected refusals. .. py:attribute:: stack :type: tuple[BaseCache, Ellipsis] .. py:method:: __post_init__() .. py:method:: save(call: fleche.call.Call) .. py:method:: load(key) -> fleche.call.LazyCall .. py:method:: load_value(key) .. py:method:: contains(key: str) -> bool .. py:method:: push(cache: BaseCache) -> CacheStack .. py:method:: evict(key: str | fleche.digest.Digest) -> None .. py:method:: expand(key: fleche.digest.Digest | str) -> fleche.digest.Digest Expand a short digest prefix to its full-length digest. :param key: the short digest prefix to expand :type key: str or :class:`Digest` :returns: the full-length digest :rtype: :class:`Digest` :raises KeyError: if the key is not found :raises AmbiguousDigestError: if the prefix matches more than one entry .. py:method:: _shrink(*keys: fleche.digest.Digest | str) -> tuple[Digest, ...] Partition and shrink all keys; always returns a same-length tuple of short digests. .. py:method:: _query(call: CacheStack._query.call) -> Iterable[fleche.call.LazyCall] Aggregate query results across the stack, avoiding duplicates. The caches are queried from bottom to top. Results are deduplicated by their lookup key (via ``Call.to_lookup_key()``) and yielded in the order they are first seen. :param call: A template ``Call`` where ``None`` fields act as wildcards. :Yields: *Call | LazyCall* -- Matching calls from any cache in the stack, without duplicates. .. py:method:: _first_hit(op: Callable[[BaseCache], Any], *, exc: type[BaseException] = KeyError) -> Any Return the first successful result from iterating the stack. Invokes ``op(cache)`` on each cache in ``self.stack`` in order and returns immediately when a call does not raise *exc*. If every cache raises *exc* the exception is re-raised. This is the **first-hit-wins** pattern: used when any single cache can satisfy the request and caches earlier in the stack are preferred (e.g. :meth:`load_value`). The caller supplies the per-cache operation as a lambda so the key (or other closure state) is always available in the traceback without adding an extra helper argument. :param op: Callable that accepts a single :class:`BaseCache` and returns the desired result. Called at most once per cache. :param exc: Exception *class* treated as a cache miss. Defaults to :class:`KeyError`. Must be a single type (not a tuple) because it is also used in the ``raise`` at the end. :raises exc: If every cache in the stack raises *exc*. .. py:method:: _collect(op: Callable[[BaseCache], Any], *, exc: type[BaseException] = KeyError) -> list Collect one result per cache, skipping misses. Invokes ``op(cache)`` on every cache in ``self.stack`` and appends each non-raising result to a list. Caches that raise *exc* are silently skipped; all other exceptions propagate normally. This is the **collect-and-combine** pattern: used when all caches may hold relevant data and the caller needs to aggregate results before returning (e.g. :meth:`expand` and :meth:`shrink`, which pass the collected list to ``_combine_expand``/``_combine_shrink``). :param op: Callable that accepts a single :class:`BaseCache` and returns a result to collect. Called exactly once per cache. :param exc: Exception *class* to treat as a miss and skip. Defaults to :class:`KeyError`. :returns: A list of all non-raising results in stack order. May be empty when every cache misses; the caller is responsible for handling that case (typically by raising :class:`KeyError`). .. py:method:: _foreach(op: Callable[[BaseCache], None], *, exc: type[BaseException] | tuple[type[BaseException], Ellipsis] = (Rejected, KeyError)) -> None Apply an operation to every cache in the stack, swallowing refusals. Invokes ``op(cache)`` on every cache in ``self.stack`` unconditionally. Exceptions of type *exc* are caught and discarded; any other exception propagates normally. This is the **apply-everywhere** pattern: used when an operation should be attempted on all caches regardless of whether individual caches support it (e.g. :meth:`evict`, where read-only caches raise :class:`Rejected` and empty caches raise :class:`KeyError`, and both are expected non-fatal outcomes). :param op: Callable that accepts a single :class:`BaseCache`. Its return value is ignored. Called exactly once per cache. :param exc: Exception type(s) to swallow. Defaults to ``(Rejected, KeyError)`` — the two standard refusal signals used across the cache hierarchy. Pass a tuple to swallow multiple types. .. py:class:: SizeLimitedMixin Bases: :py:obj:`BaseCache` Mixin that enforces a maximum number of cached calls with random eviction. Combine this with :class:`Cache` (mixin first in MRO) to get a size-limited cache:: @dataclass class SizeLimitedCache(SizeLimitedMixin, Cache): max_size: int When a new call is saved and the number of cached calls exceeds ``max_size``, a call record is selected for eviction via :meth:`_pick_eviction_target`. Value storage is intentionally left untouched. The concrete class must provide a ``max_size`` integer, which is provided automatically when mixed with :class:`Cache`. .. py:attribute:: max_size :type: int .. py:attribute:: _lock :type: threading.RLock .. py:attribute:: _keys :type: set[str] .. py:method:: __post_init__(*args, **kwargs) .. py:method:: _pick_eviction_target(keys: list[str]) -> str Select the call to evict from a sample of cached call keys. The default implementation chooses uniformly at random. Override this method to implement a different eviction policy without touching any other part of the class. :param keys: A non-empty list of all tracked call keys. :returns: The key of the call that should be evicted. .. py:method:: _enforce_size_limit() -> None Evict call records until the cache is within ``max_size``. .. py:method:: save(call: SizeLimitedMixin.save.call) -> str .. py:method:: evict(key: str | fleche.digest.Digest) -> None .. py:class:: SizeLimitedCache Bases: :py:obj:`SizeLimitedMixin`, :py:obj:`Cache` A :class:`Cache` that enforces a maximum number of cached calls. When a new call is saved and the number of cached calls exceeds ``max_size``, a call record is selected for eviction via :meth:`_pick_eviction_target`. The default policy evicts uniformly at random; override :meth:`_pick_eviction_target` to change this. :param values: Value storage (forwarded to :class:`Cache`). :param _calls: Call storage (forwarded to :class:`Cache`). :param max_size: Maximum number of calls to keep. .. py:attribute:: max_size :type: int