feat: define all 8 SQLModel tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:35:13 -06:00
parent 45b93a2988
commit 3bf1e13adc
10 changed files with 529 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
"""SQLModel model definitions for SneakySwole.
All 8 tables are defined here and re-exported for convenient imports.
"""
from app.models.user import User
from app.models.exercise import Exercise
from app.models.warmup import Warmup
from app.models.workout_day import WorkoutDay
from app.models.user_exercise_program import UserExerciseProgram
from app.models.workout_session import WorkoutSession
from app.models.workout_log import WorkoutLog
from app.models.progress_log import ProgressLog
__all__ = [
"User",
"Exercise",
"Warmup",
"WorkoutDay",
"UserExerciseProgram",
"WorkoutSession",
"WorkoutLog",
"ProgressLog",
]

35
app/models/exercise.py Normal file
View File

@@ -0,0 +1,35 @@
"""Exercise model for the exercise library catalog.
Each exercise belongs to a workout day and includes form cues.
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class Exercise(SQLModel, table=True):
"""An exercise in the workout library.
Attributes:
id: Primary key, auto-incremented.
name: Exercise name (e.g., "DB Chest Press (Floor)").
muscle_group: Target muscle group (e.g., "Chest", "Shoulders").
workout_day: Which day this exercise belongs to (Push/Pull/Lower/Full Body).
sets: Default number of sets.
tempo: Tempo notation (e.g., "3-1-2" = 3s eccentric, 1s pause, 2s concentric).
form_cues: Detailed form instructions.
created_at: Timestamp when the record was created.
"""
__tablename__ = "exercises"
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
muscle_group: str = Field(default="")
workout_day: str = Field(index=True)
sets: int = Field(default=3)
tempo: str = Field(default="")
form_cues: str = Field(default="")
created_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,39 @@
"""ProgressLog model for tracking progression suggestions and outcomes.
Records what the progression engine suggested vs what the user actually did.
"""
import datetime as dt
from typing import Optional
from sqlmodel import Field, SQLModel
class ProgressLog(SQLModel, table=True):
"""A progression tracking entry for a specific exercise.
Attributes:
id: Primary key, auto-incremented.
user_id: FK to users table.
exercise_id: FK to exercises table.
date: The date this progression entry applies to.
suggested_reps: What the engine recommended.
suggested_weight: What the engine recommended.
actual_reps: What the user actually did.
actual_weight: What the user actually used.
progression_applied: Type of progression (e.g., "reps_increase", "weight_increase", "deload").
created_at: Timestamp when the record was created.
"""
__tablename__ = "progress_log"
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id", index=True)
exercise_id: int = Field(foreign_key="exercises.id")
date: dt.date = Field(default_factory=dt.date.today)
suggested_reps: Optional[int] = Field(default=None)
suggested_weight: Optional[str] = Field(default=None)
actual_reps: Optional[int] = Field(default=None)
actual_weight: Optional[str] = Field(default=None)
progression_applied: Optional[str] = Field(default=None)
created_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)

39
app/models/user.py Normal file
View File

@@ -0,0 +1,39 @@
"""User model for profile management.
Stores admin and regular user profiles with physical stats and goals.
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class User(SQLModel, table=True):
"""A user profile in the system.
Attributes:
id: Primary key, auto-incremented.
username: Unique login identifier.
password_hash: bcrypt-hashed password (admin only initially).
display_name: Human-readable name shown in the UI.
height: User's height as a string (e.g., "6'0\"").
weight: User's weight as a string (e.g., "260 lbs").
goals: Free-text training goals.
is_admin: Whether this user has admin privileges.
created_at: Timestamp when the record was created.
updated_at: Timestamp of the last update.
"""
__tablename__ = "users"
id: Optional[int] = Field(default=None, primary_key=True)
username: str = Field(index=True, unique=True)
password_hash: str = Field(default="")
display_name: str = Field(default="")
height: Optional[str] = Field(default=None)
weight: Optional[str] = Field(default=None)
goals: Optional[str] = Field(default=None)
is_admin: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,37 @@
"""UserExerciseProgram model for per-user exercise programming.
Links a user to an exercise with week 1 and week 4 rep/weight targets.
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class UserExerciseProgram(SQLModel, table=True):
"""Per-user programming for a specific exercise.
Attributes:
id: Primary key, auto-incremented.
user_id: FK to users table.
exercise_id: FK to exercises table.
wk1_reps: Week 1 target reps (string to support "30 sec" style).
wk4_reps: Week 4 target reps.
wk1_weight: Week 1 target weight (e.g., "30 lbs", "BW").
wk4_weight: Week 4 target weight.
created_at: Timestamp when the record was created.
updated_at: Timestamp of the last update.
"""
__tablename__ = "user_exercise_programs"
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id", index=True)
exercise_id: int = Field(foreign_key="exercises.id", index=True)
wk1_reps: str = Field(default="")
wk4_reps: str = Field(default="")
wk1_weight: str = Field(default="")
wk4_weight: str = Field(default="")
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)

33
app/models/warmup.py Normal file
View File

@@ -0,0 +1,33 @@
"""Warmup model for the standardized warmup routine.
Warmups are displayed before every workout in a fixed order.
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class Warmup(SQLModel, table=True):
"""A warmup exercise in the standardized routine.
Attributes:
id: Primary key, auto-incremented.
name: Warmup name (e.g., "Cat / Cow").
type: Category (e.g., "Thoracic Mob", "Hip Mobility").
reps: Rep scheme as a string (e.g., "8 reps", "8 each side").
form_cues: Detailed form instructions.
sort_order: Display order in the warmup sequence.
created_at: Timestamp when the record was created.
"""
__tablename__ = "warmups"
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
type: str = Field(default="")
reps: str = Field(default="")
form_cues: str = Field(default="")
sort_order: int = Field(default=0)
created_at: datetime = Field(default_factory=datetime.utcnow)

