aeat.application.bucket_maintenance._service module

BucketMaintenanceService composition implementation.

The service delegates every cross-store mutation to its existing single-writer primitive (see the ADR 2026-06-03-cli-workflow-redesign-adr). It contributes the bucket-maintenance audit-event emission that the inner primitives do not own; the inner primitives keep emitting their lifecycle events (PROFILE_RENAMED etc.) so each operator action surfaces both perspectives in the bucket-event history.

This module uses BucketEventHistoryRepository for event emission, UserProfilePortableExport for sealed export/import payloads, and ExportArchiveHeader for archive frontmatter. The archive file is an explicit operator handoff artifact; bucket state remains owned by the profile and secure repository primitives the service composes.

ensure_archive_schema_readable(archive_schema_version)[source]

Refuse a sealed-archive version this application cannot restore.

The gate is a ceiling with a durability floor, not an equality: a version above _ARCHIVE_SCHEMA_VERSION was exported by a newer application and is refused as such, and a version below _ARCHIVE_DURABILITY_FLOOR predates the durability guarantee.

Unlike the secure-object and bundle tiers, the archive tier carries NO upgrade dispatch: this is a range gate only, and nothing here transforms an older archive layout on restore. The lineage gate therefore pins _ARCHIVE_DURABILITY_FLOOR == _ARCHIVE_SCHEMA_VERSION: raising the current version forces an explicit decision in the same change — raise the floor too (dropping older archives, the pre-release posture) or land a version-aware reader/restore transform and widen the gate then. A floor held below current without that machinery would pass this gate green while restore misreads the old layout.

Raises:

BucketImportError – When the version is above the ceiling or below the durability floor.

Return type:

None

Parameters:

archive_schema_version (int)

class BucketMaintenanceService(*, event_repository=None)[source]

Bases: object

Compose existing primitives behind the bucket-maintenance surface.

The service holds no state of its own. An optional event-history repository override is accepted for tests that want to assert against an in-memory or alternate-backend repository; production instantiates the default which is bound to the active bucket via BucketEventHistoryRepository.

Parameters:

event_repository (BucketEventHistoryRepositoryProtocol | None)

rename(command)[source]

Relabel the bucket identified by command.bucket_id.

Reads the current operator-visible label, delegates the cross-store relabel to rename_profile(), then emits BUCKET_RENAMED carrying the previous label in the payload so the audit consumer can render the before / after pair without re-reading the manifest.

The inner rename_profile() call emits PROFILE_RENAMED from the lifecycle service; the two events are co-emitted by design — the lifecycle event records the data change, the maintenance event records the operator-surface invocation.

Both events land in the renamed bucket’s OWN event history: the lifecycle service already binds its event repository to the target bucket’s database, and the maintenance emission mirrors that binding so the audit trail can never split from the records it describes when the renamed bucket is not the active one.

Returns:

The result of the rename operation.

Return type:

RenameBucketResult

Parameters:

command (RenameBucketCommand)

delete(command)[source]

Destructively erase the bucket identified by command.bucket_id.

Composes the existing two-step erase pattern: soft tombstone via delete_profile_with_lifecycle_span() (clears the active-profile pointer, writes the manifest lifecycle status, tombstones the encrypted record, emits PROFILE_TOMBSTONED) followed by hard directory removal via remove_profile_bucket_directory(). The BUCKET_DELETED event is emitted into the bucket’s own history between the soft and hard steps so the operator’s verb invocation is recorded before the storage is gone.

Refuses unless command.confirmed is True; refuses if the target bucket is the active profile (the operator must switch profiles first, per the 2026-05-15 amendment to the bucket ADR). Both refusals are service-boundary contracts, not CLI ergonomics — a programmatic caller observes the same guarantees.

Returns:

The result of the delete operation.

Return type:

DeleteBucketResult

Parameters:

command (DeleteBucketCommand)

archive(command)[source]

Move the bucket identified by command.bucket_id into reversible dormancy.

Composes reactivate_profile_with_lifecycle_span()’s counterpart, delete_profile_with_lifecycle_span() — the SAME soft-tombstone primitive delete() composes — but deliberately stops there: the hard directory removal (remove_profile_bucket_directory()) that delete() performs afterward never runs, so the bucket directory, manifest, and encrypted record all survive intact and restore() can bring the same bucket back.

