cadrumo.adapters.persistence.storage.envelope.secure_bound_repository module

Generic SQL-backed envelope repository for secure objects.

Concrete domain and application repositories that wrap adapters.persistence.storage.sql.secure_objects.SecureObjectRepository all share the same boilerplate: adapters.persistence.storage.envelope.contract.Envelope wrapping, namespace, sensitivity, schema-version, Pydantic payload type, and a function that extracts the natural id from the payload.

This module provides adapters.persistence.storage.envelope.secure_bound_repository.SecureBoundRepository, a generic base class that captures that shared shape exactly once. Concrete subclasses override the four class-level descriptors (namespace, payload_type, sensitivity, schema_version) and implement extract_identifier; they inherit envelope_path_for, lock_target_for, load, save, to_secure_object_write, delete, iter_ids, and iter_records for free.

The base class does NOT replace adapters.persistence.storage.sql.secure_objects.SecureObjectRepository; it composes one.

class SecureBoundRepository(*, bucket_id=None, objects=None, settings=None)[source]

Bases: Generic

Generic repository over encrypted SQL-backed envelopes for one payload type.

Subclasses MUST set class attributes:

Subclasses MUST implement extract_identifier() so that save() and iter_ids() can recover the natural id from the decrypted payload.

Parameters:
namespace: ClassVar[str]
sensitivity: ClassVar[SensitivityClass]
schema_version: ClassVar[int]
payload_type: ClassVar[type[BaseModel]]
extract_identifier(payload)[source]

Return the natural id for payload (used as the SQL object key).

Subclasses MUST override. The base implementation raises NotImplementedError.

Return type:

str

Parameters:

payload (T)

classmethod payload_model()[source]

Return the concrete Pydantic payload model for this repository.

Return type:

type[T]

property store_dir: Path

Return a logical backend marker for diagnostic CLI output.

envelope_path_for(identifier)[source]

Return a logical path marker for identifier.

Return type:

Path

Parameters:

identifier (str)

lock_target_for(identifier)[source]

Return a logical lock-target marker for identifier.

SQL transactions govern actual write atomicity; this is only surfaced for diagnostic parity with file-backed repositories.

Return type:

Path

Parameters:

identifier (str)

property secure_object_repository: SecureObjectRepository

Return the concrete secure-object backend.

Returns:

The adapters.persistence.storage.sql.secure_objects.SecureObjectRepository backing this logical repository.

load(identifier)[source]

Return the persisted payload or None if absent.

The payload’s own natural identity is compared with the key it was looked up under. save derives the row key from the payload via extract_identifier(), so the two are meant to be one fact in two encodings – but nothing on the read path checked that, and a row written under a different key returned its payload unremarked. A caller asking for one record was handed another and had no way to tell: the returned object describes itself truthfully, it simply is not the one that was requested.

Raises:
  • ClassificationError – Refused by the shared envelope gate.

  • EnvelopeVersionError – Refused by the shared envelope gate.

  • SecureObjectRowIdentityError – The payload’s natural identity differs from identifier. Raised rather than returning None, because the row exists – reporting it absent would hide a real inconsistency behind an ordinary miss.

Return type:

T | None

Parameters:

identifier (str)

save(payload)[source]

Persist payload as an encrypted envelope row.

The natural id is recovered from the payload via extract_identifier().

Return type:

None

Parameters:

payload (T)

to_secure_object_write(payload, *, expected_revision_id=None)[source]

Return the prepared upsert for payload without committing it.

The caller hands the returned SecureObjectWrite to adapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.save_many() (or apply_batch) alongside a sibling repository’s write, so both land in ONE SQL unit of work and a crash between them is impossible. That is the same co-emit shape the filing path already uses to keep the participation index from drifting from the filing catalogue; the catalogue repositories expose to_secure_object_write for exactly this, and this method extends it to every envelope-bound repository.

The write carries the identical adapters.persistence.storage.envelope.contract.Envelope, classification and schema version save() would persist directly — both build it through _identified_envelope(), so the committed and the prepared forms cannot drift apart.

Return type:

SecureObjectWrite

Parameters:
  • payload (T)

  • expected_revision_id (str | None)

to_secure_object_deletion(identifier)[source]

Return the prepared removal for identifier without committing it.

The deletion counterpart of to_secure_object_write(), so a caller that must remove some rows and write others can hand both to adapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.apply_batch() and land the whole change in ONE SQL unit of work. adapters.persistence.storage.sql.secure_object_records.SecureObjectDeletion addresses a row by its stored HMAC digest, so the natural id is bound here with the same digest the storage column uses.

Return type:

SecureObjectDeletion

Parameters:

identifier (str)

replace_records(replacements, stale_identifiers)[source]

Commit replacements and remove stale_identifiers in one transaction.

The atomic set-replace primitive: a caller that must clear a window of rows and write its successor set gets all-or-nothing semantics instead of a delete loop followed by a save loop, where a failure part-way through leaves the store holding neither the old set nor the new one.

An identifier that is BOTH stale and replaced is written, not deleted: adapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.apply_batch() applies writes before deletions, so a row carried across the replacement would otherwise be upserted and then removed in the same transaction.

Parameters:
  • replacements (Sequence[T]) – The payloads that constitute the new set.

  • stale_identifiers (Iterable[str]) – Natural ids of rows to remove. Ids also carried by replacements are ignored.

Return type:

None

delete(identifier)[source]

Remove the row for identifier; return whether a row was deleted.

Return type:

bool

Parameters:

identifier (str)

iter_ids()[source]

Yield every persisted identifier in storage order.

Order is the secure-object storage order (the object_key digest order), not the natural-id order: a caller that needs a specific order sorts the result itself. Streams one identifier at a time rather than buffering and sorting the whole namespace in memory.

Every id is the one its own row is filed under: the scan refuses a row whose payload rebuilds a different key rather than reporting an id the store does not actually hold that record at.

Raises:

SecureObjectRowIdentityError – A row’s payload rebuilds a different natural id than the key it is filed under.

Return type:

Iterator[str]

iter_records()[source]

Yield every persisted payload whose identity matches the key it is filed under.

Streams each payload straight from adapters.persistence.storage.sql.secure_objects.SecureObjectRepository.list_records() without buffering the whole namespace or sorting it in memory. Order is storage-defined (the object_key digest order), not the natural-id order; a caller that needs a specific order sorts the result itself.

Parses each row’s decrypted payload bytes directly from list_records rather than routing through load(). The SQL WHERE object_key = ? lookup inside load() cannot match the stored ciphertext when object_key is an randomized ciphertext column (AES-256-GCM uses a random nonce, so the bind-parameter ciphertext differs from the stored ciphertext every time). Iterating directly over the decrypted rows is the correct pattern for full-scan enumeration.

Verification is the DEFAULT, not an opt-in a caller must remember: a scan that filters on payload fields trusts each record to describe the key it is stored under, and the callers most exposed to that – window counts, carry-forward reads, reconciliation history – are the ones least able to notice a foreign row. An opt-in variant only protects the callers that already suspected the problem.

Raises:

SecureObjectRowIdentityError – A row’s payload rebuilds a different natural id than the key it is filed under.

Return type:

Iterator[T]