Source code for aeat.application.user_profile._aggregate
"""The whole-profile in-memory aggregate.A logical profile is not one record. It is, concretely, a bucketdirectory on disk, a plaintext ``manifest.toml``, an encrypted:class:`UserProfileRecord` row in a per-bucket SQLite table, anappend-only bucket-event history, and the ``active-profile`` pointer.Historically no object owned that set; every operation wrote whicheverstores it remembered and consistency was by convention.:class:`ProfileAggregate` is the single in-memory object that holds aprofile's whole state: its immutable UUID identity, its mutableoperator-facing label, the manifest-derived metadata, the secure:class:`UserProfileRecord`, and its lifecycle status. Application codeloads and saves the aggregate; :class:`ProfileRepository` is the solewriter of the physical stores it projects onto."""from__future__importannotationsfromdatetimeimportdatetimefrompydanticimportBaseModel,ConfigDict,Field,field_validator,model_validatorfrom...adapters.persistence.storage.bucketimportManifestKdfParamsfrom...core.identityimportProfileIdas_ProfileIdfrom...core.timeimportvalidate_utc_awarefrom...domain.user_profileimportUserProfileRecord,UserProfileStatus,UserProfileValidationError_PROFILE_AGGREGATE_CONFIG=ConfigDict(strict=True,frozen=True,extra="forbid",hide_input_in_errors=True)_AGGREGATE_MISMATCH_MESSAGE="profile aggregate projections are inconsistent"def_aggregate_mismatch_error(*,mismatch:str,translated_message:str)->UserProfileValidationError:returnUserProfileValidationError(_AGGREGATE_MISMATCH_MESSAGE,translated_message=translated_message,context={"mismatch":mismatch},)
[docs]classProfileAggregate(BaseModel):"""The whole logical profile as one strict, frozen in-memory object. The aggregate carries every store the logical profile fragments across: - ``profile_id`` — the immutable UUIDv4 identity. The bucket directory, the secure-object key, and the active-profile pointer all key on it. - ``label`` — the operator-chosen display name. Mutable; carried both in the manifest and the secure record. - ``manifest`` fields (``created_at``, ``kdf_params``, ``recovery_enrolled``, ``manifest_schema_version``) — the plaintext bucket-metadata projection. - ``record`` — the encrypted :class:`UserProfileRecord`. - ``status`` — the lifecycle status, mirrored from the record so consumers can read it without unwrapping ``record``. """model_config=_PROFILE_AGGREGATE_CONFIGprofile_id:_ProfileIdlabel:str=Field(min_length=1,max_length=160)created_at:datetimekdf_params:ManifestKdfParamsrecovery_enrolled:boolmanifest_schema_version:int=Field(ge=1)record:UserProfileRecordstatus:UserProfileStatus@field_validator("created_at")@classmethoddef_check_created_at(cls,value:datetime)->datetime:returnvalidate_utc_aware(value)@model_validator(mode="after")def_validate_cross_store_agreement(self)->ProfileAggregate:"""Reject an aggregate whose projections disagree on identity. The aggregate is the single object a repository assembles from several physical stores; if those stores disagree on the profile UUID, the operator-facing label, or the lifecycle status the aggregate is already inconsistent and must never be constructed. This validator is the last line — :func:`verify_profile_integrity` surfaces the same drift with operator-facing diagnostics before the aggregate is built. ``label`` is carried in two physical stores — the plaintext manifest-derived projection and the encrypted record's ``display_name``. A rename writes both sequentially; a crash between the two writes leaves a torn rename that is otherwise undetectable at load. The label agreement check below is the structural defence against that torn-rename state. """ifself.record.profile_id!=self.profile_id:raise_aggregate_mismatch_error(mismatch="profile_id",translated_message="application.user_profile.errors.aggregate_profile_id_mismatch",)ifself.label!=self.record.display_name:raise_aggregate_mismatch_error(mismatch="label",translated_message="application.user_profile.errors.aggregate_label_mismatch",)ifself.record.statusisnotself.status:raise_aggregate_mismatch_error(mismatch="status",translated_message="application.user_profile.errors.aggregate_status_mismatch",)returnself