loom.core.cache

class loom.core.cache.BatchFingerprintResolver(*args, **kwargs)[source]

Bases: Protocol

Optional capability: fingerprint several tag groups in one round trip.

A DependencyResolver that also implements this protocol is asked for every fingerprint of a page at once, which turns the per-entity counter lookups of a cached list into a single backend read. Resolvers that do not implement it keep working through DependencyResolver.fingerprint(), one call per group.

Implementations must return exactly the same value fingerprint would return for each group, in the same order as tag_groups.

async fingerprint_many(tag_groups)[source]

Compute one fingerprint per tag group.

Parameters:

tag_groups (Sequence[list[str]]) – Dependency tag names, grouped per entity.

Returns:

One fingerprint per group, in input order.

Return type:

list[str]

class loom.core.cache.CacheGateway(*, alias='default')[source]

Bases: object

Facade over aiocache with msgpack serialization for entity data.

Two distinct usage modes:

Data gateway — configured with MsgspecSerializer. Used by CachedRepository for entity and list storage. All values are msgpack-encoded on write and decoded on read.

Counter gateway — configured without a serializer (raw backend). Used by GenerationalDependencyResolver for generation counter storage. Values are stored as plain Python integers, enabling atomic native increment on both Redis and SimpleMemoryCache.

The gateway auto-detects which mode it is in at construction time and routes incr() accordingly — no manual configuration needed beyond choosing the right aiocache alias.

Parameters:

alias (str) – Registered aiocache alias to retrieve the backend from.

Example:

data_gateway    = CacheGateway(alias=config.aiocache_alias)
counter_gateway = CacheGateway(alias=config.effective_counter_alias)
resolver = GenerationalDependencyResolver(counter_gateway)
static configure(raw_config)[source]

Apply a configuration mapping to the global aiocache registry.

Parameters:

raw_config (Mapping[str, Any]) – Configuration dict compatible with aiocache.caches.set_config.

Return type:

None

classmethod apply_config(config)[source]

Configure aiocache from a CacheConfig.

Equivalent to configure() but also injects config.max_size into every aiocache.SimpleMemoryCache backend entry that does not already declare its own max_size. Entries for other backend types (Redis, Memcached, …) are forwarded unchanged.

Parameters:

config (CacheConfig) – Resolved cache configuration.

Return type:

None

Example:

cache_cfg = CacheConfig(
    aiocache_alias="cache",
    counter_alias="counters",
    max_size=1000,
    aiocache_config={
        "cache":    {"cache": "aiocache.SimpleMemoryCache", ...},
        "counters": {"cache": "aiocache.SimpleMemoryCache"},
    },
)
CacheGateway.apply_config(cache_cfg)
async get_value(key: str, *, type: type[T]) T | None[source]
async get_value(key: str, *, type: None = None) Any
async get_value(key: str, *, type: type[T] | None = None) T | Any | None

Retrieve a cached value, optionally converting it to the given type.

Parameters:
  • key (str) – Cache key.

  • type (type[T] | None) – Optional target type for msgspec.convert.

Returns:

The cached value (converted to type if given) or None on miss.

Return type:

T | Any | None

async multi_get_values(keys, *, type=None)[source]

Retrieve multiple values in a single round-trip.

Parameters:
  • keys (list[str]) – Cache keys to look up.

  • type (type[T] | None) – Optional target type for msgspec.convert on each value.

Returns:

Values in the same order as keys, with None for misses.

Return type:

list[T | Any | None]

async exists(key)[source]

Check whether a key exists in the cache.

Parameters:

key (str) – Cache key to check.

Returns:

True if the key is present.

Return type:

bool

async set_value(key, value, ttl=None)[source]

Store a value under the given key.

On a serialised backend the value is encoded here, so a value the serializer rejects raises CacheWriteError and nothing reaches the backend. A raw backend stores value as is.

Parameters:
  • key (str) – Cache key.

  • value (Any) – Value to store.

  • ttl (int | None) – Time-to-live in seconds. None means no expiration.

Raises:

CacheWriteError – If the backend serializer cannot encode value.

Return type:

None

async multi_set_values(pairs, ttl=None)[source]

Store multiple key-value pairs in a single round-trip.

On a serialised backend every pair is encoded before any is written, so one rejected value stores none of them.

