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.
- activity_parser-0.10.0/PKG-INFO +178 -0
- activity_parser-0.10.0/README.md +155 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/pyproject.toml +2 -1
- {activity_parser-0.9.0 → activity_parser-0.10.0}/src/activity_parser/__init__.py +2 -0
- activity_parser-0.10.0/src/activity_parser/output.py +79 -0
- activity_parser-0.10.0/src/activity_parser/parse_activity.py +137 -0
- activity_parser-0.10.0/src/activity_parser/parse_fit_file.py +234 -0
- activity_parser-0.10.0/src/activity_parser/parse_tcx_gpx.py +237 -0
- activity_parser-0.10.0/src/activity_parser/postprocess.py +37 -0
- activity_parser-0.10.0/src/activity_parser/xml_fields.py +217 -0
- activity_parser-0.10.0/tests/files/README.md +16 -0
- activity_parser-0.10.0/tests/files/fit/DeveloperData.fit +0 -0
- activity_parser-0.10.0/tests/files/fit/garmin-fenix-5-run.fit +0 -0
- activity_parser-0.10.0/tests/files/gpx/gpx10.gpx +30 -0
- activity_parser-0.10.0/tests/files/gpx/gpxdata.gpx +32 -0
- activity_parser-0.10.0/tests/files/gpx/gpxtpx_v1.gpx +38 -0
- activity_parser-0.10.0/tests/files/gpx/metadata.gpx +18 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/mixed_offset.gpx +11 -4
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/no_time.gpx +14 -5
- activity_parser-0.10.0/tests/files/gpx/route_no_time.gpx +18 -0
- activity_parser-0.10.0/tests/files/gpx/unknown_extension.gpx +25 -0
- activity_parser-0.10.0/tests/files/tcx/activity_metadata.tcx +19 -0
- activity_parser-0.10.0/tests/files/tcx/cadence_collision.tcx +33 -0
- activity_parser-0.10.0/tests/files/tcx/duplicate_cadence.tcx +22 -0
- activity_parser-0.10.0/tests/files/tcx/laps_only.tcx +14 -0
- activity_parser-0.10.0/tests/files/tcx/lx_namespace_reset.tcx +26 -0
- activity_parser-0.10.0/tests/files/tcx/mixed_content.tcx +20 -0
- activity_parser-0.10.0/tests/files/tcx/running.tcx +62 -0
- activity_parser-0.10.0/tests/files/tcx/unknown_collision.tcx +26 -0
- activity_parser-0.10.0/tests/files/tcx/vendor_type_attribute.tcx +22 -0
- activity_parser-0.10.0/tests/fit_fixtures.py +220 -0
- activity_parser-0.10.0/tests/test_parse_activity.py +120 -0
- activity_parser-0.10.0/tests/test_parse_fit.py +518 -0
- activity_parser-0.10.0/tests/test_parse_tcx_gpx.py +379 -0
- activity_parser-0.10.0/tests/test_postprocess.py +50 -0
- activity_parser-0.10.0/tests/test_readme.py +44 -0
- activity_parser-0.10.0/tests/test_xml_fields.py +78 -0
- activity_parser-0.9.0/PKG-INFO +0 -97
- activity_parser-0.9.0/README.md +0 -74
- activity_parser-0.9.0/src/activity_parser/parse_activity.py +0 -281
- activity_parser-0.9.0/src/activity_parser/parse_fit_file.py +0 -105
- activity_parser-0.9.0/src/activity_parser/parse_tcx_gpx.py +0 -201
- activity_parser-0.9.0/tests/files/README.md +0 -10
- activity_parser-0.9.0/tests/test_parse_activity.py +0 -60
- activity_parser-0.9.0/tests/test_parse_fit.py +0 -99
- activity_parser-0.9.0/tests/test_parse_tcx_gpx.py +0 -212
- {activity_parser-0.9.0 → activity_parser-0.10.0}/.github/workflows/ci.yml +0 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/.github/workflows/release.yml +0 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/.gitignore +0 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/.pre-commit-config.yaml +0 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/LICENSE +0 -0
- {activity_parser-0.9.0 → activity_parser-0.10.0}/src/activity_parser/py.typed +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/fit}/garmin-edge-820-bike.fit +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/fit}/garmin-fenix-5-bike.fit +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/bad_speed.gpx +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/multi_segment.gpx +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/gpx}/sample.gpx +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/mixed_offset.tcx +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/no_position.tcx +0 -0
- {activity_parser-0.9.0/tests/files → activity_parser-0.10.0/tests/files/tcx}/no_time.tcx +0 -0
- {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
|
+
[](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml)
|
|
27
|
+
[](https://pypi.org/project/activity-parser/)
|
|
28
|
+
[](https://pypi.org/project/activity-parser/)
|
|
29
|
+
[](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
|
+
[](https://github.com/tabishm52/activity_parser/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/activity-parser/)
|
|
5
|
+
[](https://pypi.org/project/activity-parser/)
|
|
6
|
+
[](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.
|
|
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
|