<filename>rengine/workout_plan.py

from flask import Config
from rengine.workouts import BaseWorkout
from rengine.config import *
from rengine.data_structures import StrengthExerciseQueue
from rengine.workouts import AutoGeneratedWorkout, dictionary_addition
from typing import List, Tuple

def get_strenth_exercise_queue(
    muscle_groups: List[MuscleGroup],
    equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
):
    potential_strength_exercises = []

    if EquipmentAvailable.BARBELL in equipment_available or equipment_available == EquipmentAvailable.ALL:
        if (
            MuscleGroup.HAMSTRINGS in muscle_groups
            or MuscleGroup.QUAD in muscle_groups
        ):
            potential_strength_exercises.append(("Barbell Squat", 1))
            potential_strength_exercises.append(("Romanian Deadlift", 1))
        if MuscleGroup.CHEST in muscle_groups:
            potential_strength_exercises.append(("Barbell Bench Press", 2))
        if MuscleGroup.DELTOIDS in muscle_groups:
            potential_strength_exercises.append(("Barbell Shoulder Press", 3))
        if MuscleGroup.BACK in muscle_groups:
            potential_strength_exercises.append(("Bent-Over Barbell Row", 3))
    elif EquipmentAvailable.SMITH_MACHINE in equipment_available:
        if (
            MuscleGroup.HAMSTRINGS in muscle_groups
            or MuscleGroup.QUAD in muscle_groups
        ):
            potential_strength_exercises.append(("Smith Machine Squat", 1))
            potential_strength_exercises.append(("Smith Machine Deadlift", 1))
        if MuscleGroup.CHEST in muscle_groups:
            potential_strength_exercises.append(("Smith Machine Bench Press", 2))
        if MuscleGroup.DELTOIDS in muscle_groups:
            potential_strength_exercises.append(("Smith Machine Shoulder Press", 3))
        if MuscleGroup.BACK in muscle_groups:
            potential_strength_exercises.append(("Bent-Over Smith Machine Rows", 3))
    if (
        EquipmentAvailable.DIP_BAR in equipment_available
        and MuscleGroup.TRICEPS in muscle_groups
    ):
        potential_strength_exercises.append(("Weighted Dips", 3))
    if (
        EquipmentAvailable.PULLUP_BAR in equipment_available
        and MuscleGroup.BACK in muscle_groups
    ):
        potential_strength_exercises.append(("Weighted Pull-Ups", 3))
    return StrengthExerciseQueue(potential_strength_exercises)


class BaseWorkoutPlan:
    """
    General workout plan class

    Responsibilities
        - Add workouts
        - Delete workouts
        - Export to excel


    """

    def __init__(self, workouts=None):
        self.workouts = workouts or []

    def add_workout(self, workout: BaseWorkout):
        """Simply adds workout to workout plan."""
        assert isinstance(workout, BaseWorkout)
        self.workouts.append(workout)

    def export_to_excel(self):
        """Exports workout plan to a nice looking excel sheet."""
        pass

    @property
    def cummative_load_per_muscle_group(self):
        """Calculates the cummulative load per muscle group."""
        return dictionary_addition([workout.load_per_muscle_group for workout in self.workouts])

    @property
    def unique_exercise_names(self):
        """Returns the set of all exercise names in workout plan."""
        return list(set(
            exercise.exercise_name for workout in self.workouts for exercise in workout.workout
        ))

    def __str__(self):
        workouts = ""
        for i,workout in enumerate(self.workouts):
            workouts += f"\nDay {str(i+1)}\n"+ str(workout.df) + "\n"
        return workouts