Parameters:
  • pairs (list[tuple[str, Any]]) – List of (key, value) tuples.

  • ttl (int | None) – Time-to-live in seconds applied to all entries.

Raises:

CacheWriteError – If the backend serializer cannot encode a value.

Return type:

None

async incr(key, delta=1)[source]

Increment a numeric value at the given key.

Routes to the optimal strategy based on the backend configuration detected at construction time:

  • Raw backend (no serializer): delegates to the backend’s native increment method when available. On Redis this is an atomic INCR / INCRBY command; on SimpleMemoryCache it is asyncio-safe. Falls back to GET+SET for backends that do not expose increment.

  • Serialized backend (MsgspecSerializer): uses a non-atomic GET+SET. Correct for single-process deployments; the asyncio event loop does not preempt between the two awaits within a single coroutine execution.

Parameters:
  • key (str) – Cache key holding an integer value.

  • delta (int) – Amount to increment by. Defaults to 1.

Returns:

The new value after incrementing.

Return type:

int

async delete(key)[source]

Delete a single key from the cache.

Parameters:

key (str) – Cache key to remove.

Returns:

Number of keys actually deleted (0 or 1).

Return type:

int

async delete_many(keys)[source]

Delete multiple keys from the cache.

Parameters:

keys (list[str]) – Cache keys to remove.

Returns:

Number of keys actually deleted.

Return type:

int

async clear()[source]

Remove all entries from the cache backend.

Return type:

None

async close()[source]

Release the underlying cache connection resources.

Return type:

None

class loom.core.cache.CachedCalls(*args, **kwargs)[source]

Bases: Protocol

Binder that turns declared coroutines into cached ones.

wrap(func)[source]

Return the cached form of func, or func itself.

Parameters:

func (Callable[[...], Awaitable[Any]])

Return type:

Callable[[…], Awaitable[Any]]

bind(obj)[source]

Return the public coroutine methods of obj, cached where declared.

Parameters:

obj (object)

Return type:

list[Callable[[…], Awaitable[Any]]]

class loom.core.cache.CacheBackend(*args, **kwargs)[source]

Bases: Protocol

Abstract cache backend defining the contract for key-value storage operations.

async get_value(key, *, type=None)[source]

Retrieve a value by key, optionally converting it to the given type.

Parameters:
  • key (str) – Cache key to look up.

  • type (type[T] | None) – Optional target type for deserialization.

Returns:

The cached value, converted to type when provided, or None on miss.

Return type:

T | Any | None

async set_value(key, value, ttl=None)[source]

Store a value under the given key with an optional TTL in seconds.

Parameters:
  • key (str) – Cache key.

  • value (Any) – Value to store.

  • ttl (int | None) – Time-to-live in seconds. None means no expiration.

Return type:

None

async multi_get_values(keys, *, type=None)[source]

Retrieve multiple values by their keys in a single round-trip.

Parameters:
  • keys (list[str]) – List of cache keys to look up.

  • type (type[T] | None) – Optional target type for deserialization of each value.

Returns:

A list of values in the same order as keys, with None for misses.

Return type:

list[T | Any | None]

async multi_set_values(pairs, ttl=None)[source]

Store multiple key-value pairs in a single round-trip.

Parameters:
  • pairs (list[tuple[str, Any]]) – List of (key, value) tuples to store.

  • ttl (int | None) – Time-to-live in seconds applied to all entries.

Return type:

None

async exists(key)[source]

Check whether a key exists in the cache.

Parameters:

key (str) – Cache key to check.

Returns:

True if the key is present, False otherwise.

Return type:

bool

async delete(key)[source]

Delete a single key from the cache.

Parameters:

key (str) – Cache key to remove.

Returns:

Number of keys actually deleted (0 or 1).

Return type:

int

async delete_many(keys)[source]

Delete multiple keys from the cache in a single operation.

Parameters:

keys (list[str]) – List of cache keys to remove.

Returns:

Number of keys actually deleted.

Return type:

int

async incr(key, delta=1)[source]

Atomically increment a numeric value stored at the given key.

Parameters:
  • key (str) – Cache key holding an integer value.

  • delta (int) – Amount to increment by. Defaults to 1.

Returns:

The new value after incrementing.

Return type:

int

async close()[source]

Release any resources held by the backend (connections, pools, etc.).

Return type:

None

