from __future__ import annotations import uuid from datetime import UTC, datetime from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKey, Integer, Numeric, String, Text, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass def uuid_pk() -> Mapped[uuid.UUID]: return mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) def now() -> datetime: return datetime.now(UTC) class TimestampMixin: created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now, onupdate=now) class Equipment(Base, TimestampMixin): __tablename__ = "logic_equipment" id: Mapped[uuid.UUID] = uuid_pk() owner_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True) name: Mapped[str] = mapped_column(String(160), index=True) description: Mapped[str | None] = mapped_column(Text) image_s3_url: Mapped[str | None] = mapped_column(Text) image_s3_key: Mapped[str | None] = mapped_column(Text) is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, index=True) workout_items: Mapped[list[WorkoutItem]] = relationship(back_populates="equipment") class Exercise(Base, TimestampMixin): __tablename__ = "logic_exercises" id: Mapped[uuid.UUID] = uuid_pk() owner_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True) equipment_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_equipment.id")) name: Mapped[str] = mapped_column(String(160), index=True) description: Mapped[str | None] = mapped_column(Text) image_s3_url: Mapped[str | None] = mapped_column(Text) image_s3_key: Mapped[str | None] = mapped_column(Text) is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, index=True) default_calories_per_minute: Mapped[float | None] = mapped_column(Numeric(8, 2)) equipment: Mapped[Equipment | None] = relationship() workout_items: Mapped[list[WorkoutItem]] = relationship(back_populates="exercise") class Workout(Base, TimestampMixin): __tablename__ = "logic_workouts" id: Mapped[uuid.UUID] = uuid_pk() user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) notes: Mapped[str | None] = mapped_column(Text) estimated_calories: Mapped[float] = mapped_column(Numeric(10, 2), default=0) items: Mapped[list[WorkoutItem]] = relationship( back_populates="workout", cascade="all, delete-orphan", order_by="WorkoutItem.order_index" ) class WorkoutItem(Base): __tablename__ = "logic_workout_items" __table_args__ = ( CheckConstraint( "(exercise_id IS NOT NULL AND equipment_id IS NULL) OR " "(exercise_id IS NULL AND equipment_id IS NOT NULL)", name="ck_workout_item_exactly_one_entity", ), ) id: Mapped[uuid.UUID] = uuid_pk() workout_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("logic_workouts.id", ondelete="CASCADE") ) exercise_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_exercises.id")) equipment_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_equipment.id")) order_index: Mapped[int] = mapped_column(Integer, default=0) planned_working_weight: Mapped[float | None] = mapped_column(Numeric(8, 2)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) workout: Mapped[Workout] = relationship(back_populates="items") exercise: Mapped[Exercise | None] = relationship(back_populates="workout_items") equipment: Mapped[Equipment | None] = relationship(back_populates="workout_items") sets: Mapped[list[WorkoutSet]] = relationship( back_populates="workout_item", cascade="all, delete-orphan", order_by="WorkoutSet.set_index" ) class WorkoutSet(Base): __tablename__ = "logic_workout_sets" id: Mapped[uuid.UUID] = uuid_pk() workout_item_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("logic_workout_items.id", ondelete="CASCADE") ) set_index: Mapped[int] = mapped_column(Integer) weight: Mapped[float] = mapped_column(Numeric(8, 2), default=0) reps: Mapped[int] = mapped_column(Integer, default=0) duration_seconds: Mapped[int | None] = mapped_column(Integer) calories: Mapped[float | None] = mapped_column(Numeric(8, 2)) completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) workout_item: Mapped[WorkoutItem] = relationship(back_populates="sets")