Refuses unless command.confirmed is True; refuses if the target bucket is the active profile (the operator must switch profiles first, mirroring delete()’s own contract). The BUCKET_ARCHIVED event lands in the archived bucket’s OWN event history (mirroring rename()’s binding) since the bucket still exists after this call — unlike delete()’s event, which must outlive the erased bucket.

Returns:

The result of the archive operation.

Return type:

ArchiveBucketResult

Parameters:

command (ArchiveBucketCommand)

restore(command)[source]

Bring the archived bucket identified by command.bucket_id back to active.

Composes reactivate_profile_with_lifecycle_span() — the symmetric inverse of the soft tombstone archive() composes. Refuses when the target is not currently tombstoned (i.e. was never archived, or is already active), surfaced by ProfileNotFoundError from the underlying lifecycle service.

The BUCKET_RESTORED event lands in the restored bucket’s OWN event history, mirroring archive()’s binding.

Returns:

The result of the restore operation.

Return type:

RestoreBucketResult

Parameters:

command (RestoreBucketCommand)

browse(command)[source]

Enumerate the bucket namespace inventory and return a BrowseBucketResult.

Composes SecureObjectRepository.list_namespaces() with a per-namespace row count via list_keys() (whose return is the HMAC-digest list — the count is meaningful even though the digests themselves are opaque). The result excludes any namespace whose name does not contain namespace_filter as a substring when one is supplied. Read-only; emits no bucket event.

Key-level browse (returning operator-readable keys + classification per row) requires decryption and a SensitivityClass redaction policy; deferred to a follow-up Step per the composition-pattern ADR.

Return type:

BrowseBucketResult

Parameters:

command (BrowseBucketCommand)

disk_usage(command)[source]

Measure command.bucket_id’s on-disk footprint and return a DiskUsageBucketResult.

Walks the bucket’s fixed directory layout (bucket_paths()) and sums regular-file byte sizes via os.stat — plain filesystem metadata, never decrypted content. This is the same non-active-safe posture browse() and preview_discard_sandbox() already rely on: no master key or active-bucket session is opened, so a non-active (even archived) bucket can be measured. Read-only; emits no bucket event.

Return type:

DiskUsageBucketResult

Returns:

DiskUsageBucketResult reporting the total byte count and a per-subdirectory (db, blobs, audit) breakdown, plus the bucket’s own manifest file folded into the db row (the manifest sits directly under the bucket directory, not in a fixed subdirectory of its own).

Parameters:

command (DiskUsageBucketCommand)

export(command)[source]

Write a sealed bucket archive for command.bucket_id.

The method composes the existing profile portable-bundle serializer, compute_manifest_digest(), sealed-archive writer, active bucket DEK, and bucket-event history. It does not reimplement profile export logic. When a recovery passphrase is supplied, the payload is sealed under a passphrase-derived key and the archive carries a small recovery-wrap salt member; otherwise the currently active bucket DEK seals the payload for same-host backup.

Return type:

ExportBucketResult

Returns:

An ExportBucketResult describing the written sealed archive.

Parameters:

command (ExportBucketCommand)

import_(command)[source]

Import a sealed bucket archive through the profile bundle service.

Archives with a recovery-wrap member require the matching passphrase. Archives without one are same-host backups and require the active bucket DEK to match the archive payload. New buckets are provisioned through the canonical profile create span before the UserProfilePortableExport payload is restored. The archive header’s manifest digest is authenticated through AEAD associated data during decryption; it is not recomputed against the imported host manifest.

Return type:

ImportBucketResult

Returns:

An ImportBucketResult describing the restored bucket.

Parameters:

command (ImportBucketCommand)

inspect(command)[source]

Read a sealed bucket archive’s header without decrypting or restoring it.

Composes read_sealed_archive() (the same reader import_ uses for layout and header validation) with the on-disk file size. No session is opened, no key is required, and no bucket state is written or read — this is a pure inspection of the archive file itself, so an operator can confirm a backup’s identity, age, and recovery-wrap presence before deciding whether and how to restore it.

Return type:

InspectBucketArchiveResult

Returns:

An InspectBucketArchiveResult describing the archive header.

Parameters:

command (InspectBucketArchiveCommand)

recovery_wrap_passphrase_present(command)[source]

Return whether command requests a recovery-passphrase archive.

Return type:

bool

Parameters:

command (ExportBucketCommand)