class loom.core.cache.CacheConfig(*, enabled=True, aiocache_alias='default', counter_alias=None, aiocache_config=<factory>, default_ttl=200, default_list_ttl=120, ttl_jitter=0.1, ttl=<factory>, max_size=None)[source]

Bases: LoomFrozenStruct

Configuration for cache behaviour including TTLs and aiocache backend settings.

Unknown keys are rejected: a misspelt override (ttls:) would otherwise disable every TTL in silence. The aiocache: short key that from_mapping() accepts is therefore not read from YAML; declare aiocache_config:.

Parameters:
enabled

Global toggle for cache operations.

Type:

bool

aiocache_alias

Named alias for the aiocache data backend. The data backend must be configured with MsgspecSerializer.

Type:

str

counter_alias

Named alias for the counter backend used by GenerationalDependencyResolver. This backend must have no serializer so that native atomic increment operations (Redis INCR, SimpleMemoryCache.increment) work correctly. Defaults to None, meaning the same alias as aiocache_alias is used (safe for single-process deployments; uses a non-atomic GET+SET fallback automatically).

Type:

str | None

aiocache_config

Raw configuration mapping forwarded to aiocache. Keyed by alias name. Use apply_config to have max_size injected automatically for memory backends.

Type:

dict[str, Any]

default_ttl

Default TTL in seconds for single-entity lookups.

Type:

int

default_list_ttl

Default TTL in seconds for list / index queries.

Type:

int

ttl_jitter

Fraction of the TTL used as a random spread on every cache write, in [0, 1). A write receives its TTL multiplied by a random factor in [1 - ttl_jitter, 1 + ttl_jitter] (never below one second), so entries populated in the same burst do not expire at the same instant. 0 disables the spread.

Type:

float

ttl

Per-entity TTL overrides keyed by entity name. Append _list for list overrides (e.g. {"user": 300, "user_list": 150}).

Type:

dict[str, int]

max_size

Maximum number of entries for aiocache.SimpleMemoryCache backends. Injected automatically when calling apply_config(). Has no effect on Redis or other non-memory backends.

Type:

int | None

Example YAML (Redis + separate counter backend):

cache:
  aiocache_alias: cache
  counter_alias: counters
  default_ttl: 300
  default_list_ttl: 120
  ttl_jitter: 0.1
  max_size: 1000
  ttl:
    user: 600
    user_list: 300
  aiocache_config:
    cache:
      cache: aiocache.RedisCache
      endpoint: ${oc.env:REDIS_HOST,redis}
      port: 6379
      namespace: myapp
      serializer:
        class: loom.core.cache.serializer.MsgspecSerializer
    counters:
      cache: aiocache.RedisCache
      endpoint: ${oc.env:REDIS_HOST,redis}
      port: 6379
      namespace: myapp_counters
      # no serializer — raw integer storage, atomic Redis INCR

Example YAML (in-memory for development):

cache:
  aiocache_alias: cache
  counter_alias: counters
  max_size: 500
  aiocache_config:
    cache:
      cache: aiocache.SimpleMemoryCache
      serializer:
        class: loom.core.cache.serializer.MsgspecSerializer
    counters:
      cache: aiocache.SimpleMemoryCache
      # no serializer
property effective_counter_alias: str

Return the resolved counter alias, falling back to aiocache_alias.

classmethod from_mapping(data)[source]

Create a CacheConfig from a raw dictionary (e.g. parsed TOML/YAML).

Parameters:

data (dict[str, Any]) – Flat or nested mapping with cache configuration values.

Returns:

A validated CacheConfig instance.

Return type:

CacheConfig

ttl_for_single(entity)[source]

Resolve the effective TTL for a single-entity lookup.

Parameters:

entity (str) – Normalized entity name (e.g. "user").

Returns:

TTL value in seconds.

Return type:

int

ttl_for_list(entity)[source]

Resolve the effective TTL for a list or index query.

Parameters:

entity (str) – Normalized entity name (e.g. "user").

Returns:

TTL value in seconds.

Return type:

int

exception loom.core.cache.CacheWriteError[source]

Bases: ValueError

A value could not be serialised for the cache; nothing was stored.

class loom.core.cache.CachedRepository(repository, *, config, cache, dependency_resolver)[source]

Bases: Repository[OutputT, CreateT, UpdateT, IdT], Generic[OutputT, CreateT, UpdateT, IdT]

Cache-aside wrapper with generational invalidation.