class AutoGeneratedWorkoutPlan(BaseWorkoutPlan):
    def __init__(
        self,
        target_time_per_workout,
        workout_generation_blueprint: List[dict],
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        workouts=None,
    ):
        """
        Adds support for auto generating workout plans based on workout_generation_blueprint. This list holds dictionaries with following form:

        workout_generation_blueprint = [
            ("Back", "Biceps", "Calves"),
            ("Chest", "Triceps")
        ]

        This will generate 1 back and bicep workouts then 1 chest and tricep workouts.
        """
        self.target_time_per_workout = target_time_per_workout
        self.workout_generation_blueprint = workout_generation_blueprint
        self.equipment_available = equipment_available
        self._strength_exercise_queues = {}

        super().__init__(workouts)

    def generate(self):
        """Auto generates the workouts."""
        for muscle_groups in self.workout_generation_blueprint:
            self._create_strength_exercise_queue_if_valid(muscle_groups)
            workout = AutoGeneratedWorkout(
                    self.target_time_per_workout,
                    muscle_groups,
                    self._strength_exercise_queues[muscle_groups].get().exercise_name,
                    self.equipment_available,
                    self.unique_exercise_names,
                    self.cummative_load_per_muscle_group,
                )
            workout.create()
            self.add_workout(
                workout
            )

    def _create_strength_exercise_queue_if_valid(self, muscle_groups):
        """If _strength_exercise_queue does not have a key with a group of muscles then create a queue and add to dictionary."""
        if muscle_groups not in self._strength_exercise_queues:
            self._strength_exercise_queues[muscle_groups] = get_strenth_exercise_queue(
                muscle_groups, self.equipment_available
            )



class PresetAGWP(AutoGeneratedWorkoutPlan):
    """Convience class to easily configure specific auto generated workout splits"""
    CONFIG = None
    """Config must include 'muscles_worked_by_day which is a dictionary that includes number of days that can be chosen and corresponding muscle groups'"""

    def __init__(self, target_time_per_workout: int, days: int, equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL, config = None):
        self._validate_config()
        self.config = config or self.__class__.CONFIG
        blueprints = self.config["muscles_worked_by_day"][days]
        self._validate_n_days(days)
        super().__init__(target_time_per_workout, blueprints, equipment_available)


    def _validate_config(self):
        """validates that dictionary has acceptable data."""
        config = self.__class__.CONFIG
        #check if config does not have correct number of workout blueprint tuples
        try:
            for n_days in config["muscles_worked_by_day"]:
                assert len(config["muscles_worked_by_day"][n_days]) == n_days
        except:
            raise Exception(f"Config for {self.__class__.__name__} has incorrect number of workout blueprints at n_days = {str(n_days)}.")

    def _validate_n_days(self, n_days):
        """validates that user has chosen number of days that agrees with config."""
        try:
            assert n_days in self.config["muscles_worked_by_day"]
        except:
            raise Exception(f"{str(n_days)} days is not a valid option for {self.__class__.__name__}.")


class FullBodyWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.FULL_BODY
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.FULL_BODY]

class UpperLowerWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.UPPER_LOWER_SPLIT
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.UPPER_LOWER_SPLIT]

class PushPullLegsWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.PUSH_PULL_LEGS
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.PUSH_PULL_LEGS]

class BroSplitWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.BRO_SPLIT
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.BRO_SPLIT]



def generate_workout_plan(split: WorkoutSplit, time: int, days: int, **kwargs):
    if split == WorkoutSplit.FULL_BODY:
        plan = FullBodyWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.UPPER_LOWER_SPLIT:
        plan = UpperLowerWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.PUSH_PULL_LEGS:
        plan = PushPullLegsWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.BRO_SPLIT:
        plan = BroSplitWorkoutPlan(time, days, **kwargs)
    plan.generate()
    return plan
    


if __name__ == "__main__":
    plan = generate_workout_plan("ppl", 60, 6)
    print(plan)
<filename>rengine/workout_plan.py

from flask import Config
from rengine.workouts import BaseWorkout
from rengine.config import *
from rengine.data_structures import StrengthExerciseQueue
from rengine.workouts import AutoGeneratedWorkout, dictionary_addition
from typing import List, Tuple

