"""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")