loom.core.cache.repository¶
Functions
|
Return the |
|
Auto-infer |
|
Auto-infer |
|
Whether attr is a coroutine function marked with |
|
Return the public method names protocol and its bases declare. |
|
Check that a |
|
Resolve the primary key of the wrapped repository's model. |
Classes
|
Cache-aside wrapper with generational invalidation. |
|
|
|
|
|
Primary key of the cached model as the wrapper reads it. |
|
- class loom.core.cache.repository.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 supportINon the primary key; a repository whoseallowed_filter_fieldsexcludes the primary key is detected and served with per-id reads instead, and so is a model with a composite primary key, which a filter on one field cannot match. The key is read by the attribute names the model declares (idwhen there is no model or the model declares no primary key); a composite key is the tuple of its values in declaration order.Concurrent misses of the same key on the entity read and on a
@cache_queryread 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@transactionalscope 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_queryread, while a caller served from the cache gets a freshly decoded one; treat a cached result as immutable, since aBaseModelstruct 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_jitteras 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 becauseCacheBackendtakes one TTL per call, so the rows of a single page still expire together.- Parameters:
repository (Repository[OutputT, CreateT, UpdateT, IdT])
config (CacheConfig)
cache (CacheBackend)
dependency_resolver (DependencyResolver)
- 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
Noneif 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.
- 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.
- 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:
- 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
PageResultwith 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:PaginationMode.OFFSET→PageResultPaginationMode.CURSOR→CursorResult
- Parameters:
- Returns:
A
PageResultfor offset queries or aCursorResultfor 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
Noneif 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:
Trueif the entity was deleted,Falseif not found.- Return type:
- async on_transaction_committed(events)[source]¶
Bump the tags of the mixins’ events and forward them downstream.
Under
@transactionalthe mixins’ tagged events arrive here in the plain post-commit lane, while the wrapper’s own event runs in the shielded priority lane queued bycreate/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