26
app/models/workout_day.py Normal file
View File

@@ -0,0 +1,26 @@
"""WorkoutDay model for the 4-day training split.
Defines the named workout days and their order.
"""
from typing import Optional
from sqlmodel import Field, SQLModel
class WorkoutDay(SQLModel, table=True):
"""A named workout day in the training program.
Attributes:
id: Primary key, auto-incremented.
name: Day name (Push, Pull, Lower, Full Body).
day_number: Order in the weekly rotation (1-4).
description: Brief description of the day's focus.
"""
__tablename__ = "workout_days"
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True, unique=True)
day_number: int = Field(unique=True)
description: str = Field(default="")

37
app/models/workout_log.py Normal file
View File

@@ -0,0 +1,37 @@
"""WorkoutLog model for per-exercise set logging.
Each log entry records one set of one exercise within a session.
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class WorkoutLog(SQLModel, table=True):
"""A single set log for an exercise within a workout session.
Attributes:
id: Primary key, auto-incremented.
session_id: FK to workout_sessions table.
exercise_id: FK to exercises table.
set_number: Which set this is (1, 2, 3...).
reps_completed: Actual reps performed.
weight_used: Weight used as string (e.g., "30 lbs", "BW").
felt_easy: Whether the user felt the set was easy (progression signal).
notes: Optional notes about this specific set.
created_at: Timestamp when the record was created.
"""
__tablename__ = "workout_logs"
id: Optional[int] = Field(default=None, primary_key=True)
session_id: int = Field(foreign_key="workout_sessions.id", index=True)
exercise_id: int = Field(foreign_key="exercises.id")
set_number: int = Field(default=1)
reps_completed: int = Field(default=0)
weight_used: str = Field(default="")
felt_easy: bool = Field(default=False)
notes: Optional[str] = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,31 @@
"""WorkoutSession model for tracking completed workout sessions.
Each session ties a user to a workout day on a specific date.
"""
import datetime as dt
from typing import Optional
from sqlmodel import Field, SQLModel
class WorkoutSession(SQLModel, table=True):
"""A completed workout session.
Attributes:
id: Primary key, auto-incremented.
user_id: FK to users table.
workout_day_id: FK to workout_days table.
date: The date the workout was performed.
notes: Optional free-text notes about the session.
created_at: Timestamp when the record was created.
"""
__tablename__ = "workout_sessions"
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id", index=True)
workout_day_id: int = Field(foreign_key="workout_days.id")
date: dt.date = Field(default_factory=dt.date.today)
notes: Optional[str] = Field(default=None)
created_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)

228
tests/test_models.py Normal file
View File

@@ -0,0 +1,228 @@
"""Tests for SQLModel model definitions."""
from datetime import datetime
from sqlmodel import SQLModel, Session, create_engine
from app.models.user import User
from app.models.exercise import Exercise
from app.models.warmup import Warmup
from app.models.workout_day import WorkoutDay
from app.models.user_exercise_program import UserExerciseProgram
from app.models.workout_session import WorkoutSession
from app.models.workout_log import WorkoutLog
from app.models.progress_log import ProgressLog
class TestModels:
"""Tests that all models can be instantiated and persisted."""
def _create_engine(self):
"""Create an in-memory SQLite engine with all tables."""
engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)
return engine
def test_user_model_roundtrip(self) -> None:
"""User model should persist and retrieve correctly."""
engine = self._create_engine()
user = User(
username="testuser",
password_hash="fakehash",
display_name="Test User",
height="6'0\"",
weight="200 lbs",
goals="Get strong",
is_admin=False,
)
with Session(engine) as session:
session.add(user)
session.commit()
session.refresh(user)
assert user.id is not None
assert user.username == "testuser"
assert user.is_admin is False
assert isinstance(user.created_at, datetime)
def test_exercise_model_roundtrip(self) -> None:
"""Exercise model should persist and retrieve correctly."""
engine = self._create_engine()
exercise = Exercise(
name="DB Chest Press (Floor)",
muscle_group="Chest",
workout_day="Push",
sets=3,
tempo="3-1-2",
form_cues="Lie on the floor...",
)
with Session(engine) as session:
session.add(exercise)
session.commit()
session.refresh(exercise)
assert exercise.id is not None
assert exercise.name == "DB Chest Press (Floor)"
def test_warmup_model_roundtrip(self) -> None:
"""Warmup model should persist and retrieve correctly."""
engine = self._create_engine()
warmup = Warmup(
name="Cat / Cow",
type="Thoracic Mob",
reps="8 reps",
form_cues="Start on hands and knees...",
sort_order=1,
)
with Session(engine) as session:
session.add(warmup)
session.commit()
session.refresh(warmup)
assert warmup.id is not None
def test_workout_day_model_roundtrip(self) -> None:
"""WorkoutDay model should persist and retrieve correctly."""
engine = self._create_engine()
day = WorkoutDay(
name="Push",
day_number=1,
description="Chest, shoulders, triceps",
)
with Session(engine) as session:
session.add(day)
session.commit()
session.refresh(day)
assert day.id is not None
assert day.day_number == 1
def test_user_exercise_program_model_roundtrip(self) -> None:
"""UserExerciseProgram model should persist with FK references."""
engine = self._create_engine()
with Session(engine) as session:
user = User(username="u", password_hash="h", display_name="U")
exercise = Exercise(
name="Test Ex", muscle_group="Test",
workout_day="Push", sets=3, tempo="3-1-2", form_cues="..."
)
session.add(user)
session.add(exercise)
session.commit()
session.refresh(user)
session.refresh(exercise)
program = UserExerciseProgram(
user_id=user.id,
exercise_id=exercise.id,
wk1_reps="8",
wk4_reps="12",
wk1_weight="30 lbs",
wk4_weight="40 lbs",
)
session.add(program)
session.commit()
session.refresh(program)
assert program.id is not None
assert program.user_id == user.id
def test_workout_session_model_roundtrip(self) -> None:
"""WorkoutSession model should persist correctly."""
engine = self._create_engine()
with Session(engine) as session:
user = User(username="u", password_hash="h", display_name="U")
day = WorkoutDay(name="Push", day_number=1, description="Push day")
session.add(user)
session.add(day)
session.commit()
session.refresh(user)
session.refresh(day)
ws = WorkoutSession(
user_id=user.id,
workout_day_id=day.id,
date=datetime.utcnow().date(),
)
session.add(ws)
session.commit()
session.refresh(ws)
assert ws.id is not None
def test_workout_log_model_roundtrip(self) -> None:
"""WorkoutLog model should persist correctly."""
engine = self._create_engine()
with Session(engine) as session:
user = User(username="u", password_hash="h", display_name="U")
day = WorkoutDay(name="Push", day_number=1, description="Push day")
exercise = Exercise(
name="Ex", muscle_group="Test",
workout_day="Push", sets=3, tempo="3-1-2", form_cues="..."
)
session.add_all([user, day, exercise])
session.commit()
session.refresh(user)
session.refresh(day)
session.refresh(exercise)
ws = WorkoutSession(
user_id=user.id,
workout_day_id=day.id,
date=datetime.utcnow().date(),
)
session.add(ws)
session.commit()
session.refresh(ws)
log = WorkoutLog(
session_id=ws.id,
exercise_id=exercise.id,
set_number=1,
reps_completed=8,
weight_used="30 lbs",
felt_easy=False,
)
session.add(log)
session.commit()
session.refresh(log)
assert log.id is not None
assert log.felt_easy is False
def test_progress_log_model_roundtrip(self) -> None:
"""ProgressLog model should persist correctly."""
engine = self._create_engine()
with Session(engine) as session:
user = User(username="u", password_hash="h", display_name="U")
exercise = Exercise(
name="Ex", muscle_group="Test",
workout_day="Push", sets=3, tempo="3-1-2", form_cues="..."
)
session.add_all([user, exercise])
session.commit()
session.refresh(user)
session.refresh(exercise)
pl = ProgressLog(
user_id=user.id,
exercise_id=exercise.id,
date=datetime.utcnow().date(),
suggested_reps=10,
suggested_weight="35 lbs",
actual_reps=10,
actual_weight="35 lbs",
progression_applied="reps_increase",
)
session.add(pl)
session.commit()
session.refresh(pl)
assert pl.id is not None
def test_all_models_importable_from_init(self) -> None:
"""All models should be importable from app.models."""
from app.models import (
User, Exercise, Warmup, WorkoutDay,
UserExerciseProgram, WorkoutSession, WorkoutLog, ProgressLog,
)
assert User is not None
assert Exercise is not None
assert Warmup is not None
assert WorkoutDay is not None
assert UserExerciseProgram is not None
assert WorkoutSession is not None
assert WorkoutLog is not None
assert ProgressLog is not None