Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI and pyproject file #9

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
dist
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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
```
40 changes: 40 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <dorian.sabathier+PyPi@gmail.com>"]
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"
66 changes: 49 additions & 17 deletions src/fit2gpx.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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)