From c86ae659bd34072b5cd160d8edc09194f88d8e3c Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:02:13 +0100 Subject: [PATCH 1/8] Add pyproject based on current info from pypi --- pyproject.toml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e4319ba --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[tool.poetry] +name = "fit2gpx" +version = "0.0.7" +description = "Package to convert .FIT files to .GPX files, including tools for .FIT files downloaded from Strava" +authors = ["Dorian Sabathier "] +license = "AGPL-3.0-only" +readme = "README.md" +homepage = "https://github.com/dodo-saba/fit2gpx" +repository = "https://github.com/dodo-saba/fit2gpx" +keywords = ["convert", ".fit", "fit", ".gpx", "gpx", "strava"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Other Audience", + "Topic :: Scientific/Engineering :: GIS", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10" +] +packages = [ + { include = "fit2gpx.py", from = "src" } +] + +[tool.poetry.dependencies] +python = "^3.6" +pandas = "^1.5.3" +fitdecode = "^0.10.0" +gpxpy = "^1.5.0" + +[tool.poetry.scripts] +fit2gpx = "fit2gpx:cli" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From ea5fc9a8f61a71babe25875d343fcff4f16834e6 Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:02:33 +0100 Subject: [PATCH 2/8] Add gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac8d7b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +dist From bbbe7aa0ab9f41ea9c92594d50b21e8e6449b765 Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:02:51 +0100 Subject: [PATCH 3/8] Sort imports --- src/fit2gpx.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/fit2gpx.py b/src/fit2gpx.py index cf931db..e0d0ccc 100644 --- a/src/fit2gpx.py +++ b/src/fit2gpx.py @@ -1,13 +1,14 @@ """Classes to convert FIT files to GPX, including tools to process Strava Bulk Export """ -import os import gzip +import os import shutil from datetime import datetime, timedelta -from typing import Dict, Union, Optional, Tuple -import pandas as pd -import gpxpy.gpx +from typing import Dict, Optional, Tuple, Union + import fitdecode +import gpxpy.gpx +import pandas as pd # MAIN CONVERTER CLASS From 11c1522998bfdbb0bd6a2f03ec7ea3f0715f3f7e Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:03:10 +0100 Subject: [PATCH 4/8] Support file descriptors in fit_to_gpx --- src/fit2gpx.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/fit2gpx.py b/src/fit2gpx.py index e0d0ccc..0bab774 100644 --- a/src/fit2gpx.py +++ b/src/fit2gpx.py @@ -195,14 +195,16 @@ def fit_to_gpx(self, f_in, f_out): f_in (str): file path to FIT activity f_out (str): file path to save the converted FIT file """ - # Step 0: Validate inputs - input_extension = os.path.splitext(f_in)[1] - if input_extension != '.fit': - raise Exception("Input file must be a .FIT file.") + if isinstance(f_in, str) or hasattr(f_in, '__fspath__'): + # Step 0: Validate inputs + input_extension = os.path.splitext(f_in)[1] + if input_extension != '.fit': + raise Exception("Input file must be a .FIT file.") - output_extension = os.path.splitext(f_out)[1] - if output_extension != ".gpx": - raise TypeError(f"Output file must be a .gpx file.") + if isinstance(f_out, str) or hasattr(f_out, '__fspath__'): + output_extension = os.path.splitext(f_out)[1] + if output_extension != ".gpx": + raise TypeError(f"Output file must be a .gpx file.") # Step 1: Convert FIT to pd.DataFrame df_laps, df_points = self.fit_to_dataframes(f_in) @@ -223,8 +225,12 @@ def fit_to_gpx(self, f_in, f_out): ) # Step 3: Save file - with open(f_out, 'w') as f: - f.write(gpx.to_xml()) + xml = gpx.to_xml() + if hasattr(f_out, 'write'): + f_out.write(xml) + else: + with open(f_out, 'w') as f: + f.write(xml) return gpx From 0ff37e2471c299cafce2f99e6865ec2e11ad135b Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:03:29 +0100 Subject: [PATCH 5/8] Add cli --- src/fit2gpx.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/fit2gpx.py b/src/fit2gpx.py index 0bab774..fbe1fe1 100644 --- a/src/fit2gpx.py +++ b/src/fit2gpx.py @@ -1,5 +1,6 @@ """Classes to convert FIT files to GPX, including tools to process Strava Bulk Export """ +import argparse import gzip import os import shutil @@ -444,3 +445,26 @@ def add_metadata_to_gpx(self): # Step 2.4: Print if self.status_msg: print(f'{len(gpx_files)} .gpx files have had Strava metadata added.') + + +def cli(): + parser = argparse.ArgumentParser( + prog='fit2gpx', + description="Convert a .FIT file to .GPX." + ) + parser.add_argument( + 'infile', + type=argparse.FileType('rb'), + help='path to the input .FIT file; ' + "use '-' to read the file from standard input" + ) + parser.add_argument( + 'outfile', + type=argparse.FileType('wb'), + help='path to the output .GPX file; ' + "use '-' to write the file to standard output" + ) + args = parser.parse_args() + + conv = Converter() + conv.fit_to_gpx(f_in=args.infile, f_out=args.outfile) From 3400bc1de42a8202558562435bab6ba529251dcc Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:04:02 +0100 Subject: [PATCH 6/8] Clean up whitespace in README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6041e0b..909db7a 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ I decided to create this package after spending a few hours searching for a simp #### Relevance to Strava - Pre-GPDR, you could bulk export all your Strava activities as GPX files. -- Post-GDPR, you can export an archive of your account. Whilst this includes much more data, activity GPS files are now downloaded in their original file format (eg. GPX or FIT format, some gzipped, some not) and named like 2500155647.gpx, 2500155647.gpx.gz, 2500155647.fit, and 2500155647.fit.gz. +- Post-GDPR, you can export an archive of your account. Whilst this includes much more data, activity GPS files are now downloaded in their original file format (eg. GPX or FIT format, some gzipped, some not) and named like 2500155647.gpx, 2500155647.gpx.gz, 2500155647.fit, and 2500155647.fit.gz. - [How to bulk export you Strava Data](https://support.strava.com/hc/en-us/articles/216918437-Exporting-your-Data-and-Bulk-Export#Bulk) # Overview -The fit2gpx module provides two converter classes: +The fit2gpx module provides two converter classes: - Converter: used to convert a single or multiple FIT files to pandas dataframes or GPX files - StravaConverter: used to fix all the Strava Bulk Export problems in three steps: 1. Unzip GPX and FIT files @@ -45,7 +45,7 @@ df_lap, df_point = conv.fit_to_dataframes(fname='3323369944.fit') - df_points: information per track point: longitude, latitude, altitude, timestamp, heart rate, cadence, speed, power, temperature - Note the 'enhanced_speed' and 'enhanced_altitude' are also extracted. Where overlap exists with their default counterparts, values are identical. However, the default or enhanced speed/altitude fields may be empty depending on the device used to record ([detailed information](https://pkg.go.dev/github.com/tormoder/fit#RecordMsg)). - + # Use Case 2: FIT to GPX Import module and create converter object ```python @@ -70,8 +70,8 @@ from fit2gpx import StravaConverter DIR_STRAVA = 'C:/Users/dorian-saba/Documents/Strava/' -# Step 1: Create StravaConverter object -# - Note: the dir_in must be the path to the central unzipped Strava bulk export folder +# Step 1: Create StravaConverter object +# - Note: the dir_in must be the path to the central unzipped Strava bulk export folder # - Note: You can specify the dir_out if you wish. By default it is set to 'activities_gpx', which will be created in main Strava folder specified. strava_conv = StravaConverter( @@ -92,6 +92,6 @@ strava_conv.strava_fit_to_gpx() #### pandas [pandas](https://github.com/pandas-dev/pandas) is a Python package that provides fast, flexible, and expressive data structures designed to make working with "relational" or "labeled" data both easy and intuitive. #### gpxpy -[gpxpy](https://github.com/tkrajina/gpxpy) is a simple Python library for parsing and manipulating GPX files. It can parse and generate GPX 1.0 and 1.1 files. The generated file will always be a valid XML document, but it may not be (strictly speaking) a valid GPX document. +[gpxpy](https://github.com/tkrajina/gpxpy) is a simple Python library for parsing and manipulating GPX files. It can parse and generate GPX 1.0 and 1.1 files. The generated file will always be a valid XML document, but it may not be (strictly speaking) a valid GPX document. #### fitdecode [fitdecode](https://github.com/polyvertex/fitdecode) is a rewrite of the [fitparse](https://github.com/dtcooper/python-fitparse) module allowing to parse ANT/GARMIN FIT files. From d50d8b6abc738803552f1b16da842dd9a24a0b98 Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:04:10 +0100 Subject: [PATCH 7/8] Mention cli in README --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 909db7a..cbcff2b 100644 --- a/README.md +++ b/README.md @@ -95,3 +95,31 @@ strava_conv.strava_fit_to_gpx() [gpxpy](https://github.com/tkrajina/gpxpy) is a simple Python library for parsing and manipulating GPX files. It can parse and generate GPX 1.0 and 1.1 files. The generated file will always be a valid XML document, but it may not be (strictly speaking) a valid GPX document. #### fitdecode [fitdecode](https://github.com/polyvertex/fitdecode) is a rewrite of the [fitparse](https://github.com/dtcooper/python-fitparse) module allowing to parse ANT/GARMIN FIT files. + +# Command line interface + +You can install this package using pip: + +```shell +pip install --user --upgrade . +``` + +And then you can run the `fit2gpx` command to convert a FIT file to GPX: + +```shell +fit2gpx 3323369944.fit 3323369944.gpx +``` + +You can also read the FIT file from standard input and/or write the GPX file to +standard output: + +```shell +fit2gpx - 3323369944.gpx < 3323369944.fit +fit2gpx 3323369944.fit - > 3323369944.gpx +``` + +To see the help, run: + +```shell +fit2gpx -h +``` From e5aa4dfaa1177864974ecb4cdec986ca4bce2ede Mon Sep 17 00:00:00 2001 From: Jakub Valenta Date: Sun, 12 Feb 2023 21:29:38 +0100 Subject: [PATCH 8/8] Fix stdin/stdout support --- src/fit2gpx.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fit2gpx.py b/src/fit2gpx.py index fbe1fe1..6cbdb02 100644 --- a/src/fit2gpx.py +++ b/src/fit2gpx.py @@ -100,10 +100,11 @@ def fit_to_dataframes(self, fname: str) -> Tuple[pd.DataFrame, pd.DataFrame]: Returns: dfs (tuple): df containing data about the laps , df containing data about the individual points. """ - # Check that this is a .FIT file - input_extension = os.path.splitext(fname)[1] - if input_extension.lower() != '.fit': - raise fitdecode.exceptions.FitHeaderError("Input file must be a .FIT file.") + if isinstance(fname, str) or hasattr(fname, '__fspath__'): + # Check that this is a .FIT file + input_extension = os.path.splitext(fname)[1] + if input_extension.lower() != '.fit': + raise fitdecode.exceptions.FitHeaderError("Input file must be a .FIT file.") data_points = [] data_laps = [] @@ -460,7 +461,7 @@ def cli(): ) parser.add_argument( 'outfile', - type=argparse.FileType('wb'), + type=argparse.FileType('wt'), help='path to the output .GPX file; ' "use '-' to write the file to standard output" )