aeat.application.user_profile._profile_repository module¶
The single, sole writer of a logical profile’s physical stores.
A logical profile fragments across a bucket directory, a plaintext
BucketManifest, an
encrypted UserProfileRecord row in a
per-bucket SQLite database, and an append-only
BucketEventHistoryRepository.
Before this repository, every CLI handler and application service
wrote whichever stores it remembered; consistency was by convention
and a failure mid-sequence left a half-live profile.
ProfileRepository makes “touch every store” structural.
ProfileRepository.create() and ProfileRepository.delete()
are cross-store units of work: the
filesystem directory + manifest are staged first (a directory with no
secure-object row is detectable, reclaimable garbage), the encrypted
record commits next inside the SQLite transaction, the
BucketPointer moves last. On any failure the staged
filesystem state is rolled back. ProfileRepository.load() runs
verify_profile_integrity()
so a profile whose stores have drifted surfaces the drift instead of
being served silently.
Application orchestration
(register_active_profile(),
remove_active_profile(),
select_profile()) and the CLI
config profile verbs delegate their store writes here; there is
exactly one implementation of the cross-store profile write.
See also
ProfileLifecycleServiceBucket-local record and lifecycle-event service composed by this repository.
read_profile_bucket()Manifest scan used by label lookup and live-profile surfaces.
ProfileAggregateIn-memory cross-store profile aggregate returned by this repository.
- TAX_ID_FACT_PATH¶
Profile-fact path carrying the taxpayer’s Spanish NIF / NIE / CIF.
- class ProfileSummary(**data)[source]¶
Bases:
BaseModelA typed one-row summary of a registered profile.
Returned by
ProfileRepository.list(); carries only the plaintext-manifest projection (UUID, label, lifecycle status) so a listing never needs to unlock an encrypted bucket.- Parameters:
profile_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=36, pattern=^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$, ascii_only=None)])
label (str)
status (UserProfileStatus)
- profile_id: ProfileId¶
- label: str¶
- status: UserProfileStatus¶
- class ProfileRepository(*, root=None, secure_objects=None, schema=None)[source]¶
Bases:
objectThe sole writer of a logical profile’s cross-store physical state.
The repository composes the existing lower-level pieces — the secure-record
UserProfileLifecycleRepository,provision_bucket_directory(), theBucketManifestIO helpers, and the active-profileBucketPointerIO. It is the single place those writes happen.- Parameters:
root (Path | None)
secure_objects (SecureObjectRepository | None)
schema (ProfileSchemaDefinition | None)
- create(*, label, facts=(), profile_id=None, enforce_unique_tax_id=True, routing_profile_id=None)[source]¶
Create a new profile as a cross-store unit of work.
enforce_unique_tax_id(defaultTrue) refuses a create whose tax id is already carried by a live profile — a freshconfig profile createmust not split one taxpayer’s filing history across two profiles.config profile duplicateandconfig profile importlegitimately reproduce an existing profile’s tax id and passFalse.Mints a fresh UUID identity (unless
profile_idis supplied for a caller that already minted one), then performs the all-or-nothing write — every store write, the active-profile pointer included, is inside this single unit of work:Stage the bucket directory + plaintext manifest. A directory with no manifest is detectable, reclaimable garbage — a rolled-back create; the manifest write is the point a profile becomes registered.
Write the active-profile pointer. The per-bucket SQLAlchemy engine resolves its URL from the pointer chain, so the pointer must precede the encrypted-record write.
Commit the encrypted
UserProfileRecord(the SQLite transaction).
The pre-create active-profile pointer is captured here, before the first store write, so a failure at any step rolls back the staged directory + manifest AND restores the pointer to exactly its pre-create state. No caller writes the pointer; the
missing_profile_recordtorn state (pointer aimed at a profile with no record) is unreachable because the pointer write and the record write are one unit of work.- Parameters:
label (
str) – Operator-visible display name for the new profile.facts (
Sequence[UserProfileFact]) – Initial profile fact sequence. Defaults to empty.profile_id (
str|None) – Optional explicit UUID. WhenNone, a fresh UUID is minted.enforce_unique_tax_id (
bool) – WhenTrue(default), refuses a create whose tax id is already carried by a live profile.routing_profile_id (
str|None) – Optional routing assertion; must matchprofile_idwhen supplied.
- Return type:
- Returns:
The assembled
ProfileAggregate.- Raises:
ProfileNotFoundError – If the profile already carries a manifest (it is already a registered profile).
AeatError – If an error occurs during the all-or-nothing write.
OSError – If a filesystem error occurs during directory staging.
UserProfileValidationError – If the routing profile id does not match the resolved profile id.
ValidationError – If the encrypted record fails schema validation.
- load(profile_id)[source]¶
Assemble the aggregate from every store; verify integrity.
Reads the plaintext manifest and the encrypted record, runs
verify_profile_integrity()to confirm the directory, the manifest, and the record all agree on the UUID, the lifecycle status, and the display label, then builds theProfileAggregate.- Parameters:
profile_id (
str) – The UUID of the profile to load.- Return type:
- Returns:
The assembled
ProfileAggregatefor the profile.- Raises:
ProfileNotFoundError – If the bucket directory or manifest is absent.
- save(aggregate)[source]¶
Persist changes to the aggregate across its physical stores.
The manifest projection (
label,kdf_params,recovery_enrolled) and the encrypted record are both re-written so the two label copies never drift. The active-profile pointer is not touched — selecting a profile is a distinct operation.- Return type:
- Parameters:
aggregate (ProfileAggregate)
- rename(profile_id, *, new_label)[source]¶
Change a profile’s operator-visible label across its stores.
Profile identity is an immutable UUID, so a rename is a pure metadata edit: only the label moves, in the two stores that hold a copy of it - the encrypted
UserProfileRecorddisplay_nameand the plaintext manifestlabel. There is no directory move and no re-key.Both writes happen here, inside the sole writer: the lifecycle service updates the record and emits
PROFILE_RENAMED, then the manifest label projection is rewritten.loadruns the cross-store integrity check first, so a drifted profile raisesProfileIntegrityErrorrather than being relabelled in a torn state.- Parameters:
- Return type:
- Returns:
The updated
ProfileAggregatewith the new label.- Raises:
ProfileAlreadyRegisteredError – If
new_labelis already carried by another live profile.UserProfileValidationError – If
new_labelis blank after stripping.
- delete(profile_id)[source]¶
Tombstone a profile via its lifecycle status.
deleteis a soft removal: the encrypted record is tombstoned and re-saved; the bucket directory and manifest are left intact so audit and history reads still resolve.Three stores mutate, in an order chosen so every torn intermediate fails closed:
The active-profile pointer is cleared first. A crash here leaves a still-live record with no active pointer — benign, the operator simply re-selects.
The plaintext manifest
statusis mirrored totombstonedBEFORE the record tombstone. A crash between this write and the record tombstone leaves a manifest sayingtombstonedover a still-live record. That is the safe direction: the profile immediately drops off every live surface (list/switch/ name-uniqueness all read the manifest), and the record is still loadable for repair. The reverse order — record first — would leave a manifest sayingactiveover a tombstoned record, re-opening the leak.The encrypted record is tombstoned last.
loadruns the cross-store integrity check, which compares the manifest status against the record status, so a profile left in the step-2/step-3 drift state by a crash raisesProfileIntegrityErroron the next load - reclaiming a drifted profile is therepairsurface’s domain.- Parameters:
profile_id (
str) – The UUID of the profile to tombstone.- Return type:
- Returns:
The updated
ProfileAggregatewith tombstoned status.
- reactivate(profile_id)[source]¶
Restore a tombstoned profile to active status; symmetric inverse of
delete().reactivatenever opened bydelete’s counterpart before: it is a pure lifecycle-status flip back to active. The bucket directory, manifest, and encrypted record were all left intact by the soft tombstone, so no directory or key-schedule provisioning happens here.The write order is the mirror image of
delete()’s own crash-safety reasoning: the encrypted record is reactivated FIRST, the plaintext manifest mirror SECOND. A crash between the two leaves the manifest sayingtombstonedover a now-active record — the safe direction, because the manifest scan (list/switch/ name-uniqueness) still excludes the profile from every live surface until the manifest write lands. The reverse order would leave the manifest claimingactiveover a record the encrypted-store lifecycle service still refuses to reactivate-confirm, re-opening the same exposure windowdelete()avoids.- Parameters:
profile_id (
str) – The UUID of the profile to reactivate.- Return type:
- Returns:
The updated
ProfileAggregatewith active status.- Raises:
ProfileNotFoundError – If the profile is not currently tombstoned, or if the bucket directory or manifest is absent.
- select(profile_id)[source]¶
Make a registered profile the active one.
Loads and integrity-checks the aggregate — a profile whose stores have drifted is never selected — then writes the active-profile pointer. The pointer is the only store this mutates; identity and lifecycle metadata are untouched.
A tombstoned profile is not a selectable profile:
deleteis a soft removal that retains the bucket for audit, but the profile has left the live surface. Selecting it is refused with the sameProfileNotFoundErrorclass as an unknown profile so the operator cannot unknowingly work inside a deleted profile.- Parameters:
profile_id (
str) – The UUID of the profile to activate.- Return type:
- Returns:
The
ProfileAggregatefor the newly active profile.- Raises:
ProfileNotFoundError – If the profile is not registered, or is registered but tombstoned.
- list()[source]¶
Return a typed summary for every registered profile.
Scans
<root>/buckets/*/manifest.toml— never unlocks an encrypted bucket. The lifecycle status reported is the manifeststatusmirror, kept in lockstep with the encrypted record by everyProfileRepositorywrite. Tombstoned profiles are included so callers that need the full inventory (repair, audit) see them; live-surface callers filter onstatus.Each element is a
ProfileSummary; live label lookups useread_profile_bucket().- Return type: