Changeset 53bb3e5 in OpenWorkouts-current


Ignore:
Timestamp:
Jan 9, 2019, 12:31:33 PM (5 years ago)
Author:
borja <borja@…>
Branches:
current, feature/docs, master
Children:
119412d
Parents:
e3d7b13
Message:

(#13) - fit files parsing + (#26) - generate .gpx from .fit

  • Added fitparse as a new dependency IMPORTANT: please install it in your existing envs:

pip install python-fitparse

  • Added new attribute to workouts to store attached fit files as Blob objects. IMPORTANT: please update any workouts you have in your db, adding the fit_file attribute to them (None by default)
  • Added new module with the code needed to interact with .fit files (parse, gather data, transform to gpx)
  • Added code to "load" a workout from a fit file
  • Added tools and helpers to transform values (meters->kilometers, meters-per-second->kms-per-hour, semicircles-to-degrees, etc)
  • Refactored some imports
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • ow/models/workout.py

    re3d7b13 r53bb3e5  
    77from repoze.folder import Folder
    88from pyramid.security import Allow, Everyone
    9 from ow.utilities import GPXMinidomParser
     9
     10from ow.utilities import (
     11    GPXMinidomParser,
     12    copy_blob,
     13    create_blob,
     14)
     15
     16from ow.fit import Fit
    1017
    1118
     
    5663        self.tracking_file = kw.get('tracking_file', None)  # Blob
    5764        self.tracking_filetype = ''  # unicode string
     65        # attr to store ANT fit files. For now this file is used to
     66        # generate a gpx-encoded tracking file we then use through
     67        # the whole app
     68        self.fit_file = kw.get('fit_file', None)  # Blob
    5869
    5970    @property
     
    188199        return None
    189200
     201    @property
     202    def tracking_file_path(self):
     203        """
     204        Get the path to the blob file attached as a tracking file.
     205
     206        First check if the file was not committed to the db yet (new workout
     207        not saved yet) and use the path to the temporary file on the fs.
     208        If none is found there, go for the real blob file in the blobs
     209        directory
     210        """
     211        path = None
     212        if self.tracking_file:
     213            path = self.tracking_file._p_blob_uncommitted
     214            if path is None:
     215                path = self.tracking_file._p_blob_committed
     216        return path
     217
     218    @property
     219    def fit_file_path(self):
     220        """
     221        Get the path to the blob file attached as a fit file.
     222
     223        First check if the file was not committed to the db yet (new workout
     224        not saved yet) and use the path to the temporary file on the fs.
     225        If none is found there, go for the real blob file in the blobs
     226        directory
     227        """
     228        path = None
     229        if self.fit_file:
     230            path = self.fit_file._p_blob_uncommitted
     231            if path is None:
     232                path = self.fit_file._p_blob_committed
     233        return path
     234
    190235    def load_from_file(self):
    191236        """
     
    195240        if self.tracking_filetype == 'gpx':
    196241            self.load_from_gpx()
     242        elif self.tracking_filetype == 'fit':
     243            self.load_from_fit()
    197244
    198245    def load_from_gpx(self):
     
    283330        return parser.tracks
    284331
     332    def load_from_fit(self):
     333        """
     334        Try to load data from an ANT-compatible .fit file (if any has been
     335        added to this workout).
     336
     337        "Load data" means:
     338
     339        1. Copy over the uploaded fit file to self.fit_file, so we can keep
     340           that copy around for future use
     341
     342        2. generate a gpx object from the fit file
     343
     344        3. save the gpx object as the tracking_file, which then will be used
     345           by the current code to display and gather data to be displayed/shown
     346           to the user.
     347
     348        4. Grab some basic info from the fit file and store it in the Workout
     349        """
     350        # backup the fit file
     351        self.fit_file = copy_blob(self.tracking_file)
     352
     353        # create an instance of our Fit class
     354        fit = Fit(self.fit_file_path)
     355        fit.load()
     356
     357        # fit -> gpx and store that as the main tracking file
     358        self.tracking_file = create_blob(fit.gpx, 'gpx')
     359        self.tracking_filetype = 'gpx'
     360
     361        # grab the needed data from the fit file, update the workout
     362        self.start = fit.data['start']
     363        # ensure this datetime start object is timezone-aware
     364        self.start = self.start.replace(tzinfo=timezone.utc)
     365        # duration comes in seconds, store a timedelta
     366        self.duration = timedelta(seconds=fit.data['duration'])
     367        # distance comes in meters
     368        self.distance = Decimal(fit.data['distance']) / Decimal(1000.00)
     369        self.uphill = Decimal(fit.data['uphill'])
     370        self.downhill = Decimal(fit.data['downhill'])
     371        # If the user did not provide us with a title, build one from the
     372        # info in the fit file
     373        if not self.title:
     374            self.title = fit.name
     375
     376        if fit.data['avg_hr']:
     377            self.hr_avg = Decimal(fit.data['avg_hr'])
     378            self.hr_min = Decimal(fit.data['min_hr'])
     379            self.hr_max = Decimal(fit.data['max_hr'])
     380
     381        if fit.data['avg_cad']:
     382            self.cad_avg = Decimal(fit.data['avg_cad'])
     383            self.cad_min = Decimal(fit.data['min_cad'])
     384            self.cad_max = Decimal(fit.data['max_cad'])
     385
     386        if fit.data['avg_atemp']:
     387            self.atemp_avg = Decimal(fit.data['avg_atemp'])
     388            self.atemp_min = Decimal(fit.data['min_atemp'])
     389            self.atemp_max = Decimal(fit.data['max_atemp'])
     390
     391        return True
     392
    285393    @property
    286394    def has_tracking_file(self):
     
    290398    def has_gpx(self):
    291399        return self.has_tracking_file and self.tracking_filetype == 'gpx'
     400
     401    @property
     402    def has_fit(self):
     403        return self.fit_file is not None
  • ow/tests/models/test_workout.py

    re3d7b13 r53bb3e5  
    33from datetime import datetime, timedelta, timezone
    44from decimal import Decimal
    5 from unittest.mock import patch
     5from unittest.mock import patch, Mock
    66
    77import pytest
     
    195195        assert workout.atemp['max'] == 12
    196196        assert workout.atemp['avg'] == 5
     197
     198    def test_tracking_file_path(self):
     199        workout = Workout()
     200        # no tracking file, path is None
     201        assert workout.tracking_file_path is None
     202        # workout still not saved to the db
     203        workout.tracking_file = Mock()
     204        workout.tracking_file._p_blob_uncommitted = '/tmp/blobtempfile'
     205        workout.tracking_file._p_blob_committed = None
     206        assert workout.tracking_file_path == '/tmp/blobtempfile'
     207        workout.tracking_file._p_blob_uncommitted = None
     208        workout.tracking_file._p_blob_committed = '/var/db/blobs/blobfile'
     209        assert workout.tracking_file_path == '/var/db/blobs/blobfile'
    197210
    198211    def test_load_from_file_invalid(self):
  • ow/utilities.py

    re3d7b13 r53bb3e5  
    11import re
    2 import datetime
     2from datetime import datetime
     3from decimal import Decimal
     4from shutil import copyfileobj
     5
    36from unidecode import unidecode
    47from xml.dom import minidom
    5 from decimal import Decimal
     8from ZODB.blob import Blob
    69
    710
     
    7982                rfc3339 = trkpt.getElementsByTagName('time')[0].firstChild.data
    8083                try:
    81                     t = datetime.datetime.strptime(
     84                    t = datetime.strptime(
    8285                        rfc3339, '%Y-%m-%dT%H:%M:%S.%fZ')
    8386                except ValueError:
    84                     t = datetime.datetime.strptime(
     87                    t = datetime.strptime(
    8588                        rfc3339, '%Y-%m-%dT%H:%M:%SZ')
    8689
     
    113116                    'cad': cad,
    114117                    'atemp': atemp})
     118
     119
     120def semicircles_to_degrees(semicircles):
     121    return semicircles * (180 / pow(2, 31))
     122
     123
     124def degrees_to_semicircles(degrees):
     125    return degrees * (pow(2, 31) / 180)
     126
     127
     128def miles_to_kms(miles):
     129    factor = 0.62137119
     130    return miles / factor
     131
     132
     133def kms_to_miles(kms):
     134    factor = 0.62137119
     135    return kms * factor
     136
     137
     138def meters_to_kms(meters):
     139    return meters / 1000
     140
     141
     142def kms_to_meters(kms):
     143    return kms * 1000
     144
     145
     146def mps_to_kmph(mps):
     147    """
     148    Transform a value from meters-per-second to kilometers-per-hour
     149    """
     150    return mps * 3.6
     151
     152
     153def kmph_to_mps(kmph):
     154    """
     155    Transform a value from kilometers-per-hour to meters-per-second
     156    """
     157    return kmph * 0.277778
     158
     159
     160def copy_blob(blob):
     161    """
     162    Create a copy of a blob object, returning another blob object that is
     163    the copy of the given blob file.
     164    """
     165    new_blob = Blob()
     166    if getattr(blob, 'file_extension', None):
     167        new_blob.file_extension = blob.file_extension
     168    with blob.open('r') as orig_blob, new_blob.open('w') as dest_blob:
     169        orig_blob.seek(0)
     170        copyfileobj(orig_blob, dest_blob)
     171    return new_blob
     172
     173
     174def create_blob(data, file_extension):
     175    """
     176    Create a ZODB blob file from some data, return the blob object
     177    """
     178    blob = Blob()
     179    blob.file_extension = file_extension
     180    with blob.open('w') as open_blob:
     181        # use .encode() to convert the string to bytes if needed
     182        if not isinstance(data, bytes):
     183            data = data.encode('utf-8')
     184        open_blob.write(data)
     185    return blob
  • setup.py

    re3d7b13 r53bb3e5  
    2828    'gpxpy',
    2929    'lxml',
    30     'pytz'
     30    'pytz',
     31    'fitparse'
    3132]
    3233
Note: See TracChangeset for help on using the changeset viewer.