Attributes the wrapper does not define pass through to the wrapped repository, so its capability set (create_many, count, custom queries) is exactly the wrapped one.

The cached list path reloads the entities missing from a warm index with a single <primary key> IN (...) query, so the wrapped repository should support IN on the primary key; a repository whose allowed_filter_fields excludes the primary key is detected and served with per-id reads instead. The key is read by the attribute name the model declares (id when there is no model or the model declares no primary key).

Concurrent misses of the same key on the entity read and on a @cache_query read are coalesced inside the process: the first caller loads and the rest await that result, so a burst on a hot key costs one repository call. Two conditions bound that:

  • The wrapper must be application-scoped. A wrapper built per request has a coalescing group of one, so it never coalesces anything and only pays the bookkeeping.

  • Coalescing is skipped while the wrapped repository reports a caller-scoped session (has_caller_scoped_session()), which is what a @transactional scope or a unit of work binds. A coalesced load runs in its own task and outlives the caller that started it, so inside a transaction it would query a session already closed by that caller’s teardown, and would serve another caller the uncommitted writes of the first. Inside a transaction the read runs inline.

Callers that miss together are served the same object by the coalesced load, on the entity read as on a @cache_query read, while a caller served from the cache gets a freshly decoded one; treat a cached result as immutable, since a BaseModel struct is mutable unless declared frozen.

The cached list and query reads are deliberately left out of the coalescing: their miss path is not a pure loader — it caches the page’s entities as a side effect — and its result feeds _load_items_from_index, which re-enters the coalesced entity read, so the herd is already collapsed one level down.

Every TTL is spread inside ttl_jitter as it reaches the backend, so two write calls do not choose the same expiry. The spread is per write call: a batch write (a cached page, an index refill) carries one TTL for the whole batch because CacheBackend takes one TTL per call, so the rows of a single page still expire together.

Parameters:
property entity_name: str

Normalized name of the cached entity.

async get_by_id(obj_id, profile='default')[source]

Fetch a single entity by its primary key.

Parameters:
  • obj_id (IdT) – Primary key of the entity.

  • profile (str) – Loading profile name for eager-load options.

Returns:

The entity output struct, or None if not found.

Return type:

OutputT | None

async get_by(field, value, profile='default')[source]

Fetch one entity by arbitrary field.

This path intentionally delegates to the wrapped repository without cache-aside behavior for now. Field-based lookups can target mutable columns and the cache invalidation surface is broader than id-based access; keeping it uncached preserves correctness while the lookup cache policy is designed explicitly.

Parameters:
Return type:

OutputT | None

async exists_by(field, value)[source]

Check existence by arbitrary field.

Existence checks are delegated directly to the wrapped repository to avoid stale negative/positive cache entries on mutable fields.

Parameters:
Return type:

bool

async count()[source]

Count every entity, forwarded uncached to the wrapped repository.

Counts are not cached: a total changes on every write and a stale value is worse than the single round trip.

Return type:

int

async list_paginated(page_params, filter_params=None, profile='default')[source]

Fetch a paginated list of entities.

Parameters:
  • page_params (PageParams) – Pagination parameters (page and limit).

  • filter_params (FilterParams | None) – Optional filter criteria.

  • profile (str) – Loading profile name for eager-load options.

Returns:

A PageResult with the matching items and pagination metadata.

Return type:

PageResult[OutputT]

async list_with_query(query, profile='default')[source]

Fetch entities using a structured QuerySpec.

Supports both offset and cursor pagination, structured filters, and explicit sort directives. The concrete return type depends on query.pagination:

Parameters:
  • query (QuerySpec) – Structured query specification.

  • profile (str) – Loading profile name for eager-load options.

Returns:

A PageResult for offset queries or a CursorResult for cursor queries.

Return type:

PageResult[OutputT] | CursorResult[OutputT]

async create(data)[source]

Persist a new entity.

Parameters:

data (CreateT) – Creation payload struct.

Returns:

The newly created entity output struct.

Return type:

OutputT

async update(obj_id, data)[source]

Apply a partial update to an existing entity.

Parameters:
  • obj_id (IdT) – Primary key of the entity to update.

  • data (UpdateT) – Partial update payload struct.

Returns:

The updated entity output struct, or None if not found.

Return type:

OutputT | None

async delete(obj_id)[source]

Delete an entity by its primary key.

Parameters:

