cadrumo.adapters.persistence.profile._secure_enveloped_document module¶
Canonical encrypted persistence kernel for Envelope-wrapped profile singleton documents.
Both kernels read and write through a SecureObjectRepository supplied
at construction; neither opens storage of its own.
Sibling of ProfileBareModelSecurePersistence
(_secure_model_document.py). The two kernels cover two DIFFERENT on-disk wire
shapes that both happen to persist “one whole singleton Pydantic document per
profile bucket” — they are not interchangeable, and a namespace’s existing shape
decides which kernel it enrolls in, never the other way round:
ProfileBareModelSecurePersistencestores the document’s ownmodel_dump_json()bytes directly. Classification, schema version, and write timestamp live ONLY as columns on the encrypted SQL row (SecureObjectWrite); nothing is duplicated inside the JSON payload.ProfileEnvelopedModelSecurePersistence(this module) wraps the document inEnvelopebefore serialising, so classification/schema-version/written-at are duplicated BOTH as SQL-row columns AND as fields inside the stored JSON. The inner fields are re-checked against the consumer’s expectation on every load, as defense-in-depth against a row whose column metadata and embedded payload metadata have drifted apart.
Twelve profile repositories predate either kernel and hand-rolled the
Envelope-wrapped shape independently: this module is the shared home for that
shape now, so a namespace already carrying Envelope-wrapped rows on disk can
enroll in a kernel WITHOUT a wire-format break. Picking
ProfileBareModelSecurePersistence for one of those namespaces would
silently change what bytes get written and, symmetrically, would fail to parse
the Envelope-wrapped rows already on disk for that namespace — a real
behaviour change, not a refactor. A brand-new namespace with no format to
preserve should default to the leaner ProfileBareModelSecurePersistence
instead of adopting this one; this module exists to converge EXISTING
Envelope-wrapped repositories on one implementation, not to grow the
Envelope-wrapped shape’s footprint.
- class ProfileEnvelopedModelSecurePersistence(*, objects, definition, model_type, empty_document, serialization_context=None)[source]¶
Bases:
GenericPersist one Envelope-wrapped strict Pydantic document through a governed secure object.
The namespace definition remains the authority for object key, sensitivity, and schema version.
savedelegates toto_secure_object_write()so an ordinary singleton save and a caller-composed co-commit write build the identicalEnvelopebytes and cannot drift apart.- Parameters:
objects (SecureObjectRepository)
definition (SecureObjectNamespaceDefinition)
model_type (type[DocumentT])
empty_document (Callable[[], DocumentT])
- logical_path(marker)[source]¶
Return the registry-defined logical marker for one repository surface.
- Return type:
Path- Parameters:
marker (str)
- exists()[source]¶
Report whether the singleton object has been persisted.
Checks the encrypted store for an object under this namespace and key without decrypting or validating it, so
Trueattests to presence only, not integrity.- Return type:
- load()[source]¶
Load, decrypt, and unwrap the persisted document, or return its declared empty form.
The outer SQL-row classification/schema-version columns are checked by the underlying
expected_class/max_supported_versionload arguments; the innerEnvelopefields are re-checked independently as defense-in-depth against a row whose embedded payload metadata has drifted from its own columns.- Raises:
ClassificationError – The inner envelope’s classification disagrees with this repository’s declared sensitivity.
EnvelopeVersionError – The inner envelope’s schema version is not the consumer’s current version.
- Return type:
DocumentT
- load_revisioned(*, validation_context=None)[source]¶
Return the stored document and the revision id it was read at.
The public read for a caller composing a GUARDED co-commit: it cannot use
mutate(), whose write commits on its own, but it needs the same revision to carry on its write. Absent rows report theABSENT_SECURE_OBJECT_REVISION_IDsentinel, so the first writer of a singleton is guarded exactly like every later one.
- to_secure_object_write(document, *, expected_revision_id=None)[source]¶
Prepare the Envelope-wrapped encrypted-SQL upsert without committing it.
Callers that need to co-commit this document with sibling secure objects pass the returned value into their existing
save_manytransaction. The returned write carries the identicalEnvelopebytessave()would persist directly.expected_revision_idis the compare-and-swap half, and without it a co-commit carries the same lost updatemutate()exists to prevent: these documents are SINGLETONS, so composing from an unguarded read writes back the whole document and discards any entry another caller added in between. Pass the revisionload_revisioned()reported and re-run the composition if the substrate refuses the write. It stays optional because a caller writing a document it did not derive from a read has no revision to assert – but a caller that DID read one and omits it is choosing the silent discard.- Return type:
SecureObjectWrite
- Parameters:
document (DocumentT)
expected_revision_id (str | None)
- save(document)[source]¶
Encrypt, wrap in an Envelope, and save one document.
- Return type:
None
- Parameters:
document (DocumentT)
- mutate(mutation, *, attempts=4)[source]¶
Apply
mutationto the stored document as one guarded unit of work.These documents are SINGLETONS: every entry lives in one encrypted row, so “add one entry” is really read-whole-document, rebuild, write whole document. Performed unguarded, two callers adding DIFFERENT entries both read the same document and the later write silently discards the earlier caller’s entry – a lost update, and not a conflict any uniqueness check would notice, because the two entries never met. On a financial catalogue that is a dropped invoice, which under-declares.
The write carries the revision the document was READ at, so the substrate refuses it if the row moved in between; the mutation is then re-applied to the newly-current document.
mutationis therefore called once per attempt and MUST be a pure function of the document it is handed – it must not close over a value derived from an earlier read, or the retry re-applies a stale decision.- Parameters:
mutation (Callable[[DocumentT], DocumentT]) – Builds the next document from the current one. May raise to refuse the mutation outright (a duplicate-identifier check, for instance); the refusal propagates unretried, since a conflict is what the retry exists for and a refusal is not one.
attempts (int) – Maximum reads of the current document. Exceeding them raises the substrate’s conflict error rather than looping.
- Return type:
DocumentT
- Returns:
The document as written.
- Raises:
SecureObjectRevisionConflictError – Contention persisted across every attempt.