Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from datetime import datetime, timedelta | |
| from sqlmodel import Session, select | |
| from tracker.active_program import resolve_active_program | |
| from tracker.config import TrackerConfig | |
| from tracker.models import ( | |
| BodyWeightLog, | |
| Exercise, | |
| Program, | |
| ProgramExercise, | |
| RunLog, | |
| WorkoutLog, | |
| ) | |
| from tracker.progression import suggest_next | |
| from tracker.training_schemas import ( | |
| AddProgramExerciseInput, | |
| BodyWeightEntry, | |
| CreateProgramInput, | |
| ExerciseResponse, | |
| ExerciseWithSuggestion, | |
| HistoryResponse, | |
| LogRunInput, | |
| LogWorkoutInput, | |
| ProgramDetailResponse, | |
| ProgramExerciseDetail, | |
| ProgramSummary, | |
| RunLogEntry, | |
| TodayResponse, | |
| UpdateProgramExerciseInput, | |
| WeightInput, | |
| WorkoutSessionSummary, | |
| ) | |
| from tracker.workout_completion import find_last_completed_workout | |
| from tracker.workout_day_text import build_program_day_summary_text | |
| def latest_body_weight_kg(session: Session) -> float | None: | |
| row = session.exec( | |
| select(BodyWeightLog).order_by(BodyWeightLog.logged_at.desc()).limit(1), | |
| ).first() | |
| if row is None: | |
| return None | |
| return row.weight_kg | |
| def fetch_today( | |
| session: Session, | |
| cfg: TrackerConfig, | |
| day_label: str, | |
| program_id: int | None, | |
| ) -> tuple[TodayResponse | None, str | None]: | |
| if program_id is not None: | |
| program = session.get(Program, program_id) | |
| else: | |
| program = resolve_active_program(session, cfg) | |
| if program is None: | |
| return None, "No program found" | |
| program_exercises = session.exec( | |
| select(ProgramExercise) | |
| .where(ProgramExercise.program_id == program.program_id) | |
| .where(ProgramExercise.day_label == day_label) | |
| .order_by(ProgramExercise.order_index), | |
| ).all() | |
| day_summary_text = build_program_day_summary_text( | |
| list(program_exercises), | |
| session, | |
| tracker_base_url=cfg.tracker_app_url, | |
| ) | |
| body_weight_kg = latest_body_weight_kg(session) | |
| exercises_with_suggestions: list[ExerciseWithSuggestion] = [] | |
| for pe in program_exercises: | |
| exercise = session.get(Exercise, pe.exercise_id) | |
| if exercise is None: | |
| continue | |
| last_logs = session.exec( | |
| select(WorkoutLog) | |
| .where(WorkoutLog.exercise_id == pe.exercise_id) | |
| .where(WorkoutLog.program_id == program.program_id) | |
| .where(WorkoutLog.day_label == day_label) | |
| .order_by(WorkoutLog.logged_at.desc()) | |
| .limit(pe.target_sets), | |
| ).all() | |
| suggestion = suggest_next( | |
| pe, | |
| last_logs, | |
| exercise, | |
| body_weight_kg, | |
| calibration_bench_lb_per_hand=cfg.calibration_dumbbell_bench_lb_per_hand, | |
| calibration_squat_lb_per_hand=cfg.calibration_dumbbell_squat_lb_per_hand, | |
| dumbbell_weight_quantum_lb=cfg.dumbbell_weight_quantum_lb, | |
| ) | |
| exercises_with_suggestions.append(ExerciseWithSuggestion( | |
| exercise_id=exercise.exercise_id, | |
| name=exercise.name, | |
| muscle_group=exercise.muscle_group, | |
| equipment=exercise.equipment, | |
| target_sets=pe.target_sets, | |
| target_rep_min=pe.target_rep_min, | |
| target_rep_max=pe.target_rep_max, | |
| rest_seconds=pe.rest_seconds, | |
| superset_group=pe.superset_group, | |
| weight_increment_kg=pe.weight_increment_kg, | |
| suggestion=suggestion, | |
| )) | |
| return TodayResponse( | |
| program_name=program.name, | |
| program_id=program.program_id, | |
| day_label=day_label, | |
| day_summary_text=day_summary_text, | |
| exercises=exercises_with_suggestions, | |
| ), None | |
| def log_workout_session(session: Session, body: LogWorkoutInput) -> int: | |
| now = datetime.now() | |
| logged_count = 0 | |
| for s in body.sets: | |
| log_entry = WorkoutLog( | |
| exercise_id=s.exercise_id, | |
| program_id=body.program_id, | |
| day_label=body.day_label, | |
| logged_at=now, | |
| set_number=s.set_number, | |
| reps_target=s.reps_done, | |
| reps_done=s.reps_done, | |
| weight_kg=s.weight_kg, | |
| completed=True, | |
| ) | |
| session.add(log_entry) | |
| logged_count += 1 | |
| session.commit() | |
| return logged_count | |
| def log_body_weight(session: Session, body: WeightInput) -> float: | |
| today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) | |
| today_end = today_start + timedelta(days=1) | |
| existing = session.exec( | |
| select(BodyWeightLog) | |
| .where(BodyWeightLog.logged_at >= today_start) | |
| .where(BodyWeightLog.logged_at < today_end), | |
| ).first() | |
| if existing: | |
| existing.weight_kg = body.weight_kg | |
| session.add(existing) | |
| else: | |
| session.add(BodyWeightLog(weight_kg=body.weight_kg)) | |
| session.commit() | |
| return body.weight_kg | |
| def run_log_to_entry(session: Session, row: RunLog) -> RunLogEntry: | |
| program_name = "" | |
| if row.program_id is not None: | |
| program = session.get(Program, row.program_id) | |
| program_name = program.name if program else "" | |
| run_log_id = row.run_log_id | |
| if run_log_id is None: | |
| msg = "run_log_id missing after load" | |
| raise ValueError(msg) | |
| return RunLogEntry( | |
| run_log_id=run_log_id, | |
| logged_at=row.logged_at, | |
| program_id=row.program_id, | |
| day_label=row.day_label, | |
| program_name=program_name, | |
| distance_km=row.distance_km, | |
| duration_minutes=row.duration_minutes, | |
| notes=row.notes, | |
| ) | |
| def log_running_session(session: Session, body: LogRunInput) -> tuple[int | None, str | None]: | |
| program = session.get(Program, body.program_id) | |
| if program is None: | |
| return None, "Program not found" | |
| row = RunLog( | |
| program_id=body.program_id, | |
| day_label=body.day_label, | |
| distance_km=body.distance_km, | |
| duration_minutes=body.duration_minutes, | |
| notes=body.notes, | |
| ) | |
| session.add(row) | |
| session.commit() | |
| session.refresh(row) | |
| run_log_id = row.run_log_id | |
| if run_log_id is None: | |
| return None, "Run log insert failed" | |
| return run_log_id, None | |
| def fetch_runs( | |
| session: Session, | |
| days: int, | |
| program_id: int | None, | |
| day_label: str | None, | |
| ) -> list[RunLogEntry]: | |
| cutoff = datetime.now() - timedelta(days=days) | |
| statement = select(RunLog).where(RunLog.logged_at >= cutoff) | |
| if program_id is not None: | |
| statement = statement.where(RunLog.program_id == program_id) | |
| if day_label is not None: | |
| statement = statement.where(RunLog.day_label == day_label) | |
| statement = statement.order_by(RunLog.logged_at.desc()) | |
| rows = session.exec(statement).all() | |
| return [ | |
| run_log_to_entry(session, r) | |
| for r in rows | |
| if r.run_log_id is not None | |
| ] | |
| def fetch_history(session: Session, days: int) -> HistoryResponse: | |
| cutoff = datetime.now() - timedelta(days=days) | |
| weights = session.exec( | |
| select(BodyWeightLog) | |
| .where(BodyWeightLog.logged_at >= cutoff) | |
| .order_by(BodyWeightLog.logged_at), | |
| ).all() | |
| logs = session.exec( | |
| select(WorkoutLog) | |
| .where(WorkoutLog.logged_at >= cutoff) | |
| .order_by(WorkoutLog.logged_at), | |
| ).all() | |
| sessions_map: dict[str, list[WorkoutLog]] = {} | |
| for log in logs: | |
| date_key = log.logged_at.strftime("%Y-%m-%d") | |
| day_key = f"{date_key}:{log.program_id}:{log.day_label}" | |
| sessions_map.setdefault(day_key, []).append(log) | |
| workout_sessions: list[WorkoutSessionSummary] = [] | |
| for group_logs in sessions_map.values(): | |
| first = group_logs[0] | |
| program = session.get(Program, first.program_id) | |
| exercise_ids = {log.exercise_id for log in group_logs} | |
| workout_sessions.append(WorkoutSessionSummary( | |
| logged_at=first.logged_at, | |
| program_name=program.name if program else "Unknown", | |
| day_label=first.day_label, | |
| exercise_count=len(exercise_ids), | |
| total_sets=len(group_logs), | |
| )) | |
| run_rows = session.exec( | |
| select(RunLog) | |
| .where(RunLog.logged_at >= cutoff) | |
| .order_by(RunLog.logged_at.desc()), | |
| ).all() | |
| runs_list = [ | |
| run_log_to_entry(session, r) | |
| for r in run_rows | |
| if r.run_log_id is not None | |
| ] | |
| return HistoryResponse( | |
| body_weights=[ | |
| BodyWeightEntry(logged_at=w.logged_at, weight_kg=w.weight_kg) | |
| for w in weights | |
| ], | |
| workout_sessions=sorted(workout_sessions, key=lambda s: s.logged_at), | |
| runs=runs_list, | |
| ) | |
| def list_program_summaries(session: Session) -> list[ProgramSummary]: | |
| programs = session.exec(select(Program)).all() | |
| result: list[ProgramSummary] = [] | |
| for p in programs: | |
| pe_list = session.exec( | |
| select(ProgramExercise) | |
| .where(ProgramExercise.program_id == p.program_id), | |
| ).all() | |
| day_labels = {pe.day_label for pe in pe_list} | |
| result.append(ProgramSummary( | |
| program_id=p.program_id, | |
| name=p.name, | |
| description=p.description, | |
| day_count=len(day_labels), | |
| exercise_count=len(pe_list), | |
| )) | |
| return result | |
| def create_program_from_input(session: Session, body: CreateProgramInput) -> ProgramSummary: | |
| program = Program(name=body.name, description=body.description) | |
| session.add(program) | |
| session.flush() | |
| total_exercises = 0 | |
| for day in body.days: | |
| for ex in day.exercises: | |
| session.add(ProgramExercise( | |
| program_id=program.program_id, | |
| day_label=day.day_label, | |
| exercise_id=ex.exercise_id, | |
| order_index=ex.order_index, | |
| target_sets=ex.target_sets, | |
| target_rep_min=ex.target_rep_min, | |
| target_rep_max=ex.target_rep_max, | |
| weight_increment_kg=ex.weight_increment_kg, | |
| rest_seconds=ex.rest_seconds, | |
| superset_group=ex.superset_group, | |
| starting_weight_kg=ex.starting_weight_kg, | |
| )) | |
| total_exercises += 1 | |
| session.commit() | |
| return ProgramSummary( | |
| program_id=program.program_id, | |
| name=program.name, | |
| description=program.description, | |
| day_count=len(body.days), | |
| exercise_count=total_exercises, | |
| ) | |
| def fetch_active_program_detail( | |
| session: Session, | |
| cfg: TrackerConfig, | |
| ) -> tuple[ProgramDetailResponse | None, str | None]: | |
| program = resolve_active_program(session, cfg) | |
| if program is None: | |
| return None, "No program found" | |
| pe_list = session.exec( | |
| select(ProgramExercise) | |
| .where(ProgramExercise.program_id == program.program_id), | |
| ).all() | |
| day_labels = sorted({pe.day_label for pe in pe_list}) | |
| last_day, last_date = find_last_completed_workout(session, program.program_id) | |
| return ProgramDetailResponse( | |
| program_id=program.program_id, | |
| name=program.name, | |
| description=program.description, | |
| day_labels=day_labels, | |
| last_workout_day=last_day, | |
| last_workout_date=last_date, | |
| ), None | |
| def list_exercise_catalog(session: Session) -> list[ExerciseResponse]: | |
| exercises = session.exec(select(Exercise).order_by(Exercise.name)).all() | |
| return [ | |
| ExerciseResponse( | |
| exercise_id=ex.exercise_id, | |
| name=ex.name, | |
| muscle_group=ex.muscle_group, | |
| equipment=ex.equipment, | |
| ) | |
| for ex in exercises | |
| ] | |
| def add_exercise_to_program( | |
| session: Session, | |
| program_id: int, | |
| body: AddProgramExerciseInput, | |
| ) -> tuple[ProgramExerciseDetail | None, str | None]: | |
| program = session.get(Program, program_id) | |
| if program is None: | |
| return None, f"Program {program_id} not found" | |
| exercise = session.get(Exercise, body.exercise_id) | |
| if exercise is None: | |
| return None, f"Exercise {body.exercise_id} not found" | |
| pe = ProgramExercise( | |
| program_id=program_id, | |
| day_label=body.day_label, | |
| exercise_id=body.exercise_id, | |
| order_index=body.order_index, | |
| target_sets=body.target_sets, | |
| target_rep_min=body.target_rep_min, | |
| target_rep_max=body.target_rep_max, | |
| weight_increment_kg=body.weight_increment_kg, | |
| rest_seconds=body.rest_seconds, | |
| superset_group=body.superset_group, | |
| starting_weight_kg=body.starting_weight_kg, | |
| ) | |
| session.add(pe) | |
| session.commit() | |
| return ProgramExerciseDetail( | |
| exercise_id=body.exercise_id, | |
| exercise_name=exercise.name, | |
| day_label=body.day_label, | |
| order_index=body.order_index, | |
| target_sets=body.target_sets, | |
| target_rep_min=body.target_rep_min, | |
| target_rep_max=body.target_rep_max, | |
| weight_increment_kg=body.weight_increment_kg, | |
| rest_seconds=body.rest_seconds, | |
| superset_group=body.superset_group, | |
| starting_weight_kg=body.starting_weight_kg, | |
| ), None | |
| def remove_program_exercise( | |
| session: Session, | |
| program_id: int, | |
| exercise_id: int, | |
| day_label: str, | |
| ) -> str | None: | |
| pe = session.exec( | |
| select(ProgramExercise) | |
| .where(ProgramExercise.program_id == program_id) | |
| .where(ProgramExercise.exercise_id == exercise_id) | |
| .where(ProgramExercise.day_label == day_label), | |
| ).first() | |
| if pe is None: | |
| return ( | |
| f"Exercise {exercise_id} not found in program {program_id} day {day_label}" | |
| ) | |
| session.delete(pe) | |
| session.commit() | |
| return None | |
| def update_program_exercise( | |
| session: Session, | |
| program_id: int, | |
| exercise_id: int, | |
| day_label: str, | |
| body: UpdateProgramExerciseInput, | |
| ) -> tuple[ProgramExerciseDetail | None, str | None]: | |
| pe = session.exec( | |
| select(ProgramExercise) | |
| .where(ProgramExercise.program_id == program_id) | |
| .where(ProgramExercise.exercise_id == exercise_id) | |
| .where(ProgramExercise.day_label == day_label), | |
| ).first() | |
| if pe is None: | |
| return None, ( | |
| f"Exercise {exercise_id} not found in program {program_id} day {day_label}" | |
| ) | |
| if body.target_sets is not None: | |
| pe.target_sets = body.target_sets | |
| if body.target_rep_min is not None: | |
| pe.target_rep_min = body.target_rep_min | |
| if body.target_rep_max is not None: | |
| pe.target_rep_max = body.target_rep_max | |
| if body.weight_increment_kg is not None: | |
| pe.weight_increment_kg = body.weight_increment_kg | |
| if body.rest_seconds is not None: | |
| pe.rest_seconds = body.rest_seconds | |
| if body.order_index is not None: | |
| pe.order_index = body.order_index | |
| if body.superset_group is not None: | |
| pe.superset_group = body.superset_group | |
| if body.starting_weight_kg is not None: | |
| pe.starting_weight_kg = body.starting_weight_kg | |
| session.add(pe) | |
| session.commit() | |
| exercise = session.get(Exercise, exercise_id) | |
| return ProgramExerciseDetail( | |
| exercise_id=exercise_id, | |
| exercise_name=exercise.name if exercise else "Unknown", | |
| day_label=day_label, | |
| order_index=pe.order_index, | |
| target_sets=pe.target_sets, | |
| target_rep_min=pe.target_rep_min, | |
| target_rep_max=pe.target_rep_max, | |
| weight_increment_kg=pe.weight_increment_kg, | |
| rest_seconds=pe.rest_seconds, | |
| superset_group=pe.superset_group, | |
| starting_weight_kg=pe.starting_weight_kg, | |
| ), None | |