feat: define all 8 SQLModel tables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
35
app/models/exercise.py
Normal 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)
|
||||
39
app/models/progress_log.py
Normal file
39
app/models/progress_log.py
Normal 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
39
app/models/user.py
Normal 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)
|
||||
37
app/models/user_exercise_program.py
Normal file
37
app/models/user_exercise_program.py
Normal 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
33
app/models/warmup.py
Normal 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
26
app/models/workout_day.py
Normal 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
37
app/models/workout_log.py
Normal 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)
|
||||
31
app/models/workout_session.py
Normal file
31
app/models/workout_session.py
Normal 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
228
tests/test_models.py
Normal 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
|
||||
Reference in New Issue
Block a user