diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac8d7b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +dist diff --git a/README.md b/README.md index 6041e0b..cbcff2b 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,34 @@ 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. + +# 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 +``` 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" diff --git a/src/fit2gpx.py b/src/fit2gpx.py index cf931db..6cbdb02 100644 --- a/src/fit2gpx.py +++ b/src/fit2gpx.py @@ -1,13 +1,15 @@ """Classes to convert FIT files to GPX, including tools to process Strava Bulk Export """ -import os +import argparse 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 @@ -98,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 = [] @@ -194,14 +197,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) @@ -222,8 +227,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 @@ -437,3 +446,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('wt'), + 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)