Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/activity_parser/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ def fill_activity(activity: Activity, records: pd.DataFrame, laps: pd.DataFrame)
A field the file itself reports is never overwritten; this only fills fields the
file left ``None``. ``total_timer_time``, ``total_ascent`` and ``total_descent`` are
not derived and are left as the file reported them, ``None`` included.

Should not be called for a file holding more than one activity, since the filled
fields would aggregate over all activities.
"""
# Prefer sum of lap data, fall back to calculating from records
total_elapsed_time = _coalesce(
Expand Down
3 changes: 2 additions & 1 deletion src/activity_parser/parse_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ def parse_fit(
)

activity = build_activity(convert_units_mapping(session or {}, SESSION_UNITS), file_id)
activity = fill_activity(activity, records, laps)
if len(messages.get("session", [])) <= 1:
activity = fill_activity(activity, records, laps)

return records, laps, activity

Expand Down
8 changes: 6 additions & 2 deletions src/activity_parser/parse_tcx_gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ def parse_tcx(
etree.strip_elements(root, "{*}Track")
laps = build_dataframe(root.iter("{*}Lap"), TCX_LAP_FIELDS)

activity = fill_activity(tcx_activity(root), records, laps)
activity = tcx_activity(root)
if len(root.findall(".//{*}Activity")) <= 1:
activity = fill_activity(activity, records, laps)

return records, laps, activity

Expand Down Expand Up @@ -261,6 +263,8 @@ def parse_gpx(
records = build_dataframe(root.iter("{*}trkpt"), GPX_TRACKPOINT_FIELDS)
records = index_by_time(records, "time")

activity = fill_activity(gpx_activity(root), records, pd.DataFrame())
activity = gpx_activity(root)
if len(root.findall("{*}trk")) <= 1:
activity = fill_activity(activity, records, pd.DataFrame())

return records, pd.DataFrame(), activity
3 changes: 3 additions & 0 deletions tests/test_parse_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ def test_parse_fit_multi_session_uses_first_session_and_file_id():
assert activity.sport == "running"
assert activity.total_elapsed_time == 60.0
assert activity.creator == "garmin 1111"
# Nothing from the second session leaks in.
assert activity.total_distance is None
assert activity.avg_heart_rate is None


def test_parse_fit_chained_files(tmp_path):
Expand Down
10 changes: 9 additions & 1 deletion tests/test_parse_tcx_gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ def test_tcx_multi_activity_merges_records_and_laps_first_activity_wins_summary(
assert laps["total_distance"].tolist() == pytest.approx([0.5, 1.0])
assert activity.sport == "cycling"
assert activity.start_time == pd.Timestamp("2026-01-05T08:00:00Z")
# Computed fields must not mix the two Activities' merged records/laps together.
assert activity.total_elapsed_time is None
assert activity.total_distance is None
assert activity.avg_heart_rate is None
assert activity.max_heart_rate is None
assert activity.avg_speed is None


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -401,8 +407,10 @@ def test_gpx_1_0_base_fields():

def test_gpx_multiple_tracks_merged():
# Two separate <trk> elements, unlike multi_segment.gpx's two <trkseg> in one.
records, _, _ = ActivityParser().parse(MULTI_TRACK_GPX)
records, _, activity = ActivityParser().parse(MULTI_TRACK_GPX)
assert records["latitude"].tolist() == pytest.approx([37.0, 37.5])
# Not derived from the merged records' time span.
assert activity.total_elapsed_time is None


# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/test_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ def test_readme_laps_table_matches_default_columns():

def test_readme_activity_table_matches_dataclass_fields():
text = README.read_text()
section = _section(text, "### Activity", "## Parser notes")
section = _section(text, "### Activity", "## Examples")
assert _table_column_names(section) == [f.name for f in fields(Activity)]