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. The key is read by the attribute name the model declares (idwhen 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_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