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 ActivitySource(Base, TimestampMixin): __tablename__ = "logic_activity_sources" __table_args__ = ( CheckConstraint("kind IN ('exercise', 'machine')", name="ck_activity_source_kind"), CheckConstraint( "category IN ('chest', 'back', 'legs', 'shoulders', 'biceps', 'triceps', " "'core', 'cardio', 'full_body', 'other')", name="ck_activity_source_category", ), CheckConstraint( "equipment IN ('barbell', 'dumbbell', 'machine', 'cable', 'bodyweight', " "'kettlebell', 'cardio_machine', 'other')", name="ck_activity_source_equipment", ), CheckConstraint( "measurement_type IN ('weight_reps', 'reps_only', 'duration', " "'distance_duration', 'duration_calories')", name="ck_activity_source_measurement_type", ), CheckConstraint( "difficulty IN ('beginner', 'intermediate', 'advanced')", name="ck_activity_source_difficulty", ), ) id: Mapped[uuid.UUID] = uuid_pk() owner_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True) slug: Mapped[str] = mapped_column(String(180), unique=True, index=True) kind: Mapped[str] = mapped_column(String(20), index=True) title: Mapped[str] = mapped_column(String(160), index=True) description: Mapped[str | None] = mapped_column(Text) category: Mapped[str] = mapped_column(String(32), index=True) equipment: Mapped[str] = mapped_column(String(32), index=True) measurement_type: Mapped[str] = mapped_column(String(32)) difficulty: Mapped[str] = mapped_column(String(20), default="intermediate") 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)) workout_items: Mapped[list[WorkoutItem]] = relationship(back_populates="activity_source") 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" __table_args__ = ( CheckConstraint( "status IN ('active', 'finished', 'discarded')", name="ck_workout_status", ), ) id: Mapped[uuid.UUID] = uuid_pk() user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True) status: Mapped[str] = mapped_column(String(20), default="active", 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) total_sets: Mapped[int] = mapped_column(Integer, default=0) total_volume: Mapped[float] = mapped_column(Numeric(12, 2), default=0) 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( "(CASE WHEN activity_source_id IS NOT NULL THEN 1 ELSE 0 END + " "CASE WHEN exercise_id IS NOT NULL THEN 1 ELSE 0 END + " "CASE WHEN equipment_id IS NOT NULL THEN 1 ELSE 0 END) = 1", name="ck_workout_item_exactly_one_entity", ), CheckConstraint( "source_kind IN ('exercise', 'machine', 'equipment')", name="ck_workout_item_source_kind", ), ) id: Mapped[uuid.UUID] = uuid_pk() workout_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("logic_workouts.id", ondelete="CASCADE") ) source_kind: Mapped[str] = mapped_column(String(20)) title_snapshot: Mapped[str] = mapped_column(String(160)) image_s3_url_snapshot: Mapped[str | None] = mapped_column(Text) measurement_type_snapshot: Mapped[str] = mapped_column(String(32), default="weight_reps") category_snapshot: Mapped[str] = mapped_column(String(32), default="other") equipment_snapshot: Mapped[str] = mapped_column(String(32), default="other") activity_source_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("logic_activity_sources.id") ) 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") activity_source: Mapped[ActivitySource | None] = relationship(back_populates="workout_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) distance_km: Mapped[float | None] = mapped_column(Numeric(8, 3)) 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")