cadrumo.application.user_profile.bundle_export module

Single application authority for portable profile-bundle publication.

Both operator purposes – portable transfer and the subject-access request – share this one service and one bundle schema; only their typed ProfileBundleExportPurpose metadata differs. Publication is a three-phase durable sequence so a crash in any window recovers honestly: the service serializes to a restrictive temporary file, fsyncs it, records a durable PREPARED operation-state journal OUTSIDE the target artifact (carrying the payload digest), atomically replaces the target, fsyncs the parent directory, transitions the journal to COMPLETED, and only then emits the completion event before clearing the journal. A durably-published bundle is never un-published; every crash window is reconciled without loss:

  • a PREPARED operation whose destination does not hold the recorded digest never published – it is cleared as an orphan and no event is emitted;

  • a PREPARED operation whose destination matches the digest, or a COMPLETED operation, published durably – reconciliation emits its PROFILE_EXPORTED event (idempotently, from the fixed event_occurred_at) so no durably-published bundle is ever left without its audit event.

Reconciliation is not an optional maintenance chore: export_profile_bundle() runs it before every publication, so the next export an operator performs – to any destination, for any profile – is what clears a previous crash’s orphan journal and its leftover cleartext staged temp.

The sealed recovery archive is a separate surface and is not folded in here.

class PreparedProfileExport(operation, staged_path, pointer, bundle, request)[source]

Bases: object

In-memory handle to one staged, journalled, not-yet-published export.

Parameters:
operation: ProfileBundleExportOperation
staged_path: Path
pointer: ProfileBucketPointer
bundle: UserProfilePortableExport
request: ProfileBundleExportRequest
class ProfileBundleExportReconciliation(reconciled, failures)[source]

Bases: object

Outcome of one reconciliation sweep across every export journal.

reconciled are the operations the sweep resolved and cleared; failures are the ones it isolated and left journalled for a later attempt. An operation skipped because a live export holds its target lock is in neither: it is healthy in-flight work, not a failure.

Parameters:
reconciled: tuple[ProfileBundleExportOperation, ...]
failures: tuple[ProfileBundleExportReconcileFailure, ...]
export_profile_bundle(request, *, profile_decode_context)[source]

Resolve, serialize, atomically publish, and record one profile export.

profile_decode_context must come from the caller’s pinned authority operation; no schema is discovered while exporting. Reconciles any crash-interrupted prior publication BEFORE taking this export’s target lock (see _reconcile_crash_orphans_before_publication()), then holds one exclusive lock on the resolved target for the whole publication so a concurrent export to the same file is excluded, and composes prepare_profile_export() and publish_prepared_export().

Return type:

ProfileBundleExportResult

Parameters:
prepare_profile_export(request, *, journal=None, profile_decode_context)[source]

Record the PREPARED journal, THEN stage the bundle to a restrictive temp.

profile_decode_context is required because preparing the export reads the encrypted profile record and must not rediscover its schema through a process-global or filesystem fallback.

The journal is written BEFORE any cleartext reaches disk, naming the staged path it is about to write. That ordering is the whole crash contract: the reconciliation sweep is journal-driven, so cleartext staged without a journal entry pointing at it would be unreachable by every recovery surface. Staging first left exactly that window – serialization, category derivation, and a journal write that can wait on the repository lock all sat between the cleartext hitting disk and the record naming it – and a crash inside it stranded a readable profile bundle that no sweep could ever find (sensitive-financial-data-secure-storage-only).

A crash between the journal write and a completed staging is harmless: the reconciliation finds a PREPARED operation whose destination does not hold the recorded digest, clears the staged path it names (whether that file exists, is half-written, or was never created), and emits no event.

Return type:

PreparedProfileExport

Parameters:
publish_prepared_export(prepared, *, journal=None, profile_decode_context)[source]

Atomically replace the target, mark COMPLETED, then emit the event.

Replaces the target with the staged temp and fsyncs the parent directory – the durability point – then transitions the operation journal to COMPLETED and emits the PROFILE_EXPORTED completion event before clearing the journal. A durably-published bundle is NEVER un-published: if the completion event cannot be written, the COMPLETED journal is left in place so a later reconcile_prepared_exports() emits the pending event, and the failure is surfaced rather than rolled back. The event is derived from the operation’s fixed event_occurred_at, so the live emission and any reconciliation emission collapse to one idempotent event.

Return type:

ProfileBundleExportResult

Parameters:
reconcile_prepared_exports(*, journal=None, profile_decode_context)[source]

Reconcile crash-interrupted exports honestly in a fresh process.

export_profile_bundle() calls this before every publication (see _reconcile_crash_orphans_before_publication()), and the operator can invoke it directly, so recovery happens on the next export or on demand.

Every operation is isolated. One journal that cannot be read, or one operation that cannot be finalised – a profile deleted after its crash, a destination on a volume that has gone away – is recorded in failures and the walk continues, so a single bad record can never starve every later-ordered operation of its recovery. A failed operation keeps its journal, so the next sweep retries it. Failures are reported rather than swallowed: a journal left behind may still describe cleartext bundle bytes on disk, which is precisely what an operator needs told.

Each operation is reconciled only while holding the SAME per-destination lock a live export_profile_bundle() holds across its whole publication. An operation whose target lock cannot be acquired without waiting is an in-flight export, not a crash orphan, and is skipped (a skip, not a failure) – so a reconcile running concurrently with a live same-target export can never unlink its live staged temp or delete its journal (which would fail the live os.replace). Under the lock the operation is re-read and resolved by state:

  • COMPLETED (replace already landed): the durably-published bundle is missing only its audit trail, so the PROFILE_EXPORTED event is emitted (idempotently, from the fixed event_occurred_at) and the journal cleared.

  • PREPARED whose destination content matches the recorded digest: the atomic replace landed but the COMPLETED transition did not, so the bundle IS published – the event is emitted and the journal cleared, closing the replace-to-COMPLETED crash window.

  • PREPARED with no matching published content: a genuine orphan – its staged temp is removed and its journal cleared, and no event is emitted for an artifact that was never durably published.

Return type:

ProfileBundleExportReconciliation

Parameters: