Spaces:
Running
Running
| from typing import List, Optional | |
| from datetime import date | |
| import uuid | |
| from domain.training.run import Run | |
| from domain.training.weekly_snapshot import WeeklySnapshot | |
| class WeeklySnapshotBuilder: | |
| """ | |
| Pure domain component to build WeeklySnapshot objects from raw runs. | |
| Deterministic, no side effects, no repository dependencies. | |
| """ | |
| def build( | |
| self, | |
| runner_id: uuid.UUID, | |
| week_start_date: date, | |
| runs: List[Run] | |
| ) -> WeeklySnapshot: | |
| """ | |
| Aggregates a list of runs into a weekly snapshot for a specific week. | |
| """ | |
| if not runs: | |
| return WeeklySnapshot.from_metrics( | |
| runner_id=runner_id, | |
| week_start_date=week_start_date, | |
| total_distance_km=0.0, | |
| avg_pace_sec_per_km=0.0, | |
| total_time_sec=0, | |
| run_count=0, | |
| consistency_score=0.0, | |
| ) | |
| total_distance_m = sum(run.total_distance_m for run in runs) | |
| total_duration_s = sum(run.total_duration_s for run in runs) | |
| total_distance_km = total_distance_m / 1000.0 | |
| avg_pace_sec_per_km = 0.0 | |
| if total_distance_km > 0: | |
| avg_pace_sec_per_km = total_duration_s / total_distance_km | |
| hr_values = [run.avg_hr_bpm for run in runs if run.avg_hr_bpm is not None] | |
| avg_hr = sum(hr_values) / len(hr_values) if hr_values else None | |
| max_hr_values = [run.max_hr_bpm for run in runs if run.max_hr_bpm is not None] | |
| max_hr = max(max_hr_values) if max_hr_values else None | |
| # Simple consistency metric: 5 runs = 100% (min 1, max 5) | |
| consistency_score = float(min(len(runs) * 20, 100)) | |
| return WeeklySnapshot.from_metrics( | |
| runner_id=runner_id, | |
| week_start_date=week_start_date, | |
| total_distance_km=total_distance_km, | |
| avg_pace_sec_per_km=avg_pace_sec_per_km, | |
| total_time_sec=int(total_duration_s), | |
| run_count=len(runs), | |
| consistency_score=consistency_score, | |
| avg_hr=avg_hr, | |
| max_hr=max_hr, | |
| ) | |