def get_strenth_exercise_queue(
    muscle_groups: List[MuscleGroup],
    equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
):
    potential_strength_exercises = []

    if EquipmentAvailable.BARBELL in equipment_available or equipment_available == EquipmentAvailable.ALL:
        if (
            MuscleGroup.HAMSTRINGS in muscle_groups
            or MuscleGroup.QUAD in muscle_groups
        ):
            potential_strength_exercises.append(("Barbell Squat", 1))
            potential_strength_exercises.append(("Romanian Deadlift", 1))
        if MuscleGroup.CHEST in muscle_groups:
            potential_strength_exercises.append(("Barbell Bench Press", 2))
        if MuscleGroup.DELTOIDS in muscle_groups:
            potential_strength_exercises.append(("Barbell Shoulder Press", 3))
        if MuscleGroup.BACK in muscle_groups:
            potential_strength_exercises.append(("Bent-Over Barbell Row", 3))
    elif EquipmentAvailable.SMITH_MACHINE in equipment_available:
        if (
            MuscleGroup.HAMSTRINGS in muscle_groups
            or MuscleGroup.QUAD in muscle_groups
        ):
            potential_strength_exercises.append(("Smith Machine Squat", 1))
            potential_strength_exercises.append(("Smith Machine Deadlift", 1))
        if MuscleGroup.CHEST in muscle_groups:
            potential_strength_exercises.append(("Smith Machine Bench Press", 2))
        if MuscleGroup.DELTOIDS in muscle_groups:
            potential_strength_exercises.append(("Smith Machine Shoulder Press", 3))
        if MuscleGroup.BACK in muscle_groups:
            potential_strength_exercises.append(("Bent-Over Smith Machine Rows", 3))
    if (
        EquipmentAvailable.DIP_BAR in equipment_available
        and MuscleGroup.TRICEPS in muscle_groups
    ):
        potential_strength_exercises.append(("Weighted Dips", 3))
    if (
        EquipmentAvailable.PULLUP_BAR in equipment_available
        and MuscleGroup.BACK in muscle_groups
    ):
        potential_strength_exercises.append(("Weighted Pull-Ups", 3))
    return StrengthExerciseQueue(potential_strength_exercises)


class BaseWorkoutPlan:
    """
    General workout plan class

    Responsibilities
        - Add workouts
        - Delete workouts
        - Export to excel


    """

    def __init__(self, workouts=None):
        self.workouts = workouts or []

    def add_workout(self, workout: BaseWorkout):
        """Simply adds workout to workout plan."""
        assert isinstance(workout, BaseWorkout)
        self.workouts.append(workout)

    def export_to_excel(self):
        """Exports workout plan to a nice looking excel sheet."""
        pass

    @property
    def cummative_load_per_muscle_group(self):
        """Calculates the cummulative load per muscle group."""
        return dictionary_addition([workout.load_per_muscle_group for workout in self.workouts])

    @property
    def unique_exercise_names(self):
        """Returns the set of all exercise names in workout plan."""
        return list(set(
            exercise.exercise_name for workout in self.workouts for exercise in workout.workout
        ))

    def __str__(self):
        workouts = ""
        for i,workout in enumerate(self.workouts):
            workouts += f"\nDay {str(i+1)}\n"+ str(workout.df) + "\n"
        return workouts

class AutoGeneratedWorkoutPlan(BaseWorkoutPlan):
    def __init__(
        self,
        target_time_per_workout,
        workout_generation_blueprint: List[dict],
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        workouts=None,
    ):
        """
        Adds support for auto generating workout plans based on workout_generation_blueprint. This list holds dictionaries with following form:

        workout_generation_blueprint = [
            ("Back", "Biceps", "Calves"),
            ("Chest", "Triceps")
        ]

        This will generate 1 back and bicep workouts then 1 chest and tricep workouts.
        """
        self.target_time_per_workout = target_time_per_workout
        self.workout_generation_blueprint = workout_generation_blueprint
        self.equipment_available = equipment_available
        self._strength_exercise_queues = {}

        super().__init__(workouts)

    def generate(self):
        """Auto generates the workouts."""
        for muscle_groups in self.workout_generation_blueprint:
            self._create_strength_exercise_queue_if_valid(muscle_groups)
            workout = AutoGeneratedWorkout(
                    self.target_time_per_workout,
                    muscle_groups,
                    self._strength_exercise_queues[muscle_groups].get().exercise_name,
                    self.equipment_available,
                    self.unique_exercise_names,
                    self.cummative_load_per_muscle_group,
                )
            workout.create()
            self.add_workout(
                workout
            )

    def _create_strength_exercise_queue_if_valid(self, muscle_groups):
        """If _strength_exercise_queue does not have a key with a group of muscles then create a queue and add to dictionary."""
        if muscle_groups not in self._strength_exercise_queues:
            self._strength_exercise_queues[muscle_groups] = get_strenth_exercise_queue(
                muscle_groups, self.equipment_available
            )



