cadrumo.domain.buckets.event_repository module¶
Domain boundary error, pure helper, and emit primitive for the event catalogue.
BucketEventHistoryPersistenceError is the storage-boundary error the
persistence adapter raises when the encrypted
BucketEventHistoryCatalogue cannot be loaded or persisted safely;
append_bucket_event() is the pure insertion helper over a catalogue, and
emit_bucket_event() is the derive-append-save primitive every emitting
domain shares. The concrete encrypted-SQL repository lives in the persistence
adapter
(cadrumo.adapters.persistence.profile.buckets.BucketEventHistoryRepository),
behind BucketEventHistoryRepositoryProtocol; this module owns only the
domain error, the pure helper, and the primitive that composes them.
- exception BucketEventHistoryPersistenceError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
BucketsErrorRaised when the bucket-event-history catalogue cannot be persisted or loaded.
This wraps storage-boundary failures from the persistence adapter’s
cadrumo.adapters.persistence.profile.buckets.BucketEventHistoryRepositorywhile preserving translated recovery context for callers.- Parameters:
- Return type:
None
- code¶
- append_bucket_event(catalogue, event)[source]¶
Return a new
BucketEventHistoryCataloguewitheventinserted.Content-addressed: a re-emission of the same
BucketEventcollapses to the sameevent_idand the existing entry is left in place.That is what this always claimed and what it now does. It previously assigned unconditionally, which is indistinguishable from collapsing for an identical event – and that equivalence is exactly why the gap held.
payload_versionis the one fieldderive_bucket_event_id()does not hash, so two events could share an id while disagreeing about their payload contract, and the later one silently replaced an immutable audit revision. The catalogue afterwards looked like one content-addressed record, so nothing downstream could tell a revision had been erased.A colliding id carrying a different version fails closed rather than being folded in. Deriving the id from the version instead would make the two records distinct, but then a caller that merely mis-stated the version would append a duplicate of an event that already exists – a quieter wrong answer than a refusal naming both versions.
- Return type:
- Parameters:
catalogue (BucketEventHistoryCatalogue)
event (BucketEvent)
- bucket_event_history_write(repository, events)[source]¶
Return the catalogue write appending
events, without committing it.The commit half of the co-emission pattern
build_bucket_event()exists for. A mutation that saves its state and emits afterwards can come to rest durable-but-unrecorded – the state survives while the history has no matching entry and no retryable marker names the gap. Folding this write into the owning catalogue’s batch puts the state and the event it promises in one SQL unit of work, so neither can land without the other.Appending is idempotent on content, so re-deriving the same event collapses onto the same
event_idrather than duplicating the entry.Lives here rather than in any one emitting domain because the shape is not domain-specific: every emitter that needs its event to share a transaction with its state change needs exactly this, and a per-domain copy is how one of them silently drifts from the append contract.
The read is REVISIONED, and that half was missing. Composing from a plain
load()writes back the revision it read, so an event another writer appended between the read and the batch is discarded – the singleton-row lossemit_bucket_event()avoids by appending under the guard, landing on the one shape that cannot use it. Carrying the observed revision turns that silent discard into a refusal the caller can retry, which is the weaker outcome only until you notice the discard leaves nothing to notice: the surviving content-addressed events are all internally consistent and the missing one leaves no gap.A caller wanting the retry re-runs this composition against the newly current catalogue; it is a pure function of what it reads, so re-running is safe.
- Return type:
- Parameters:
repository (BucketEventHistoryRepositoryProtocol)
events (tuple[BucketEvent, ...])
- build_bucket_event(*, bucket_id, event_type, occurred_at, actor, object_type, object_id, payload, payload_version)[source]¶
Derive one
BucketEventwithout persisting it.The derive half of
emit_bucket_event(), split out so a caller that must commit the event in the SAME unit of work as the state change it records can obtain the event, fold it into a catalogue withappend_bucket_event(), and hand the resulting catalogue toBucketEventHistoryRepositoryProtocol.to_secure_object_write()for co-emission. A mutation that saves its catalogues first and emits afterwards can come to rest with durable state and no history entry; deriving without saving is what lets the two share one transaction.- Parameters:
bucket_id (
str) – Owning bucket identifier.event_type (
BucketEventType) – The transition being recorded.occurred_at (
datetime) – UTC timestamp of the transition. Callers that need a retry to collapse onto one entry pass a stored timestamp rather than a fresh clock read, since the id is derived from it.actor (
str) – Actor label; leading and trailing whitespace is stripped.object_type (
BucketEventObjectType) – Kind of the affected domain object.object_id (
str) – Stable identifier of the affected object.payload (
Mapping[str,str]) – Compact structured detail. Never secrets or key material.payload_version (
int) – Schema version ofpayloadfor this event family.
- Return type:
- Returns:
The derived
BucketEvent, not yet appended or persisted.
- emit_bucket_event(*, repository, bucket_id, event_type, occurred_at, actor, object_type, object_id, payload, payload_version)[source]¶
Derive, append, and persist one
BucketEvent, returning it.Every emitting domain shares this primitive rather than re-deriving the id-build-append-save sequence, so an emission cannot drift from the content-addressed id contract in
derive_bucket_event_id(). The returned event is the durable bucket-scoped audit pointer for the mutation; payloads stay compact and reference the owning record instead of duplicating it.payload_versionis required, not defaulted. It is the ONLY field that does not participate in the derived id, so a wrong value cannot be caught by the id check and would silently misdeclare the payload contract of already-written events. Each emitting domain versions its own payload independently, so there is no project-wide default that would be correct here; a domain that wants one supplies it in its own narrow wrapper.- Parameters:
repository (
BucketEventHistoryRepositoryProtocol) – Port over the bucket-event-history catalogue.bucket_id (
str) – Owning bucket identifier.event_type (
BucketEventType) – The transition being recorded.occurred_at (
datetime) – UTC timestamp of the transition. Callers that need a retry to collapse onto one entry pass a stored timestamp rather than a fresh clock read, since the id is derived from it.actor (
str) – Actor label; leading and trailing whitespace is stripped.object_type (
BucketEventObjectType) – Kind of the affected domain object.object_id (
str) – Stable identifier of the affected object.payload (
Mapping[str,str]) – Compact structured detail. Never secrets or key material.payload_version (
int) – Schema version ofpayloadfor this event family.
- Return type:
- Returns:
The appended
BucketEvent.
- emit_bucket_events(*, repository, events)[source]¶
Append every event in
eventsto the catalogue in ONE round-trip.The plural counterpart of
emit_bucket_event(), for a caller that records several transitions belonging to one atomic mutation. Emitting them one at a time costs a full catalogue load and save per event, and the catalogue grows without bound, so an N-event mutation paid N decrypt/encrypt cycles over a monotonically larger payload.This does NOT collapse the audit trail: each event is appended individually through
append_bucket_event(), so N distinct transitions remain N distinct catalogue entries with their own content-addressed ids. Only the number of catalogue round-trips changes. Two byte-identical events still collapse onto one id, exactly as they do when emitted separately – that is the content-addressing contract, not an effect of batching.Events are derived by the caller (through
build_bucket_event()) rather than here: a batch’s events differ in type and payload, so there is no single set of derive arguments to accept.- Parameters:
repository (
BucketEventHistoryRepositoryProtocol) – Port over the bucket-event-history catalogue.events (
Sequence[BucketEvent]) – The already-derived events to append. An empty sequence is a no-op and does not touch the catalogue.
- Return type: