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: BucketsError

Raised 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.BucketEventHistoryRepository while preserving translated recovery context for callers.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
append_bucket_event(catalogue, event)[source]

Return a new BucketEventHistoryCatalogue with event inserted.

Content-addressed: a re-emission of the same BucketEvent collapses to the same event_id and 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_version is the one field derive_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:

BucketEventHistoryCatalogue

Parameters:
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_id rather 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 loss emit_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:

SecureObjectWrite

Parameters:
build_bucket_event(*, bucket_id, event_type, occurred_at, actor, object_type, object_id, payload, payload_version)[source]

Derive one BucketEvent without 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 with append_bucket_event(), and hand the resulting catalogue to BucketEventHistoryRepositoryProtocol.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 of payload for this event family.

Return type:

BucketEvent

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_version is 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 of payload for this event family.

Return type:

BucketEvent

Returns:

The appended BucketEvent.

emit_bucket_events(*, repository, events)[source]

Append every event in events to 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:
Return type:

None