class PresetAGWP(AutoGeneratedWorkoutPlan):
    """Convience class to easily configure specific auto generated workout splits"""
    CONFIG = None
    """Config must include 'muscles_worked_by_day which is a dictionary that includes number of days that can be chosen and corresponding muscle groups'"""

    def __init__(self, target_time_per_workout: int, days: int, equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL, config = None):
        self._validate_config()
        self.config = config or self.__class__.CONFIG
        blueprints = self.config["muscles_worked_by_day"][days]
        self._validate_n_days(days)
        super().__init__(target_time_per_workout, blueprints, equipment_available)


    def _validate_config(self):
        """validates that dictionary has acceptable data."""
        config = self.__class__.CONFIG
        #check if config does not have correct number of workout blueprint tuples
        try:
            for n_days in config["muscles_worked_by_day"]:
                assert len(config["muscles_worked_by_day"][n_days]) == n_days
        except:
            raise Exception(f"Config for {self.__class__.__name__} has incorrect number of workout blueprints at n_days = {str(n_days)}.")

    def _validate_n_days(self, n_days):
        """validates that user has chosen number of days that agrees with config."""
        try:
            assert n_days in self.config["muscles_worked_by_day"]
        except:
            raise Exception(f"{str(n_days)} days is not a valid option for {self.__class__.__name__}.")


class FullBodyWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.FULL_BODY
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.FULL_BODY]

class UpperLowerWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.UPPER_LOWER_SPLIT
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.UPPER_LOWER_SPLIT]

class PushPullLegsWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.PUSH_PULL_LEGS
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.PUSH_PULL_LEGS]

class BroSplitWorkoutPlan(PresetAGWP):
    WORKOUT_SPLIT = WorkoutSplit.BRO_SPLIT
    CONFIG = AUTO_GENERATED_WORKOUT_PLAN_SPLIT_CONFIG[WorkoutSplit.BRO_SPLIT]



def generate_workout_plan(split: WorkoutSplit, time: int, days: int, **kwargs):
    if split == WorkoutSplit.FULL_BODY:
        plan = FullBodyWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.UPPER_LOWER_SPLIT:
        plan = UpperLowerWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.PUSH_PULL_LEGS:
        plan = PushPullLegsWorkoutPlan(time, days, **kwargs)
    elif split == WorkoutSplit.BRO_SPLIT:
        plan = BroSplitWorkoutPlan(time, days, **kwargs)
    plan.generate()
    return plan
    


if __name__ == "__main__":
    plan = generate_workout_plan("ppl", 60, 6)
    print(plan)
General workout plan class      Responsibilities         - Add workouts         - Delete workouts         - Export to excel Simply adds workout to workout plan. Exports workout plan to a nice looking excel sheet. Calculates the cummulative load per muscle group. Returns the set of all exercise names in workout plan. Adds support for auto generating workout plans based on workout_generation_blueprint. This list holds dictionaries with following form:          workout_generation_blueprint = [             ("Back", "Biceps", "Calves"),             ("Chest", "Triceps")         ]          This will generate 1 back and bicep workouts then 1 chest and tricep workouts. Auto generates the workouts. If _strength_exercise_queue does not have a key with a group of muscles then create a queue and add to dictionary. Convience class to easily configure specific auto generated workout splits Config must include 'muscles_worked_by_day which is a dictionary that includes number of days that can be chosen and corresponding muscle groups' validates that dictionary has acceptable data. #check if config does not have correct number of workout blueprint tuples validates that user has chosen number of days that agrees with config.