obj_id (IdT) – Primary key of the entity to delete.

Returns:

True if the entity was deleted, False if not found.

Return type:

bool

async on_transaction_committed(events)[source]

Bump the tags of the mixins’ events and forward them downstream.

Under @transactional the mixins’ tagged events arrive here in the plain post-commit lane, while the wrapper’s own event runs in the shielded priority lane queued by create/update/delete. Both bumps are wanted: they are idempotent, and only this lane carries the relation/projection tags the wrapper cannot compute.

Parameters:

events (tuple[MutationEvent, ...]) – Mutation events committed by the wrapped repository.

Return type:

None

class loom.core.cache.DependencyResolver(*args, **kwargs)[source]

Bases: Protocol

Protocol for cache dependency tracking and invalidation.

async fingerprint(tags)[source]

Compute a composite fingerprint from the current generation of each tag.

Parameters:

tags (list[str]) – Dependency tag names.

Returns:

A stable hash string representing the combined tag state.

Return type:

str

async bump_from_events(events)[source]

Increment generation counters for the tags affected by mutation events.

A resolver implemented outside the framework keeps whatever granularity it implements; see GenerationalDependencyResolver for the built-in policy.

Parameters:

events (tuple[MutationEvent, ...]) – Mutation events produced within a transaction.

Return type:

None

entity_tags(entity, entity_id)[source]

Return dependency tags for a single entity lookup.

Parameters:
  • entity (str) – Normalized entity name.

  • entity_id (object | None) – Primary key of the entity, or None.

Returns:

List of tag names that should be tracked for this entity.

Return type:

list[str]

list_tags(entity, filter_fingerprint)[source]

Return dependency tags for a list/index query.

Parameters:
  • entity (str) – Normalized entity name.

  • filter_fingerprint (str) – Hash of the applied filter parameters.

Returns:

List of tag names that should be tracked for this list query.

Return type:

list[str]

class loom.core.cache.GenerationalDependencyResolver(cache)[source]

Bases: DependencyResolver, BatchFingerprintResolver

Generational tags with monotonic counters in cache backend.

A mutation bumps only the tags it affects (see bump_from_events()), so updating one row leaves every other cached row of the same entity warm. The bare entity tag stays in every key’s tag list but the framework never bumps it: it is the manual entity-wide flush handle. An operator increments tag:<entity> by hand to evict every key of that entity at once.

Parameters:

cache (CacheBackend)

async fingerprint(tags)[source]

Compute a composite fingerprint from generation counters of all tags.

Parameters:

tags (list[str]) – Dependency tag names.

Returns:

A stable hash representing the combined tag generation state.

Return type:

str

async fingerprint_many(tag_groups)[source]

Compute one fingerprint per tag group, reading every counter once.

The distinct tags of every group are read together, in batches, so the cost is one round trip per COUNTER_BATCH_SIZE distinct tags instead of one per group.

Parameters:

tag_groups (Sequence[list[str]]) – Dependency tag names, grouped per entity.

Returns:

One fingerprint per group, in input order; each value is identical to what fingerprint() returns for the same group.

Return type:

list[str]

async bump_from_events(events)[source]

Increment generation counters for the tags affected by mutation events.

Every event bumps entity:list and entity:id:<k> per id; the event’s own tags are bumped as they come. The bare entity tag is never bumped by the framework: it is reserved for a manual entity-wide flush.

The counters are bumped concurrently rather than one round trip at a time: with K distinct tags, a sequential loop pays K round trips in series, while awaiting them together pays roughly one round trip regardless of K once the backend is network-bound (see the benchmark referenced in the PR that introduced this).

Parameters:

events (tuple[MutationEvent, ...]) – Mutation events to process.

Return type:

None

entity_tags(entity, entity_id)[source]

Return dependency tags for a single entity lookup.

Parameters:
  • entity (str) – Normalized entity name.

  • entity_id (object | None) – Primary key of the entity, or None.

Returns:

List of tag names for this entity.

Return type:

list[str]

list_tags(entity, filter_fingerprint)[source]

Return dependency tags for a list/index query.

Parameters:
  • entity (str) – Normalized entity name.

  • filter_fingerprint (str) – Hash of the applied filter parameters.

Returns:

List of tag names for this list query.

Return type:

list[str]

class loom.core.cache.MsgspecSerializer[source]

Bases: object

aiocache serializer backed by msgspec msgpack.

