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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ FIT also exposes FIT-specific fields not available from TCX/GPX (e.g. `fractiona
FIT parsing wraps [`fitdecode`](https://github.com/polyvertex/fitdecode), which decodes against Garmin's public FIT SDK profile — field names, types, and units come from that profile, not this package.

- Newer devices sometimes write only `enhanced_altitude`/`enhanced_speed` (and lap `enhanced_avg_speed`/`enhanced_max_speed`); `parse()` coalesces these into `altitude`/`speed` transparently.
- Messages/fields fitdecode can't resolve against the profile (e.g. proprietary extensions) are dropped.
- Messages/fields fitdecode can't resolve against the profile (e.g. proprietary extensions) are kept as raw values under `unknown_<n>` names; `parse()`'s canonical columns still exclude them.

## TCX & GPX files

Expand Down
12 changes: 6 additions & 6 deletions src/activity_parser/parse_fit_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def copy_fit_frames(
"""Yields FIT data frames from a file-like object."""
processor = fitdecode.StandardUnitsDataProcessor()
for frame in fitdecode.FitReader(fit_file, processor=processor):
if isinstance(frame, fitdecode.FitDataMessage) and frame.mesg_type is not None:
if isinstance(frame, fitdecode.FitDataMessage):
yield frame


def frame_to_dict(frame: fitdecode.FitDataMessage) -> dict[str, Any]:
"""Convert one FIT frame to a dict, dropping unknown fields."""
return {field.name: field.value for field in frame.fields if field.field is not None}
"""Convert one FIT frame to a dict."""
return {field.name: field.value for field in frame.fields}


def parse_fit_frames(
Expand Down Expand Up @@ -81,9 +81,9 @@ def parse_fit(
) -> tuple[pd.DataFrame, pd.DataFrame, dict[str, Any]]:
"""Loads a FIT activity into Pandas DataFrames.

FIT frames and data fields that are marked as 'unknown' by fitdecode are dropped
during import. Assumes that the FIT file is all one activity, i.e. chained FIT files
will be merged into one set of return values.
Message types and fields fitdecode can't resolve against the FIT SDK profile are
kept under fitdecode's ``unknown_<num>`` names. Assumes that the FIT file is all one
activity, i.e. chained FIT files will be merged into one set of return values.

Args:
file: File-like or path-like object. A path-like argument ending in ``.gz`` will
Expand Down
17 changes: 17 additions & 0 deletions tests/test_parse_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ def test_parse_fit_records_index(path):
assert "file_id" in extra


def test_parse_fit_keeps_unknown_record_fields():
records, _, _ = parse_fit(EDGE_820)
assert "unknown_61" in records.columns
assert "unknown_66" in records.columns


def test_parse_fit_keeps_unknown_messages_in_extra():
_, _, extra = parse_fit(EDGE_820)
assert "unknown_22" in extra


def test_activity_parser_drops_unknown_fields_from_canonical_output():
records, laps, _ = ActivityParser().parse(EDGE_820)
assert not any(col.startswith("unknown_") for col in records.columns)
assert not any(col.startswith("unknown_") for col in laps.columns)


@pytest.mark.parametrize("path", [EDGE_820, FENIX_5], ids=lambda p: p.stem)
def test_parse_fit_numeric_dtypes(path):
records, _, _ = parse_fit(path)
Expand Down