Spaces:
Running
Running
| from typing import List, Optional, Literal | |
| from datetime import date, timedelta, datetime | |
| import uuid | |
| from domain.training.run import Run | |
| from domain.training.planned_session import PlannedSession | |
| from domain.runner.goal import Goal | |
| from domain.runner.profile import RunnerProfile | |
| def match_run_to_session(run: Run, sessions: List[PlannedSession]) -> Optional[uuid.UUID]: | |
| """ | |
| Matches a run to a planned session based on date and distance. | |
| Rules: | |
| - abs(run_date - planned_date) <= 1 day | |
| - run_distance >= 0.8 * target_distance | |
| - Match only one session (first match wins) | |
| """ | |
| run_date = run.start_time.date() | |
| run_dist_km = run.total_distance_m / 1000.0 | |
| for session in sessions: | |
| if session.completed_run_id is not None: | |
| continue | |
| date_diff = abs((run_date - session.planned_date).days) | |
| if date_diff <= 1 and run_dist_km >= (0.8 * session.target_distance_km): | |
| return session.id | |
| return None | |
| def classify_week(sessions: List[PlannedSession], current_volume: float, goal_volume: float) -> str: | |
| """ | |
| Classifies the week's structure based on completed sessions. | |
| Returns canonical string: strong_week, structured_but_light, rebuild_week, reset_week. | |
| """ | |
| if not sessions: | |
| return "reset_week" | |
| long_run_completed = any(s.session_type == "long_run" and s.completed_run_id for s in sessions) | |
| weekday_sessions = [s for s in sessions if s.session_type == "weekday"] | |
| weekdays_completed = sum(1 for s in weekday_sessions if s.completed_run_id) | |
| # Heuristics | |
| if long_run_completed: | |
| if weekdays_completed >= 2 or current_volume >= goal_volume: | |
| return "strong_week" | |
| else: | |
| return "structured_but_light" | |
| else: | |
| if weekdays_completed >= 2 or (current_volume > 0 and current_volume >= 0.5 * goal_volume): | |
| return "rebuild_week" | |
| else: | |
| return "reset_week" | |
| def generate_week_template( | |
| runner_id: uuid.UUID, | |
| week_start: date, | |
| goal: Optional[Goal], | |
| profile: Optional[RunnerProfile], | |
| previous_sessions: List[PlannedSession] = None, | |
| ) -> List[PlannedSession]: | |
| """ | |
| Generates a week template (list of PlannedSession). | |
| Logic: | |
| - Default 3 weekday + 1 long run | |
| - Long run distance based on goal (e.g. 30–40% weekly volume) | |
| - Weekday runs evenly distributed | |
| - Respect baseline weekly distance | |
| """ | |
| # 1. Determine Target Weekly Volume | |
| baseline = profile.baseline_weekly_km if (profile and profile.baseline_weekly_km is not None) else 20.0 | |
| target_volume = baseline | |
| if goal and goal.type == "volume" and goal.target_value is not None: | |
| target_volume = goal.target_value | |
| # Simple growth logic if previous week was successful (optional/not requested but good to have) | |
| # For now, stay simple as per requirements. | |
| # 2. Distribute Volume | |
| long_run_dist = target_volume * 0.35 | |
| weekday_total = target_volume - long_run_dist | |
| weekday_count = 3 | |
| weekday_dist = weekday_total / weekday_count | |
| # 3. Assign Dates | |
| # Mon (0), Tue (1), Wed (2), Thu (3), Fri (4), Sat (5), Sun (6) | |
| # We'll use Tue, Thu, Fri for weekdays and Sun for long run | |
| template_dates = { | |
| 1: ("weekday", weekday_dist), | |
| 3: ("weekday", weekday_dist), | |
| 4: ("weekday", weekday_dist), | |
| 6: ("long_run", long_run_dist), | |
| } | |
| sessions = [] | |
| for day_offset, (s_type, dist) in template_dates.items(): | |
| sessions.append( | |
| PlannedSession( | |
| runner_id=runner_id, | |
| week_start_date=week_start, | |
| session_type=s_type, | |
| planned_date=week_start + timedelta(days=day_offset), | |
| target_distance_km=round(dist, 1), | |
| ) | |
| ) | |
| return sessions | |