Source code for cadrumo.domain.transactions.classification_rule
"""Domain model for a ledger classification rule.A rule binds a regex description-pattern to a target:class:`~cadrumo.domain.transactions.enums.BusinessClassification`.Rule IDs are content-addressed (SHA-256 of the rule's key fields) socreation is idempotent: adding the same pattern + classification pairtwice produces the same rule_id and the repository overwrites the priorentry rather than duplicating it."""from__future__importannotationsimportrefromdatetimeimportdatetimefromtypingimportAnnotatedfrompydanticimportBaseModel,Field,StringConstraints,field_validatorfrom...core.errors.hierarchyimportpydantic_validation_boundaryfrom...core.hashingimportsha256_hexfrom...core.identity.digestimportContentDigestfrom...core.modelsimportSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom...core.time.clockimportnowfrom...core.time.utcimportUtcInstantfrom.enumsimportBusinessClassificationfrom.errorsimportClassificationRuleErrorRuleDescriptionPattern=Annotated[str,StringConstraints(min_length=1)]"""The regular expression a rule matches a transaction description against."""RulePriority=Annotated[int,Field(ge=1)]"""Resolution order for competing rules; the lower integer wins."""RuleActor=Annotated[str,StringConstraints(min_length=1)]"""Who authored the rule, as recorded on it."""def_compute_rule_id(description_pattern:str,classification:BusinessClassification,category_id:str|None,)->str:"""Return the SHA-256 content-addressed rule id."""raw=f"{description_pattern}|{classification.value}|{category_idor''}"returnsha256_hex(raw.encode())
[docs]classLedgerClassificationRule(BaseModel):"""A persisted ledger classification rule. ``rule_id`` is content-addressed so that persisting the same pattern twice is idempotent. Priority resolution: lower ``priority`` integer wins; ties broken by ``created_at`` ascending (earliest rule wins). """model_config=_STRICT_FROZENrule_id:ContentDigestdescription_pattern:RuleDescriptionPatternclassification:BusinessClassificationcategory_id:str|None=Nonepriority:RulePriority=100created_at:UtcInstantactor:RuleActor@field_validator("description_pattern")@classmethod@pydantic_validation_boundarydef_validate_regex(cls,value:str)->str:try:re.compile(value)exceptre.errorasexc:raiseClassificationRuleError(f"description_pattern is not a valid regex: {exc}")fromexcreturnvalue
[docs]@classmethoddefcreate(cls,*,description_pattern:str,classification:BusinessClassification,category_id:str|None=None,priority:int=100,actor:str,created_at:datetime|None=None,)->LedgerClassificationRule:"""Construct a :class:`LedgerClassificationRule` with the content-addressed ``rule_id``."""rule_id=_compute_rule_id(description_pattern,classification,category_id)returncls(rule_id=rule_id,description_pattern=description_pattern,classification=classification,category_id=category_id,priority=priority,created_at=created_atornow(),actor=actor,)
[docs]defmatches(self,description:str)->bool:"""Return ``True`` when this rule's pattern matches ``description`` (case-insensitive)."""returnbool(re.search(self.description_pattern,description,re.IGNORECASE))