feat: Initial implementation of FitTrack Report Generator

This commit introduces the initial version of the FitTrack Report Generator, a FastAPI application for analyzing workout files.

Key features include:
- Parsing of FIT, TCX, and GPX workout files.
- Analysis of power, heart rate, speed, and elevation data.
- Generation of summary reports and charts.
- REST API for single and batch workout analysis.

The project structure has been set up with a `src` directory for core logic, an `api` directory for the FastAPI application, and a `tests` directory for unit, integration, and contract tests.

The development workflow is configured to use Docker and modern Python tooling.
This commit is contained in:
2025-10-11 09:54:13 -07:00
parent 6643a64ff0
commit 9e0bd322d3
152 changed files with 25695 additions and 49 deletions

View File

@@ -0,0 +1,103 @@
import pytest
import pandas as pd
from datetime import datetime, timedelta
from src.core.workout_data import WorkoutData, WorkoutMetadata, PowerData, HeartRateData, SpeedData, ElevationData
from src.core.chart_generator import ChartGenerator
@pytest.fixture
def sample_workout_data():
# Create dummy time-series data
timestamps = pd.to_datetime([datetime(2025, 1, 1, 10, 0, 0) + timedelta(seconds=i) for i in range(600)])
power = pd.Series([150 + 50 * (i % 10) for i in range(600)], index=timestamps)
heart_rate = pd.Series([120 + 10 * (i % 5) for i in range(600)], index=timestamps)
speed = pd.Series([5 + 2 * (i % 7) for i in range(600)], index=timestamps)
altitude = pd.Series([100 + 10 * (i % 12) for i in range(600)], index=timestamps)
time_series_data = pd.DataFrame({
"power": power,
"heart_rate": heart_rate,
"speed": speed,
"altitude": altitude
})
metadata = WorkoutMetadata(
start_time=datetime(2025, 1, 1, 10, 0, 0),
duration=timedelta(minutes=10),
device="Garmin",
file_type="FIT"
)
power_data = PowerData(
raw_power_stream=power.tolist(),
average_power=power.mean(),
normalized_power=power.mean() * 1.05, # Dummy value
intensity_factor=0.8,
training_stress_score=50,
zone_distribution={'Z1': 100, 'Z2': 200, 'Z3': 300}
)
heart_rate_data = HeartRateData(
raw_hr_stream=heart_rate.tolist(),
average_hr=heart_rate.mean(),
max_hr=heart_rate.max(),
zone_distribution={'Z1': 150, 'Z2': 250, 'Z3': 200}
)
speed_data = SpeedData(
raw_speed_stream=speed.tolist(),
average_speed=speed.mean(),
max_speed=speed.max(),
zone_distribution={'S1': 100, 'S2': 200, 'S3': 300}
)
elevation_data = ElevationData(
raw_elevation_stream=altitude.tolist(),
total_ascent=100,
total_descent=50,
max_elevation=200,
min_elevation=50
)
return WorkoutData(
metadata=metadata,
time_series_data=time_series_data,
power_data=power_data,
heart_rate_data=heart_rate_data,
speed_data=speed_data,
elevation_data=elevation_data
)
def test_generate_power_curve_chart(sample_workout_data, tmp_path):
chart_generator = ChartGenerator(sample_workout_data)
output_file = tmp_path / "power_curve.png"
chart_generator.generate_power_curve_chart(output_file)
assert output_file.exists()
assert output_file.stat().st_size > 0
def test_generate_elevation_profile_chart(sample_workout_data, tmp_path):
chart_generator = ChartGenerator(sample_workout_data)
output_file = tmp_path / "elevation_profile.png"
chart_generator.generate_elevation_profile_chart(output_file)
assert output_file.exists()
assert output_file.stat().st_size > 0
def test_generate_power_zone_distribution_chart(sample_workout_data, tmp_path):
chart_generator = ChartGenerator(sample_workout_data)
output_file = tmp_path / "power_zone_distribution.png"
chart_generator.generate_zone_distribution_chart("power", output_file)
assert output_file.exists()
assert output_file.stat().st_size > 0
def test_generate_hr_zone_distribution_chart(sample_workout_data, tmp_path):
chart_generator = ChartGenerator(sample_workout_data)
output_file = tmp_path / "hr_zone_distribution.png"
chart_generator.generate_zone_distribution_chart("heart_rate", output_file)
assert output_file.exists()
assert output_file.stat().st_size > 0
def test_generate_speed_zone_distribution_chart(sample_workout_data, tmp_path):
chart_generator = ChartGenerator(sample_workout_data)
output_file = tmp_path / "speed_zone_distribution.png"
chart_generator.generate_zone_distribution_chart("speed", output_file)
assert output_file.exists()
assert output_file.stat().st_size > 0