diff --git a/README.md b/README.md index 81ff078..9d101bd 100644 --- a/README.md +++ b/README.md @@ -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_` names; `parse()`'s canonical columns still exclude them. ## TCX & GPX files diff --git a/src/activity_parser/parse_fit_file.py b/src/activity_parser/parse_fit_file.py index 0db8aa8..c7c06bb 100644 --- a/src/activity_parser/parse_fit_file.py +++ b/src/activity_parser/parse_fit_file.py @@ -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( @@ -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_`` 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 diff --git a/tests/test_parse_fit.py b/tests/test_parse_fit.py index e1bf1d4..d3e5913 100644 --- a/tests/test_parse_fit.py +++ b/tests/test_parse_fit.py @@ -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)