"""Schema-backed registry of editable taxpayer-profile keys.The registry is a tuple of strict :class:`ProfileKey` records compiledfrom the wizard descriptor catalogue(``aeat.application.wizard._catalogue.WIZARD_FLOWS``) and pushed into thisdomain registry via :func:`register_profile_keys` when the wizard package isimported (its ``__init__`` eagerly runs the compiler's registration). Thedomain never pulls upward into the application layer (DB-17): reading theregistry before the push raises :class:`ProfileKeysRegistrationError`. Eachentry carries the canonical key path (dot-separated), a requirement flag(required vs optional for declaration export), and a short multilingualdescription rendered in operator-facing surfaces.Adding a new key means appending a :class:`WizardQuestion` to the relevantflow in the wizard catalogue. The :class:`ProfileKey` class itself remains thecanonical schema record consumed by ``validate_profile`` and every profileeditor surface."""from__future__importannotationsfromenumimportStrEnumfromtypingimportTYPE_CHECKINGfrompydanticimportBaseModel,Field,field_validator,model_validatorfrom...coreimportSTRICT_FROZEN_CONFIGfrom...core.i18nimportTranslatableastrfrom._errorsimportProfileKeysRegistrationError,ProfileValidationErrorfrom._normaliseimportnormalise_keyifTYPE_CHECKING:PROFILE_KEYS:tuple[ProfileKey,...]
[docs]classProfileKeyRequirement(StrEnum):"""Whether a profile key is mandatory before declaration export."""REQUIRED="required"OPTIONAL="optional"
[docs]classProfileKey(BaseModel):"""Strict frozen record describing one editable profile key."""model_config=STRICT_FROZEN_CONFIGkey:str=Field(min_length=1,max_length=128)requirement:ProfileKeyRequirementdescription:trrequired_when_key:str|None=Nonerequired_when_value:str|None=None@field_validator("key")@classmethoddef_validate_key_shape(cls,value:str)->str:"""Reject blank or whitespace-padded keys; keep dot-separated paths intact."""ifnotvalue.strip():raiseProfileValidationError("key must not be empty or whitespace-only")ifvalue.strip()!=value:raiseProfileValidationError("key must not be padded with whitespace")returnvalue@field_validator("description")@classmethoddef_validate_description_key(cls,value:tr)->tr:"""Require profile-owned translation keys for authoritative descriptions."""ifnotvalue.strip():raiseProfileValidationError("description must not be empty")ifnotstr(value).startswith("profile.keys."):raiseProfileValidationError("description must use a profile translation key")returnvalue@field_validator("required_when_key","required_when_value")@classmethoddef_validate_conditional_requirement(cls,value:str|None)->str|None:ifvalueandvalue.strip()!=value:raiseProfileValidationError("conditional requirement fields must not be padded")ifvalue=="":raiseProfileValidationError("conditional requirement fields must not be empty")returnvalue@model_validator(mode="after")def_validate_conditional_requirement_pair(self)->ProfileKey:ifbool(self.required_when_key)!=bool(self.required_when_value):raiseProfileValidationError("required_when_key and required_when_value must be set together")returnself
[docs]@classmethoddeffrom_key(cls,raw:str)->ProfileKey:"""Return the :class:`ProfileKey` for ``raw`` after canonical normalisation. Normalisation strips surrounding whitespace, lowercases, and folds dashes into dots so ``"TAX.ID"`` and ``"tax.id"`` resolve to the same registry entry. Args: raw: Raw profile key string, possibly with non-canonical casing or separator characters. Returns: The matching :class:`ProfileKey` from the registry. Raises: KeyError: When the normalised form is not in the registry. """canonical=normalise_key(raw)try:return_by_key()[canonical]exceptKeyErrorasexc:raiseKeyError(f"unknown profile key: {raw!r}")fromexc
[docs]defregister_profile_keys(keys:tuple[ProfileKey,...])->None:"""Seed the domain profile-key registry from outside the domain layer. The compiled tuple normally lives behind a lazy import inside :func:`_build_profile_keys`. Outer layers (the wizard compiler) can call this function at their own import time to seed the cache directly, so the lazy import is never triggered. Calling this function twice with different tuples raises a :class:`RuntimeError` so the registration stays single-writer. """if_PROFILE_KEYS_CACHE:if_PROFILE_KEYS_CACHE[0]==keys:returnraiseProfileKeysRegistrationError()_PROFILE_KEYS_CACHE.append(keys)_BY_KEY_CACHE.append({entry.key:entryforentryinkeys})
def_profile_keys()->tuple[ProfileKey,...]:ifnot_PROFILE_KEYS_CACHE:raiseProfileKeysRegistrationError("profile keys are not registered; import the wizard catalogue ""(aeat.application.wizard) so the compiled keys are pushed via ""register_profile_keys before the profile-key registry is read",)return_PROFILE_KEYS_CACHE[0]
[docs]defprofile_keys()->tuple[ProfileKey,...]:"""Return the full registered :class:`ProfileKey` tuple, resolved at call time. Unlike the :data:`PROFILE_KEYS` module attribute (resolved once, at whatever moment a caller's ``from ... import PROFILE_KEYS`` statement executes), this function always defers resolution to the moment it is called. Callers that read the registry from inside a function body (rather than at their own module-import time) should prefer this function so they cannot race the wizard catalogue's registration. """return_profile_keys()
def_by_key()->dict[str,ProfileKey]:_profile_keys()return_BY_KEY_CACHE[0]def__getattr__(name:str)->tuple[ProfileKey,...]:"""Lazily resolve ``PROFILE_KEYS`` at first attribute access."""ifname=="PROFILE_KEYS":return_profile_keys()raiseAttributeError(f"module {__name__!r} has no attribute {name!r}")
[docs]defget_profile_key(key:str)->ProfileKey:"""Return the :class:`ProfileKey` for ``key``. Performs canonical normalisation (strip / lowercase / dash-to-dot) before the registry lookup so case-insensitive callers resolve to the same entry as the canonical form. Args: key: Raw profile key string to look up. Returns: The matching :class:`ProfileKey` from the registry. """returnProfileKey.from_key(key)
[docs]defrequired_profile_keys()->tuple[ProfileKey,...]:"""Return only the keys whose ``requirement`` is ``REQUIRED``. Returns: Tuple of :class:`ProfileKey` entries that are required. """returntuple(entryforentryin_profile_keys()ifentry.requirementisProfileKeyRequirement.REQUIRED)
[docs]defoptional_profile_keys()->tuple[ProfileKey,...]:"""Return only the :class:`ProfileKey` entries whose ``requirement`` is ``OPTIONAL``."""returntuple(entryforentryin_profile_keys()ifentry.requirementisProfileKeyRequirement.OPTIONAL)