activity-parser 0.9.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. activity_parser-0.10.0/PKG-INFO +178 -0
  2. activity_parser-0.10.0/README.md +155 -0
  3. {activity_parser-0.9.0 → activity_parser-0.10.0}/pyproject.toml +2 -1
  4. {activity_parser-0.9.0 → activity_parser-0.10.0}/src/activity_parser/__init__.py +2 -0
  5. activity_parser-0.10.0/src/activity_parser/output.py +79 -0
  6. activity_parser-0.10.0/src/activity_parser/parse_activity.py +137 -0
  7. activity_parser-0.10.0/src/activity_parser/parse_fit_file.py +234 -0
  8. activity_parser-0.10.0/src/activity_parser/parse_tcx_gpx.py +237 -0
  9. activity_parser-0.10.0/src/activity_parser/postprocess.py +37 -0
  10. activity_parser-0.10.0/src/activity_parser/xml_fields.py +217 -0
  11. activity_parser-0.10.0/tests/files/README.md +16 -0
  12. activity_parser-0.10.0/tests/files/fit/DeveloperData.fit +0 -0
  13. activity_parser-0.10.0/tests/files/fit/garmin-fenix-5-run.fit +0 -0
  14. activity_parser-0.10.0/tests/files/gpx/gpx10.gpx +30 -0
  15. activity_parser-0.10.0/tests/files/gpx/gpxdata.gpx +32 -0
  16. activity_parser-0.10.0/tests/files/gpx/gpxtpx_v1.gpx +38 -0
  17. activity_parser-0.10.0/tests/files/gpx/metadata.gpx +18 -0
  18. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/mixed_offset.gpx +11 -4
  19. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/no_time.gpx +14 -5
  20. activity_parser-0.10.0/tests/files/gpx/route_no_time.gpx +18 -0
  21. activity_parser-0.10.0/tests/files/gpx/unknown_extension.gpx +25 -0
  22. activity_parser-0.10.0/tests/files/tcx/activity_metadata.tcx +19 -0
  23. activity_parser-0.10.0/tests/files/tcx/cadence_collision.tcx +33 -0
  24. activity_parser-0.10.0/tests/files/tcx/duplicate_cadence.tcx +22 -0
  25. activity_parser-0.10.0/tests/files/tcx/laps_only.tcx +14 -0
  26. activity_parser-0.10.0/tests/files/tcx/lx_namespace_reset.tcx +26 -0
  27. activity_parser-0.10.0/tests/files/tcx/mixed_content.tcx +20 -0
  28. activity_parser-0.10.0/tests/files/tcx/running.tcx +62 -0
  29. activity_parser-0.10.0/tests/files/tcx/unknown_collision.tcx +26 -0
  30. activity_parser-0.10.0/tests/files/tcx/vendor_type_attribute.tcx +22 -0
  31. activity_parser-0.10.0/tests/fit_fixtures.py +220 -0
  32. activity_parser-0.10.0/tests/test_parse_activity.py +120 -0
  33. activity_parser-0.10.0/tests/test_parse_fit.py +518 -0
  34. activity_parser-0.10.0/tests/test_parse_tcx_gpx.py +379 -0
  35. activity_parser-0.10.0/tests/test_postprocess.py +50 -0
  36. activity_parser-0.10.0/tests/test_readme.py +44 -0
  37. activity_parser-0.10.0/tests/test_xml_fields.py +78 -0
  38. activity_parser-0.9.0/PKG-INFO +0 -97
  39. activity_parser-0.9.0/README.md +0 -74
  40. activity_parser-0.9.0/src/activity_parser/parse_activity.py +0 -281
  41. activity_parser-0.9.0/src/activity_parser/parse_fit_file.py +0 -105
  42. activity_parser-0.9.0/src/activity_parser/parse_tcx_gpx.py +0 -201
  43. activity_parser-0.9.0/tests/files/README.md +0 -10
  44. activity_parser-0.9.0/tests/test_parse_activity.py +0 -60
  45. activity_parser-0.9.0/tests/test_parse_fit.py +0 -99
  46. activity_parser-0.9.0/tests/test_parse_tcx_gpx.py +0 -212
  47. {activity_parser-0.9.0 → activity_parser-0.10.0}/.github/workflows/ci.yml +0 -0
  48. {activity_parser-0.9.0 → activity_parser-0.10.0}/.github/workflows/release.yml +0 -0
  49. {activity_parser-0.9.0 → activity_parser-0.10.0}/.gitignore +0 -0
  50. {activity_parser-0.9.0 → activity_parser-0.10.0}/.pre-commit-config.yaml +0 -0
  51. {activity_parser-0.9.0 → activity_parser-0.10.0}/LICENSE +0 -0
  52. {activity_parser-0.9.0 → activity_parser-0.10.0}/src/activity_parser/py.typed +0 -0
  53. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/fit}/garmin-edge-820-bike.fit +0 -0
  54. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/fit}/garmin-fenix-5-bike.fit +0 -0
  55. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/bad_speed.gpx +0 -0
  56. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/multi_segment.gpx +0 -0
  57. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/sample.gpx +0 -0
  58. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/mixed_offset.tcx +0 -0
  59. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/no_position.tcx +0 -0
  60. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/no_time.tcx +0 -0
  61. {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/sample.tcx +0 -0
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: activity-parser
3
+ Version: 0.10.0
4
+ Summary: Parser for loading FIT, TCX and GPX files into Pandas DataFrames.
5
+ Project-URL: Repository, https://github.com/tabishm52/activity_parser
6
+ Project-URL: Issues, https://github.com/tabishm52/activity_parser/issues
7
+ Author-email: Tabish Mustufa <tabishm@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: fit,fitness,gps,gpx,pandas,parser,tcx
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering :: GIS
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: fitdecode
20
+ Requires-Dist: lxml
21
+ Requires-Dist: pandas
22
+ Description-Content-Type: text/markdown
23
+
24
+ # activity-parser
25
+
26
+ [![CI](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/activity-parser)](https://pypi.org/project/activity-parser/)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/activity-parser)](https://pypi.org/project/activity-parser/)
29
+ [![License](https://img.shields.io/pypi/l/activity-parser)](https://pypi.org/project/activity-parser/)
30
+
31
+ Parser for loading FIT, TCX and GPX activity files into Pandas DataFrames.
32
+
33
+ Provides a parser object for reading (optionally gzipped) FIT, TCX, and GPX activity files and converting them into Pandas DataFrames.
34
+ During import, field names extracted from activity files are normalized into a canonical set of output column names with consistent units to allow for standardized downstream data processing.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install activity-parser
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Create a new instance of the `ActivityParser` class to be reused for subsequent parsing of activity files:
45
+
46
+ ```python
47
+ import activity_parser
48
+ parser = activity_parser.ActivityParser()
49
+ ```
50
+
51
+ Parse FIT, TCX and GPX files into normalized DataFrames:
52
+
53
+ ```python
54
+ records, laps, activity = parser.parse('path/to/fit_file.fit')
55
+ records, laps, activity = parser.parse('path/to/gpx_file.gpx')
56
+ records, laps, activity = parser.parse('path/to/tcx_file.tcx')
57
+ ```
58
+
59
+ Each file is assumed to contain a single activity.
60
+ Multiple activities/tracks (chained FIT files, multi-activity TCX, multi-track GPX) have their records/laps merged into one set of results.
61
+ In those cases, the returned `Activity` summary reflects only the first activity/session in the file.
62
+
63
+ ## Output data
64
+
65
+ `records` and `laps` are each a DataFrame of normalized data, e.g.:
66
+
67
+ ```
68
+ distance speed cadence heart_rate
69
+ time
70
+ 2026-01-05 08:00:00+00:00 0.00 36.0 80 100
71
+ 2026-01-05 08:00:01+00:00 0.01 36.0 81 101
72
+ ```
73
+
74
+ A few notes on column handling:
75
+
76
+ - Column names and units are standardized across source formats for known field types.
77
+ - Unknown fields are omitted by default; customize columns via `record_columns`, `lap_columns`, and `include_all_columns` on `ActivityParser` — see its docstring for details.
78
+ - Not every column appears in every file: `parse()` only includes columns actually present.
79
+
80
+ ### Records
81
+
82
+ Not all fields are available in all source formats.
83
+ Fields marked with (\*) are exporter-dependent.
84
+
85
+ | Column | Unit | FIT | TCX | GPX |
86
+ |---|---|:-:|:-:|:-:|
87
+ | `latitude`, `longitude` | degrees | Yes | Yes | Yes |
88
+ | `altitude` | meters | Yes | Yes | Yes |
89
+ | `distance` | km | Yes | Yes | (\*) |
90
+ | `speed` | km/h | Yes | (\*) | (\*) |
91
+ | `cadence` | rpm | Yes | Yes | (\*) |
92
+ | `heart_rate` | bpm | Yes | Yes | (\*) |
93
+ | `power` | watts | Yes | (\*) | (\*) |
94
+ | `left_balance`, `right_balance` | percent | Yes | — | — |
95
+ | `accumulated_power` | watts | Yes | — | — |
96
+ | `temperature` | °C | Yes | — | (\*) |
97
+ | `water_temperature` | °C | — | — | (\*) |
98
+ | `depth` | meters | — | — | (\*) |
99
+ | `course`, `bearing` | degrees | — | — | (\*) |
100
+
101
+ ### Laps
102
+
103
+ GPX files have no lap data, so `laps` is always an empty DataFrame for GPX.
104
+ Fields marked with (\*) are exporter-dependent.
105
+
106
+ | Column | Unit | FIT | TCX |
107
+ |---|---|:-:|:-:|
108
+ | `start_time` | timestamp | Yes | Yes |
109
+ | `total_elapsed_time` | seconds | Yes | Yes |
110
+ | `total_timer_time` | seconds | Yes | — |
111
+ | `start_position_lat`, `start_position_long` | degrees | Yes | — |
112
+ | `end_position_lat`, `end_position_long` | degrees | Yes | — |
113
+ | `total_distance` | km | Yes | Yes |
114
+ | `total_ascent`, `total_descent` | meters | Yes | — |
115
+ | `avg_vam` | m/s | Yes | — |
116
+ | `avg_speed` | km/h | Yes | (\*) |
117
+ | `max_speed` | km/h | Yes | Yes |
118
+ | `avg_cadence` | rpm | Yes | Yes |
119
+ | `max_cadence` | rpm | Yes | (\*) |
120
+ | `total_strokes` | count | Yes | — |
121
+ | `steps` | count | — | (\*) |
122
+ | `avg_heart_rate`, `max_heart_rate` | bpm | Yes | Yes |
123
+ | `time_in_hr_zone` | seconds | Yes | — |
124
+ | `avg_power`, `max_power` | watts | Yes | (\*) |
125
+ | `normalized_power` | watts | Yes | — |
126
+ | `left_balance`, `right_balance` | percent | Yes | — |
127
+ | `time_in_power_zone` | seconds | Yes | — |
128
+ | `total_work` | joules | Yes | — |
129
+ | `avg_temperature`, `max_temperature` | °C | Yes | — |
130
+ | `total_calories` | kcal | Yes | Yes |
131
+ | `total_fat_calories` | kcal | Yes | — |
132
+
133
+ ### Activity
134
+
135
+ `activity` is an `Activity` dataclass instance: a small file-level summary.
136
+ Fields are `None` when the source format/file doesn't record them.
137
+ Values are transcribed from the file, never computed/derived from records or laps.
138
+
139
+ | Field | Unit | FIT | TCX | GPX |
140
+ |---|---|:-:|:-:|:-:|
141
+ | `sport` | — | Yes | Yes | — |
142
+ | `start_time` | — | Yes | Yes | Yes (\*) |
143
+ | `total_elapsed_time` | seconds | Yes | — | — |
144
+ | `total_distance` | km | Yes | — | — |
145
+ | `total_calories` | kcal | Yes | — | — |
146
+ | `avg_heart_rate`, `max_heart_rate` | bpm | Yes | — | — |
147
+ | `avg_speed`, `max_speed` | kph | Yes | — | — |
148
+ | `creator` | — | Yes | Yes | Yes |
149
+ | `notes` | — | — | Yes | Yes |
150
+
151
+ (\*) GPX's `start_time` comes from `metadata/time`, which is technically the file's export time rather than the activity's start.
152
+
153
+ ## Parser notes
154
+
155
+ ### FIT files
156
+
157
+ FIT parsing wraps [`fitdecode`](https://github.com/polyvertex/fitdecode), which decodes against Garmin's public FIT SDK profile.
158
+ Field values and types come from that profile, and units are converted via fitdecode's `StandardUnitsDataProcessor`.
159
+
160
+ This package transforms certain FIT fields beyond the processing done by fitdecode:
161
+
162
+ - Some fields have lower- and higher-precision versions; by default, `parse()` returns only the higher-precision value, under the base field's name.
163
+ - Other fields store sub-integer precision in a separate field, which `parse()` adds into the base field.
164
+ - FIT's pedal power balance is a single bit-packed byte (a side flag plus a percentage); `parse()` decodes it into `left_balance`/`right_balance`.
165
+
166
+ Messages/fields fitdecode can't resolve against the profile (e.g. proprietary extensions) are kept as raw values under `unknown_<n>` names.
167
+
168
+ ### TCX & GPX files
169
+
170
+ TCX and GPX files are parsed natively in this package.
171
+ The parser supports the following schema versions and extensions:
172
+
173
+ | Format | Schema version | Extensions |
174
+ |---|---|---|
175
+ | TCX | v2 | Garmin `ActivityExtension` v2 |
176
+ | GPX | 1.0 / 1.1 | Garmin `TrackPointExtension` v1/v2, Cluetrust `gpxdata` |
177
+
178
+ Unrecognized elements and attributes are exported as string columns named with their XML namespace in Clark notation.
@@ -0,0 +1,155 @@
1
+ # activity-parser
2
+
3
+ [![CI](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/activity-parser)](https://pypi.org/project/activity-parser/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/activity-parser)](https://pypi.org/project/activity-parser/)
6
+ [![License](https://img.shields.io/pypi/l/activity-parser)](https://pypi.org/project/activity-parser/)
7
+
8
+ Parser for loading FIT, TCX and GPX activity files into Pandas DataFrames.
9
+
10
+ Provides a parser object for reading (optionally gzipped) FIT, TCX, and GPX activity files and converting them into Pandas DataFrames.
11
+ During import, field names extracted from activity files are normalized into a canonical set of output column names with consistent units to allow for standardized downstream data processing.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install activity-parser
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Create a new instance of the `ActivityParser` class to be reused for subsequent parsing of activity files:
22
+
23
+ ```python
24
+ import activity_parser
25
+ parser = activity_parser.ActivityParser()
26
+ ```
27
+
28
+ Parse FIT, TCX and GPX files into normalized DataFrames:
29
+
30
+ ```python
31
+ records, laps, activity = parser.parse('path/to/fit_file.fit')
32
+ records, laps, activity = parser.parse('path/to/gpx_file.gpx')
33
+ records, laps, activity = parser.parse('path/to/tcx_file.tcx')
34
+ ```
35
+
36
+ Each file is assumed to contain a single activity.
37
+ Multiple activities/tracks (chained FIT files, multi-activity TCX, multi-track GPX) have their records/laps merged into one set of results.
38
+ In those cases, the returned `Activity` summary reflects only the first activity/session in the file.
39
+
40
+ ## Output data
41
+
42
+ `records` and `laps` are each a DataFrame of normalized data, e.g.:
43
+
44
+ ```
45
+ distance speed cadence heart_rate
46
+ time
47
+ 2026-01-05 08:00:00+00:00 0.00 36.0 80 100
48
+ 2026-01-05 08:00:01+00:00 0.01 36.0 81 101
49
+ ```
50
+
51
+ A few notes on column handling:
52
+
53
+ - Column names and units are standardized across source formats for known field types.
54
+ - Unknown fields are omitted by default; customize columns via `record_columns`, `lap_columns`, and `include_all_columns` on `ActivityParser` — see its docstring for details.
55
+ - Not every column appears in every file: `parse()` only includes columns actually present.
56
+
57
+ ### Records
58
+
59
+ Not all fields are available in all source formats.
60
+ Fields marked with (\*) are exporter-dependent.
61
+
62
+ | Column | Unit | FIT | TCX | GPX |
63
+ |---|---|:-:|:-:|:-:|
64
+ | `latitude`, `longitude` | degrees | Yes | Yes | Yes |
65
+ | `altitude` | meters | Yes | Yes | Yes |
66
+ | `distance` | km | Yes | Yes | (\*) |
67
+ | `speed` | km/h | Yes | (\*) | (\*) |
68
+ | `cadence` | rpm | Yes | Yes | (\*) |
69
+ | `heart_rate` | bpm | Yes | Yes | (\*) |
70
+ | `power` | watts | Yes | (\*) | (\*) |
71
+ | `left_balance`, `right_balance` | percent | Yes | — | — |
72
+ | `accumulated_power` | watts | Yes | — | — |
73
+ | `temperature` | °C | Yes | — | (\*) |
74
+ | `water_temperature` | °C | — | — | (\*) |
75
+ | `depth` | meters | — | — | (\*) |
76
+ | `course`, `bearing` | degrees | — | — | (\*) |
77
+
78
+ ### Laps
79
+
80
+ GPX files have no lap data, so `laps` is always an empty DataFrame for GPX.
81
+ Fields marked with (\*) are exporter-dependent.
82
+
83
+ | Column | Unit | FIT | TCX |
84
+ |---|---|:-:|:-:|
85
+ | `start_time` | timestamp | Yes | Yes |
86
+ | `total_elapsed_time` | seconds | Yes | Yes |
87
+ | `total_timer_time` | seconds | Yes | — |
88
+ | `start_position_lat`, `start_position_long` | degrees | Yes | — |
89
+ | `end_position_lat`, `end_position_long` | degrees | Yes | — |
90
+ | `total_distance` | km | Yes | Yes |
91
+ | `total_ascent`, `total_descent` | meters | Yes | — |
92
+ | `avg_vam` | m/s | Yes | — |
93
+ | `avg_speed` | km/h | Yes | (\*) |
94
+ | `max_speed` | km/h | Yes | Yes |
95
+ | `avg_cadence` | rpm | Yes | Yes |
96
+ | `max_cadence` | rpm | Yes | (\*) |
97
+ | `total_strokes` | count | Yes | — |
98
+ | `steps` | count | — | (\*) |
99
+ | `avg_heart_rate`, `max_heart_rate` | bpm | Yes | Yes |
100
+ | `time_in_hr_zone` | seconds | Yes | — |
101
+ | `avg_power`, `max_power` | watts | Yes | (\*) |
102
+ | `normalized_power` | watts | Yes | — |
103
+ | `left_balance`, `right_balance` | percent | Yes | — |
104
+ | `time_in_power_zone` | seconds | Yes | — |
105
+ | `total_work` | joules | Yes | — |
106
+ | `avg_temperature`, `max_temperature` | °C | Yes | — |
107
+ | `total_calories` | kcal | Yes | Yes |
108
+ | `total_fat_calories` | kcal | Yes | — |
109
+
110
+ ### Activity
111
+
112
+ `activity` is an `Activity` dataclass instance: a small file-level summary.
113
+ Fields are `None` when the source format/file doesn't record them.
114
+ Values are transcribed from the file, never computed/derived from records or laps.
115
+
116
+ | Field | Unit | FIT | TCX | GPX |
117
+ |---|---|:-:|:-:|:-:|
118
+ | `sport` | — | Yes | Yes | — |
119
+ | `start_time` | — | Yes | Yes | Yes (\*) |
120
+ | `total_elapsed_time` | seconds | Yes | — | — |
121
+ | `total_distance` | km | Yes | — | — |
122
+ | `total_calories` | kcal | Yes | — | — |
123
+ | `avg_heart_rate`, `max_heart_rate` | bpm | Yes | — | — |
124
+ | `avg_speed`, `max_speed` | kph | Yes | — | — |
125
+ | `creator` | — | Yes | Yes | Yes |
126
+ | `notes` | — | — | Yes | Yes |
127
+
128
+ (\*) GPX's `start_time` comes from `metadata/time`, which is technically the file's export time rather than the activity's start.
129
+
130
+ ## Parser notes
131
+
132
+ ### FIT files
133
+
134
+ FIT parsing wraps [`fitdecode`](https://github.com/polyvertex/fitdecode), which decodes against Garmin's public FIT SDK profile.
135
+ Field values and types come from that profile, and units are converted via fitdecode's `StandardUnitsDataProcessor`.
136
+
137
+ This package transforms certain FIT fields beyond the processing done by fitdecode:
138
+
139
+ - Some fields have lower- and higher-precision versions; by default, `parse()` returns only the higher-precision value, under the base field's name.
140
+ - Other fields store sub-integer precision in a separate field, which `parse()` adds into the base field.
141
+ - FIT's pedal power balance is a single bit-packed byte (a side flag plus a percentage); `parse()` decodes it into `left_balance`/`right_balance`.
142
+
143
+ Messages/fields fitdecode can't resolve against the profile (e.g. proprietary extensions) are kept as raw values under `unknown_<n>` names.
144
+
145
+ ### TCX & GPX files
146
+
147
+ TCX and GPX files are parsed natively in this package.
148
+ The parser supports the following schema versions and extensions:
149
+
150
+ | Format | Schema version | Extensions |
151
+ |---|---|---|
152
+ | TCX | v2 | Garmin `ActivityExtension` v2 |
153
+ | GPX | 1.0 / 1.1 | Garmin `TrackPointExtension` v1/v2, Cluetrust `gpxdata` |
154
+
155
+ Unrecognized elements and attributes are exported as string columns named with their XML namespace in Clark notation.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "activity-parser"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  description = "Parser for loading FIT, TCX and GPX files into Pandas DataFrames."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -35,6 +35,7 @@ Issues = "https://github.com/tabishm52/activity_parser/issues"
35
35
 
36
36
  [dependency-groups]
37
37
  dev = [
38
+ "garmin-fit-sdk",
38
39
  "lxml-stubs",
39
40
  "pandas-stubs",
40
41
  "pre-commit",
@@ -1,10 +1,12 @@
1
1
  """Parser for loading FIT, TCX and GPX files into Pandas DataFrames."""
2
2
 
3
+ from .output import Activity
3
4
  from .parse_activity import ActivityParser
4
5
  from .parse_fit_file import parse_fit
5
6
  from .parse_tcx_gpx import parse_gpx, parse_tcx
6
7
 
7
8
  __all__ = [
9
+ "Activity",
8
10
  "ActivityParser",
9
11
  "parse_fit",
10
12
  "parse_gpx",
@@ -0,0 +1,79 @@
1
+ """Defines the shape of ``parse()``'s return values, shared across FIT/TCX/GPX."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pandas as pd
6
+
7
+ DEFAULT_RECORD_COLUMNS: tuple[str, ...] = (
8
+ "latitude",
9
+ "longitude",
10
+ "altitude",
11
+ "distance",
12
+ "speed",
13
+ "cadence",
14
+ "heart_rate",
15
+ "power",
16
+ "left_balance",
17
+ "right_balance",
18
+ "accumulated_power",
19
+ "temperature",
20
+ "water_temperature",
21
+ "depth",
22
+ "course",
23
+ "bearing",
24
+ )
25
+
26
+ DEFAULT_LAP_COLUMNS: tuple[str, ...] = (
27
+ "start_time",
28
+ "total_elapsed_time",
29
+ "total_timer_time",
30
+ "start_position_lat",
31
+ "start_position_long",
32
+ "end_position_lat",
33
+ "end_position_long",
34
+ "total_distance",
35
+ "total_ascent",
36
+ "total_descent",
37
+ "avg_vam",
38
+ "avg_speed",
39
+ "max_speed",
40
+ "avg_cadence",
41
+ "max_cadence",
42
+ "total_strokes",
43
+ "steps",
44
+ "avg_heart_rate",
45
+ "max_heart_rate",
46
+ "time_in_hr_zone",
47
+ "avg_power",
48
+ "max_power",
49
+ "normalized_power",
50
+ "left_balance",
51
+ "right_balance",
52
+ "time_in_power_zone",
53
+ "total_work",
54
+ "avg_temperature",
55
+ "max_temperature",
56
+ "total_calories",
57
+ "total_fat_calories",
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Activity:
63
+ """File-level summary, present across FIT/TCX/GPX.
64
+
65
+ Fields are ``None`` when the source format/file doesn't record them. Values are
66
+ transcribed from the file, never computed/derived from records or laps.
67
+ """
68
+
69
+ sport: str | None = None
70
+ start_time: pd.Timestamp | None = None
71
+ total_elapsed_time: float | None = None # seconds
72
+ total_distance: float | None = None # km
73
+ total_calories: float | None = None
74
+ avg_heart_rate: float | None = None
75
+ max_heart_rate: float | None = None
76
+ avg_speed: float | None = None # kph
77
+ max_speed: float | None = None # kph
78
+ creator: str | None = None # device/app that recorded or wrote the file
79
+ notes: str | None = None
@@ -0,0 +1,137 @@
1
+ """Class for parsing FIT, TCX and GPX files into Pandas DataFrames."""
2
+
3
+ from collections.abc import Sequence
4
+ from os import PathLike
5
+ from pathlib import Path
6
+ from typing import IO
7
+
8
+ import pandas as pd
9
+
10
+ from .output import DEFAULT_LAP_COLUMNS, DEFAULT_RECORD_COLUMNS, Activity
11
+ from .parse_fit_file import parse_fit
12
+ from .parse_tcx_gpx import parse_gpx, parse_tcx
13
+
14
+
15
+ def select_and_reorder_cols(
16
+ df: pd.DataFrame,
17
+ columns: Sequence[str],
18
+ include_all_columns: bool,
19
+ ) -> pd.DataFrame:
20
+ """Selects and reorders df's columns.
21
+
22
+ Columns from ``columns`` present in ``df`` are selected, in that order; columns
23
+ absent from ``df`` are omitted.
24
+
25
+ If ``include_all_columns`` is True, any of ``df``'s remaining columns not in
26
+ ``columns`` are appended afterward, in their original order.
27
+ """
28
+ present = [col for col in columns if col in df.columns]
29
+ if include_all_columns:
30
+ selected = set(columns)
31
+ extra = [col for col in df.columns if col not in selected]
32
+ ordered = present + extra
33
+ else:
34
+ ordered = present
35
+ return df.loc[:, ordered]
36
+
37
+
38
+ def normalize_extension(ext: str) -> str:
39
+ """Normalize an extension string to one of: fit, tcx, gpx."""
40
+ normalized = ext.lower().lstrip(".")
41
+ if normalized == "gz":
42
+ raise ValueError("Ambiguous extension: .gz without base extension.")
43
+ return normalized
44
+
45
+
46
+ def infer_extension(file: str | PathLike[str]) -> str:
47
+ """Infer normalized extension from a path-like input."""
48
+ p = Path(file)
49
+ ext = p.suffix
50
+ if ext.lower() == ".gz":
51
+ ext = Path(p.stem).suffix
52
+ return normalize_extension(ext)
53
+
54
+
55
+ class ActivityParser:
56
+ """Parser for FIT, GPX and TCX files.
57
+
58
+ Each instance of this class is a parser object that can be used to import FIT, GPX
59
+ and TCX files into DataFrames. During parsing, the column names in the resulting
60
+ DataFrames are normalized to a standard set of names to allow for more
61
+ interchangeable use of DataFrames from the different activity file types.
62
+
63
+ The output of ``parse`` is controlled by ``record_columns`` and ``lap_columns``,
64
+ which are set on each instance from ``default_columns``. Reassign or mutate them to
65
+ select different columns.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ *,
71
+ include_all_columns: bool = False,
72
+ check_crc: bool = False,
73
+ strict_xml: bool = False,
74
+ ) -> None:
75
+ """Initialize parser settings and column lists.
76
+
77
+ Args:
78
+ include_all_columns: If True, ``parse`` returns all parsed columns. If
79
+ False, ``parse`` returns only the columns in ``record_columns`` and
80
+ ``lap_columns``.
81
+ check_crc: If True, ``parse`` raises ``fitdecode.FitCRCError`` on a CRC
82
+ mismatch in FIT files. If False, CRC verification is skipped.
83
+ strict_xml: If True, ``parse`` raises ``lxml.etree.XMLSyntaxError`` on
84
+ malformed TCX/GPX XML. If False, parser recovery is enabled.
85
+ """
86
+ self.record_columns: list[str] = list(DEFAULT_RECORD_COLUMNS)
87
+ self.lap_columns: list[str] = list(DEFAULT_LAP_COLUMNS)
88
+ self.include_all_columns = include_all_columns
89
+ self.check_crc = check_crc
90
+ self.strict_xml = strict_xml
91
+
92
+ def parse(
93
+ self,
94
+ file: str | PathLike[str] | IO[bytes],
95
+ ext: str | None = None,
96
+ ) -> tuple[pd.DataFrame, pd.DataFrame, Activity]:
97
+ """Loads a FIT, TCX or GPX activity into Pandas DataFrames.
98
+
99
+ Output columns come from ``record_columns``/``lap_columns``, plus any extra
100
+ columns if ``include_all_columns`` is set. Not all columns are guaranteed to
101
+ appear, only those present in the activity file.
102
+
103
+ Args:
104
+ file: Binary file-like or path-like object. A path-like argument ending in
105
+ ``.gz`` will be unzipped before processing.
106
+ ext: File type, case-insensitive: ``fit``, ``tcx``, or ``gpx``. Required for
107
+ a file-like ``file``; optional for a path-like ``file`` (inferred from
108
+ the name).
109
+
110
+ Returns:
111
+ Tuple containing records, laps, and an ``Activity`` summary.
112
+ """
113
+ if ext is not None:
114
+ ext_normalized = normalize_extension(ext)
115
+ elif isinstance(file, (str, PathLike)):
116
+ ext_normalized = infer_extension(file)
117
+ else:
118
+ raise ValueError("ext must be provided when file is a file-like object.")
119
+
120
+ if ext_normalized == "fit":
121
+ records, laps, activity = parse_fit(file, check_crc=self.check_crc)
122
+
123
+ elif ext_normalized == "tcx":
124
+ records, laps, activity = parse_tcx(file, strict_xml=self.strict_xml)
125
+
126
+ elif ext_normalized == "gpx":
127
+ # Note GPX files have no lap information; laps is always empty.
128
+ records, laps, activity = parse_gpx(file, strict_xml=self.strict_xml)
129
+
130
+ else:
131
+ raise ValueError(f"File type not supported: {ext_normalized}")
132
+
133
+ records = select_and_reorder_cols(records, self.record_columns, self.include_all_columns)
134
+ records.rename_axis("time", inplace=True)
135
+ laps = select_and_reorder_cols(laps, self.lap_columns, self.include_all_columns)
136
+
137
+ return records, laps, activity