dumps(value)[source]

Serialize a Python object to MessagePack bytes for cache storage.

Parameters:

value (Any) – Object to serialize.

Returns:

MessagePack-encoded bytes.

Return type:

bytes

loads(value)[source]

Deserialize MessagePack bytes back into a Python object.

Parameters:

value (bytes | None) – Raw bytes from cache, or None.

Returns:

The decoded Python object, or None if input is None.

Return type:

Any

loom.core.cache.cache_call(*, ttl_key=None, unless=None, version=1)[source]

Mark a coroutine whose result a bound CachedCalls may store.

The decorator only declares: it writes the policy on the function and returns the very same object, so the module imports with no configuration and the coroutine stays importable and unit-testable on its own. The composition root binds it later.

The coroutine must be a pure function of its arguments: it may not read ambient identity — a contextvar tenant, a caller’s credential — and may not hold a caller-scoped session. The key sees only the arguments, and the load is detached into its own task, so a coroutine that reads ambient state serves one caller’s answer to another.

A cached call is a TTL cache with no invalidation: unlike a repository read it carries no dependency tags, because loom cannot know what a coroutine depends on. It expires, or the caller bumps version.

Parameters:
  • ttl_key (str | None) – Key whose ttl: override applies. It shares the namespace with entity TTLs, so a key equal to an entity name deliberately shares that entity’s override.

  • unless (Callable[[Any], bool] | None) – Predicate over the result; truthy means the result is returned and nothing is stored. An empty answer from a rate-limited service is the case it exists for.

  • version (int) – Bump to invalidate every entry this function already wrote.

Returns:

The decorator that marks the coroutine.

Raises:

TypeError – The decorated object is not a coroutine function. A cached call is awaited once and its single result is stored, which a plain function, a generator and an async generator cannot honour.

Return type:

Callable[[F], F]

loom.core.cache.cached_calls(container)[source]

Return the container’s binder, or a pass-through when it has none.

A container built outside the two bootstraps that apply the cache module has no binding, and LoomContainer.resolve() raises for one. A factory called with such a container gets the announcing pass-through instead, so it never has to guess how its application was built.

Parameters:

container (LoomContainer) – Container handed to the factory.

Returns:

The registered binder, or a fresh pass-through.

Return type:

CachedCalls

loom.core.cache.cache_query(*, scope='list', ttl_key=None)[source]

Declarative marker for custom repository read methods.

Annotate the return type. The wrapper derives a codec from it and applies it to the cached read and to the fresh one alike, so a hit and a miss return the same type; the supported grammar is a msgspec.Struct, a scalar, or a list, tuple or optional of those. A return type outside it — a mapping, a generic container, Any, a forward reference the defining module cannot resolve — emits a DeprecationWarning when the repository is wrapped, and keeps the old behaviour, where the cached call returns the decoded payload rather than the declared type.

The declared type is what the caller gets, on the fresh call as on the cached one: a method annotated -> Stats that returns a subclass of Stats hands back a narrowed Stats, and the fields the subclass added are dropped. Declare the type you mean to return.

A method that returns None is not cached: the backend cannot tell a stored None from a miss, so the read runs again next time. A cached payload that no longer fits the declared type — an older deployment wrote it, and the type has since gained a field — is treated as a miss and overwritten, not raised to the caller.

Treat the returned value as immutable. Concurrent callers that miss together are served the same object by the coalesced load, while a caller served from the cache gets a freshly decoded one, so mutating a result makes the two paths disagree. Return a struct, or a fresh copy.

scope="entity" requires the model’s primary key as the first positional argument (a keyword argument does not count); the wrapper raises TypeError otherwise, before touching the cache backend. When the primary-key type resolves to a plain class the argument must be of that exact type — a datetime for a date key or a bool for an int key is rejected — while a key declared int | None resolves no class and gets no call-time validation. A read keyed by any other field is a list-scoped read.

Parameters:
  • scope (str) – "entity" for a single-entity read, "list" otherwise; decides which tags invalidate the entry and which TTL applies.

  • ttl_key (str | None) – Entity name whose TTL override applies, when the method caches something other than its own entity.

Returns:

The decorator that marks the method.

Return type:

Callable[[F], F]

loom.core.cache.cached(cls)[source]

Declarative marker for repositories that support cache wrapping.

Parameters:

cls (T)

Return type:

T