from datetime import datetime from sqlalchemy import DateTime from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column from fooder.utils.datetime import utc_now from fooder.utils.password_helper import password_helper class Base(DeclarativeBase): pass class CommonMixin: @declared_attr.directive def __tablename__(cls) -> str: return cls.__name__.lower() # type: ignore id: Mapped[int] = mapped_column(primary_key=True) version: Mapped[int] = mapped_column(default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now) last_changed: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now) __mapper_args__ = {"version_id_col": version} class SoftDeleteMixin: deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) class PasswordMixin: hashed_password: Mapped[str] def set_password(self, password: str) -> None: self.hashed_password = password_helper.hash(password) def verify_password(self, password: str) -> bool: return password_helper.verify(password, self.hashed_password) class EntryMacrosMixin: """Computed macros for entry-like models that scale product macros by grams.""" @property def amount(self) -> float: return self.grams / 100 # type: ignore[attr-defined] @property def calories(self) -> float: return self.amount * self.product.calories # type: ignore[attr-defined] @property def protein(self) -> float: return self.amount * self.product.protein # type: ignore[attr-defined] @property def carb(self) -> float: return self.amount * self.product.carb # type: ignore[attr-defined] @property def fat(self) -> float: return self.amount * self.product.fat # type: ignore[attr-defined] @property def fiber(self) -> float: return self.amount * self.product.fiber # type: ignore[attr-defined] class AggregateMacrosMixin: """Computed macros for models that sum macros across child entries.""" @property def calories(self) -> float: return sum(e.calories for e in self.entries) # type: ignore[attr-defined] @property def protein(self) -> float: return sum(e.protein for e in self.entries) # type: ignore[attr-defined] @property def carb(self) -> float: return sum(e.carb for e in self.entries) # type: ignore[attr-defined] @property def fat(self) -> float: return sum(e.fat for e in self.entries) # type: ignore[attr-defined] @property def fiber(self) -> float: return sum(e.fiber for e in self.entries) # type: ignore[attr-defined]