Remove all authentication (login, sessions, bcrypt, itsdangerous) since the app runs on a private homelab LAN. Replace with a profile picker landing page and cookie-based profile selection (1-year expiry). - Add Alembic migration to drop password_hash/is_admin columns - Delete auth service, auth routes, login template, and auth tests - Rewrite app/utils/auth.py with NoProfileSelectedError and require_active_profile dependency - Add profile creation flow (GET/POST /profiles/create) - Rewrite home page as profile picker with card layout - Update all route files to use profile dependency instead of admin auth - Remove bcrypt and itsdangerous from requirements - Remove admin_username/admin_password from config - Update all tests for new profile-based access model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
6.7 KiB
Python
178 lines
6.7 KiB
Python
"""Service for seeding the database from YAML config files.
|
|
|
|
Reads config/exercises.yaml and config/user_programs.yaml to populate
|
|
the exercise library, warmups, workout days, and user programs.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import structlog
|
|
import yaml
|
|
from sqlmodel import Session, select
|
|
|
|
from app.models.exercise import Exercise
|
|
from app.models.user import User
|
|
from app.models.user_exercise_program import UserExerciseProgram
|
|
from app.models.warmup import Warmup
|
|
from app.models.workout_day import WorkoutDay
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
# Default workout day definitions
|
|
WORKOUT_DAYS = [
|
|
{"name": "Push", "day_number": 1, "description": "Chest, shoulders, triceps"},
|
|
{"name": "Pull", "day_number": 2, "description": "Back, biceps, traps"},
|
|
{"name": "Lower", "day_number": 3, "description": "Quads, hamstrings, glutes, calves"},
|
|
{"name": "Full Body", "day_number": 4, "description": "Compound full-body movements"},
|
|
]
|
|
|
|
|
|
class SeedService:
|
|
"""Seeds the database from YAML configuration files.
|
|
|
|
Args:
|
|
session: An active SQLModel Session.
|
|
config_dir: Path to the config directory containing YAML files.
|
|
"""
|
|
|
|
def __init__(self, session: Session, config_dir: Optional[Path] = None) -> None:
|
|
self._session = session
|
|
self._config_dir = config_dir or Path(__file__).resolve().parent.parent.parent / "config"
|
|
|
|
def _load_yaml(self, filename: str) -> dict:
|
|
"""Load and parse a YAML file from the config directory."""
|
|
filepath = self._config_dir / filename
|
|
with open(filepath, "r") as f:
|
|
return yaml.safe_load(f)
|
|
|
|
def seed_workout_days(self) -> None:
|
|
"""Seed the 4 workout day records if they don't already exist."""
|
|
existing = self._session.exec(select(WorkoutDay)).first()
|
|
if existing:
|
|
logger.info("seed_skipped", table="workout_days", reason="already seeded")
|
|
return
|
|
|
|
for day_data in WORKOUT_DAYS:
|
|
day = WorkoutDay(**day_data)
|
|
self._session.add(day)
|
|
self._session.commit()
|
|
logger.info("seed_complete", table="workout_days", count=len(WORKOUT_DAYS))
|
|
|
|
def seed_exercises(self) -> None:
|
|
"""Seed exercises from config/exercises.yaml if table is empty."""
|
|
existing = self._session.exec(select(Exercise)).first()
|
|
if existing:
|
|
logger.info("seed_skipped", table="exercises", reason="already seeded")
|
|
return
|
|
|
|
data = self._load_yaml("exercises.yaml")
|
|
exercises = data.get("exercises", [])
|
|
for ex in exercises:
|
|
exercise = Exercise(
|
|
name=ex["name"],
|
|
muscle_group=ex["muscle_group"],
|
|
workout_day=ex["workout_day"],
|
|
sets=ex.get("sets", 3),
|
|
tempo=ex.get("tempo", ""),
|
|
form_cues=ex.get("form_cues", "").strip(),
|
|
)
|
|
self._session.add(exercise)
|
|
self._session.commit()
|
|
logger.info("seed_complete", table="exercises", count=len(exercises))
|
|
|
|
def seed_warmups(self) -> None:
|
|
"""Seed warmups from config/exercises.yaml if table is empty."""
|
|
existing = self._session.exec(select(Warmup)).first()
|
|
if existing:
|
|
logger.info("seed_skipped", table="warmups", reason="already seeded")
|
|
return
|
|
|
|
data = self._load_yaml("exercises.yaml")
|
|
warmups = data.get("warmup", [])
|
|
for i, wu in enumerate(warmups, start=1):
|
|
warmup = Warmup(
|
|
name=wu["name"],
|
|
type=wu.get("type", ""),
|
|
reps=wu.get("reps", ""),
|
|
form_cues=wu.get("form_cues", "").strip(),
|
|
sort_order=i,
|
|
)
|
|
self._session.add(warmup)
|
|
self._session.commit()
|
|
logger.info("seed_complete", table="warmups", count=len(warmups))
|
|
|
|
def seed_user_programs(self) -> None:
|
|
"""Seed user profiles and their exercise programs from user_programs.yaml."""
|
|
data = self._load_yaml("user_programs.yaml")
|
|
programs = data.get("programs", [])
|
|
|
|
for program in programs:
|
|
username = program["user"].lower().replace(" ", "_")
|
|
display_name = program["user"]
|
|
profile = program.get("profile", {})
|
|
|
|
# Check if user already exists
|
|
existing_user = self._session.exec(
|
|
select(User).where(User.username == username)
|
|
).first()
|
|
|
|
if existing_user:
|
|
user = existing_user
|
|
logger.info("seed_skipped", table="users", user=username, reason="already exists")
|
|
else:
|
|
user = User(
|
|
username=username,
|
|
display_name=display_name,
|
|
height=profile.get("height", ""),
|
|
weight=profile.get("weight", ""),
|
|
goals=profile.get("goals", ""),
|
|
)
|
|
self._session.add(user)
|
|
self._session.commit()
|
|
self._session.refresh(user)
|
|
logger.info("seed_complete", table="users", user=username)
|
|
|
|
# Link exercises to user
|
|
for ex_data in program.get("exercises", []):
|
|
exercise = self._session.exec(
|
|
select(Exercise).where(Exercise.name == ex_data["name"])
|
|
).first()
|
|
|
|
if exercise is None:
|
|
logger.warning("seed_exercise_not_found", name=ex_data["name"])
|
|
continue
|
|
|
|
# Check if program already exists
|
|
existing_program = self._session.exec(
|
|
select(UserExerciseProgram).where(
|
|
UserExerciseProgram.user_id == user.id,
|
|
UserExerciseProgram.exercise_id == exercise.id,
|
|
)
|
|
).first()
|
|
|
|
if existing_program:
|
|
continue
|
|
|
|
uep = UserExerciseProgram(
|
|
user_id=user.id,
|
|
exercise_id=exercise.id,
|
|
wk1_reps=str(ex_data.get("wk1_reps", "")),
|
|
wk4_reps=str(ex_data.get("wk4_reps", "")),
|
|
wk1_weight=str(ex_data.get("wk1_weight", "")),
|
|
wk4_weight=str(ex_data.get("wk4_weight", "")),
|
|
)
|
|
self._session.add(uep)
|
|
|
|
self._session.commit()
|
|
logger.info("seed_complete", table="user_exercise_programs", user=username)
|
|
|
|
def seed_all(self) -> None:
|
|
"""Run all seed operations in the correct order."""
|
|
logger.info("seed_all_started")
|
|
self.seed_workout_days()
|
|
self.seed_exercises()
|
|
self.seed_warmups()
|
|
self.seed_user_programs()
|
|
logger.info("seed_all_complete")
|