from pydantic import Field, computed_field from sqlalchemy import ForeignKey, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models._common import BaseModelDb, ExportBaseModel, _hash class Category(ExportBaseModel): name: str = Field(pattern=r"[^:\r\n]+") parent: Category | None = None @computed_field # type: ignore[prop-decorator] @property def hash_id(self) -> str: return _hash(self.name, self.parent) @classmethod def from_str(cls, s: str) -> Category | None: if not s: return None components = s.split(":") name = components[-1] rest = ":".join(components[:-1]) return cls(name=name, parent=cls.from_str(rest)) def to_str(self) -> str | None: if not self.parent: return self.name else: return f"{self.parent.to_str()}:{self.name}" def __str__(self): return self.to_str() def __repr__(self): return f'Category.from_str("{self.__str__()}")' class CategoryDb(BaseModelDb): __tablename__ = "category" __table_args__ = ( UniqueConstraint("name", "parent_id", name="uq_category_name_parent"), ) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String, nullable=False) parent_id: Mapped[int | None] = mapped_column( ForeignKey("category.id"), nullable=True ) # Relationships parent = relationship( "CategoryDb", remote_side="CategoryDb.id", back_populates="children" ) children = relationship("CategoryDb", back_populates="parent") budgets = relationship("CategoryBudgetDb", back_populates="category")