From fd3bd56da6d0a31dd2fdaa725c2f922a35b6be0b Mon Sep 17 00:00:00 2001 From: David Cooper Date: Fri, 7 Jan 2022 18:02:44 -0500 Subject: [PATCH 01/12] Update LICENSE Happy New Year! --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 18e253b..387b0b0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License -Copyright (c) 2011-2021, David Cooper -Copyright (c) 2017-2021, Carey Metcalfe +Copyright (c) 2011-2022, David Cooper +Copyright (c) 2017-2022, Carey Metcalfe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 71fc57b735ded7d19bd40088298fd42980a6edd7 Mon Sep 17 00:00:00 2001 From: Mathias B Date: Sun, 3 Apr 2022 21:54:35 +0200 Subject: [PATCH 02/12] Handle error when parsing field with sleep_time of 86400 (#138) Fixes #132 --- fitparse/processors.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/fitparse/processors.py b/fitparse/processors.py index 493f166..268859e 100644 --- a/fitparse/processors.py +++ b/fitparse/processors.py @@ -83,9 +83,18 @@ def process_type_local_date_time(self, field_data): def process_type_localtime_into_day(self, field_data): if field_data.value is not None: - m, s = divmod(field_data.value, 60) - h, m = divmod(m, 60) - field_data.value = datetime.time(h, m, s) + # NOTE: Values larger or equal to 86400 should not be possible. + # Additionally, if the value is exactly 86400, it will lead to an error when trying to + # create the time with datetime.time(24, 0 , 0). + # + # E.g. Garmin does add "sleep_time": 86400 to its fit files, + # which causes an error if not properly handled. + if field_data.value >= 86400: + field_data.value = datetime.time.max + else: + m, s = divmod(field_data.value, 60) + h, m = divmod(m, 60) + field_data.value = datetime.time(h, m, s) field_data.units = None From f28f220f8895a586fbc7f9afd62179f144fcfe61 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 3 Apr 2022 16:10:51 -0400 Subject: [PATCH 03/12] Remove unsupported Python versions from CI script --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a535b93..0d7ee4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['2.7', '3.3', '3.4', '3.5', '3.6', '3.x', 'pypy2', 'pypy3'] + python-version: ['2.7', '3.5', '3.6', '3.x', 'pypy2', 'pypy3'] steps: - uses: actions/checkout@v2 From 2a99f92bcdd43cd6179eaa9ddf5dab41b9861de2 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Wed, 7 Sep 2022 13:53:18 -0700 Subject: [PATCH 04/12] Update README.md Call for maintiner --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 50c3cad..09c8882 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ python-fitparse =============== +> :warning: **NOTE:** *I have **limited to no time** to work on this package +> these days!* +> +> I am looking for a maintainer to help with issues and updating/releasing the package. +> Please reach out via email at if you have interest in helping. +> +> Cheers, +> +> David + Here's a Python library to parse ANT/Garmin `.FIT` files. [![Build Status](https://github.com/dtcooper/python-fitparse/workflows/test/badge.svg)](https://github.com/dtcooper/python-fitparse/actions?query=workflow%3Atest) From 301970f5da492ae23635bf5e3a55b3836785fa9f Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 22 Aug 2022 21:12:56 +0100 Subject: [PATCH 05/12] Use pyupgrade --- fitparse/base.py | 30 +++++++++++++++--------------- fitparse/processors.py | 4 ++-- fitparse/profile.py | 1 - fitparse/records.py | 18 +++++++++--------- fitparse/utils.py | 2 +- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/fitparse/base.py b/fitparse/base.py index 6af6392..8880a00 100644 --- a/fitparse/base.py +++ b/fitparse/base.py @@ -20,12 +20,12 @@ from fitparse.utils import fileish_open, is_iterable, FitParseError, FitEOFError, FitCRCError, FitHeaderError -class DeveloperDataMixin(object): +class DeveloperDataMixin: def __init__(self, *args, check_developer_data=True, **kwargs): self.check_developer_data = check_developer_data self.dev_types = {} - super(DeveloperDataMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _append_dev_data_id(self, dev_data_index, application_id=None, fields=None): if fields is None: @@ -97,7 +97,7 @@ def get_dev_type(self, dev_data_index, field_def_num): if dev_data_index not in self.dev_types: if self.check_developer_data: raise FitParseError( - "No such dev_data_index=%s found when looking up field %s" % (dev_data_index, field_def_num) + f"No such dev_data_index={dev_data_index} found when looking up field {field_def_num}" ) warnings.warn( @@ -110,11 +110,11 @@ def get_dev_type(self, dev_data_index, field_def_num): if field_def_num not in dev_type['fields']: if self.check_developer_data: raise FitParseError( - "No such field %s for dev_data_index %s" % (field_def_num, dev_data_index) + f"No such field {field_def_num} for dev_data_index {dev_data_index}" ) warnings.warn( - "Field %s for dev_data_index %s missing. Adding dummy field." % (field_def_num, dev_data_index) + f"Field {field_def_num} for dev_data_index {dev_data_index} missing. Adding dummy field." ) self._append_dev_field_description( dev_data_index=dev_data_index, @@ -141,7 +141,7 @@ def __init__(self, fileish, *args, check_crc=True, data_processor=None, **kwargs # Start off by parsing the file header (sets initial attribute values) self._parse_file_header() - super(FitFileDecoder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def __del__(self): self.close() @@ -193,7 +193,7 @@ def _read_and_assert_crc(self, allow_zero=False): return if crc_computed == crc_read or (allow_zero and crc_read == 0): return - raise FitCRCError('CRC Mismatch [computed: %s, read: %s]' % ( + raise FitCRCError('CRC Mismatch [computed: {}, read: {}]'.format( Crc.format(crc_computed), Crc.format(crc_read))) ########## @@ -530,7 +530,7 @@ def _make_set(obj): if is_iterable(obj): return set(obj) else: - return set((obj,)) + return {obj} ########## # Public API @@ -550,15 +550,15 @@ def __iter__(self): return self.get_messages() -class CacheMixin(object): +class CacheMixin: """Add message caching to the FitFileDecoder""" def __init__(self, *args, **kwargs): - super(CacheMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._messages = [] def _parse_message(self): - self._messages.append(super(CacheMixin, self)._parse_message()) + self._messages.append(super()._parse_message()) return self._messages[-1] def get_messages(self, name=None, with_definitions=False, as_dict=False): @@ -572,7 +572,7 @@ def get_messages(self, name=None, with_definitions=False, as_dict=False): if self._should_yield(message, with_definitions, names): yield message.as_dict() if as_dict else message - for message in super(CacheMixin, self).get_messages(names, with_definitions, as_dict): + for message in super().get_messages(names, with_definitions, as_dict): yield message @property @@ -584,12 +584,12 @@ def parse(self): pass -class DataProcessorMixin(object): +class DataProcessorMixin: """Add data processing to the FitFileDecoder""" def __init__(self, *args, **kwargs): self._processor = kwargs.pop("data_processor", None) or FitFileDataProcessor() - super(DataProcessorMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _parse_data_message(self, header): header, def_mesg, field_datas = self._parse_data_message_components(header) @@ -612,7 +612,7 @@ class UncachedFitFile(DataProcessorMixin, FitFileDecoder): def __init__(self, fileish, *args, check_crc=True, data_processor=None, **kwargs): # Ensure all optional params are passed as kwargs - super(UncachedFitFile, self).__init__( + super().__init__( fileish, *args, check_crc=check_crc, diff --git a/fitparse/processors.py b/fitparse/processors.py index 268859e..6848584 100644 --- a/fitparse/processors.py +++ b/fitparse/processors.py @@ -5,7 +5,7 @@ UTC_REFERENCE = 631065600 # timestamp for UTC 00:00 Dec 31 1989 -class FitFileDataProcessor(object): +class FitFileDataProcessor: # TODO: Document API # Functions that will be called to do the processing: #def run_type_processor(field_data) @@ -107,7 +107,7 @@ def run_field_processor(self, field_data): if field_data.name.endswith("_speed"): self.process_field_speed(field_data) else: - super(StandardUnitsDataProcessor, self).run_field_processor(field_data) + super().run_field_processor(field_data) def process_field_distance(self, field_data): if field_data.value is not None: diff --git a/fitparse/profile.py b/fitparse/profile.py index aa6ddbd..ce44c78 100644 --- a/fitparse/profile.py +++ b/fitparse/profile.py @@ -1,4 +1,3 @@ - # ***************** BEGIN AUTOMATICALLY GENERATED FIT PROFILE ****************** # *************************** DO NOT EDIT THIS FILE **************************** # ************ EXPORTED PROFILE FROM SDK VERSION 20.8 ON 2019-03-05 ************ diff --git a/fitparse/records.py b/fitparse/records.py index f9149e4..857a1dc 100644 --- a/fitparse/records.py +++ b/fitparse/records.py @@ -15,7 +15,7 @@ from itertools import izip_longest as zip_longest -class RecordBase(object): +class RecordBase: # namedtuple-like base class. Subclasses should must __slots__ __slots__ = () @@ -83,7 +83,7 @@ class DevFieldDefinition(RecordBase): __slots__ = ('field', 'dev_data_index', 'base_type', 'def_num', 'size') def __init__(self, **kwargs): - super(DevFieldDefinition, self).__init__(**kwargs) + super().__init__(**kwargs) # For dev fields, the base_type and type are always the same. self.base_type = self.type @@ -129,7 +129,7 @@ def get_value(self, field_name): def get_values(self): # SIMPLIFY: get rid of this completely - return dict((f.name if f.name else f.def_num, f.value) for f in self.fields) + return {f.name if f.name else f.def_num: f.value for f in self.fields} @property def name(self): @@ -159,7 +159,7 @@ def __iter__(self): def __repr__(self): return '' % ( self.name, self.mesg_num, self.header.local_mesg_num, - ', '.join(["%s: %s" % (fd.name, fd.value) for fd in self.fields]), + ', '.join([f"{fd.name}: {fd.value}" for fd in self.fields]), ) def __str__(self): @@ -171,7 +171,7 @@ class FieldData(RecordBase): __slots__ = ('field_def', 'field', 'parent_field', 'value', 'raw_value', 'units') def __init__(self, *args, **kwargs): - super(FieldData, self).__init__(self, *args, **kwargs) + super().__init__(self, *args, **kwargs) if not self.units and self.field: # Default to units on field, otherwise None. # NOTE:Not a property since you may want to override this in a data processor @@ -233,7 +233,7 @@ def __repr__(self): ) def __str__(self): - return '%s: %s%s' % ( + return '{}: {}{}'.format( self.name, self.value, ' [%s]' % self.units if self.units else '', ) @@ -260,7 +260,7 @@ class FieldType(RecordBase): __slots__ = ('name', 'base_type', 'values') def __repr__(self): - return '' % (self.name, self.base_type) + return f'' class MessageType(RecordBase): @@ -342,7 +342,7 @@ def render(self, raw_value): return raw_value -class Crc(object): +class Crc: """FIT file CRC computation.""" CRC_TABLE = ( @@ -358,7 +358,7 @@ def __init__(self, value=0, byte_arr=None): self.update(byte_arr) def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, self.value or "-") + return '<{} {}>'.format(self.__class__.__name__, self.value or "-") def __str__(self): return self.format(self.value) diff --git a/fitparse/utils.py b/fitparse/utils.py index aa70a90..568df0f 100644 --- a/fitparse/utils.py +++ b/fitparse/utils.py @@ -3,7 +3,7 @@ try: from collections.abc import Iterable except ImportError: - from collections import Iterable + from collections.abc import Iterable try: # Python 3.4+ From 06b68d684a338cbffe15b903286bcacee2f27157 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 22 Aug 2022 21:22:17 +0100 Subject: [PATCH 06/12] Remove Python 2 compatibility code --- docs/index.rst | 2 +- fitparse/base.py | 8 +------- fitparse/records.py | 22 ++++------------------ fitparse/utils.py | 25 +++++++------------------ scripts/fitdump | 17 ++--------------- setup.py | 4 ++-- tests/test.py | 6 +----- tests/test_records.py | 8 +------- tests/test_utils.py | 15 +++------------ 9 files changed, 22 insertions(+), 85 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 434a376..4a4fc30 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,7 +56,7 @@ Requirements The following are required to install :mod:`fitparse`, -* `Python `_ 2.7 and above +* `Python `_ 3.6 and above API Documentation diff --git a/fitparse/base.py b/fitparse/base.py index 8880a00..b0ebf87 100644 --- a/fitparse/base.py +++ b/fitparse/base.py @@ -5,12 +5,6 @@ import struct import warnings -# Python 2 compat -try: - num_types = (int, float, long) -except NameError: - num_types = (int, float) - from fitparse.processors import FitFileDataProcessor from fitparse.profile import FIELD_TYPE_TIMESTAMP, MESSAGE_TYPES from fitparse.records import ( @@ -396,7 +390,7 @@ def _apply_scale_offset(self, field, raw_value): if isinstance(raw_value, tuple): # Contains multiple values, apply transformations to all of them return tuple(self._apply_scale_offset(field, x) for x in raw_value) - elif isinstance(raw_value, num_types): + elif isinstance(raw_value, (int, float)): if field.scale: raw_value = float(raw_value) / field.scale if field.offset: diff --git a/fitparse/records.py b/fitparse/records.py index 857a1dc..7d557fa 100644 --- a/fitparse/records.py +++ b/fitparse/records.py @@ -1,18 +1,7 @@ import math import struct -# Python 2 compat -try: - int_types = (int, long,) - byte_iter = bytearray -except NameError: - int_types = (int,) - byte_iter = lambda x: x - -try: - from itertools import zip_longest -except ImportError: - from itertools import izip_longest as zip_longest +from itertools import zip_longest class RecordBase: @@ -336,7 +325,7 @@ def render(self, raw_value): raw_value = unpacked_num # Mask and shift like a normal number - if isinstance(raw_value, int_types): + if isinstance(raw_value, int): raw_value = (raw_value >> self.bit_offset) & ((1 << self.bits) - 1) return raw_value @@ -376,7 +365,7 @@ def format(value): @classmethod def calculate(cls, byte_arr, crc=0): """Compute CRC for input bytes.""" - for byte in byte_iter(byte_arr): + for byte in byte_arr: # Taken verbatim from FIT SDK docs tmp = cls.CRC_TABLE[crc & 0xF] crc = (crc >> 4) & 0x0FFF @@ -390,10 +379,7 @@ def calculate(cls, byte_arr, crc=0): def parse_string(string): try: - try: - s = string[:string.index(0x00)] - except TypeError: # Python 2 compat - s = string[:string.index('\x00')] + s = string[:string.index('\x00')] except ValueError: # FIT specification defines the 'string' type as follows: "Null # terminated string encoded in UTF-8 format". diff --git a/fitparse/utils.py b/fitparse/utils.py index 568df0f..65f424a 100644 --- a/fitparse/utils.py +++ b/fitparse/utils.py @@ -1,15 +1,8 @@ import io import re -try: - from collections.abc import Iterable -except ImportError: - from collections.abc import Iterable +from collections.abc import Iterable -try: - # Python 3.4+ - from pathlib import PurePath -except ImportError: - PurePath = None +from pathlib import PurePath class FitParseError(ValueError): @@ -56,18 +49,14 @@ def fileish_open(fileish, mode): # BytesIO-like object return fileish elif isinstance(fileish, str): - # Python2 - file path, file contents in the case of a TypeError - # Python3 - file path - try: - return open(fileish, mode) - except TypeError: - return io.BytesIO(fileish) + # file path + return open(fileish, mode) - # Python 3 - pathlib obj - if PurePath and isinstance(fileish, PurePath): + # pathlib obj + if isinstance(fileish, PurePath): return fileish.open(mode) - # Python 3 - file contents + # file contents return io.BytesIO(fileish) diff --git a/scripts/fitdump b/scripts/fitdump index 5e4a4b4..c907972 100755 --- a/scripts/fitdump +++ b/scripts/fitdump @@ -7,16 +7,8 @@ import datetime import itertools import json import os.path -import sys import types -# Python 2 compat -try: - BrokenPipeError -except NameError: - import socket - BrokenPipeError = socket.error - import fitparse @@ -44,7 +36,8 @@ def parse_args(args=None): ) parser.add_argument('-v', '--verbose', action='count', default=0) parser.add_argument( - '-o', '--output', type=argparse.FileType(mode='w'), default="-", + '-o', '--output', type=argparse.FileType(mode='w', encoding="utf-8"), + default="-", help='File to output data into (defaults to stdout)', ) parser.add_argument( @@ -65,12 +58,6 @@ def parse_args(args=None): options = parser.parse_args(args) - # Work around argparse.FileType not accepting an `encoding` kwarg in - # Python < 3.4 by closing and reopening the file (unless it's stdout) - if options.output is not sys.stdout: - options.output.close() - options.output = codecs.open(options.output.name, 'w', encoding='UTF-8') - options.verbose = options.verbose >= 1 options.with_defs = (options.type == "readable" and options.verbose) options.as_dict = (options.type != "readable" and options.verbose) diff --git a/setup.py b/setup.py index 7c8dc13..ad51427 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ requires = None -if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 3): - sys.exit("Python 2.7 or Python 3.3+ are required.") +if sys.version_info < (3, 6): + sys.exit("Python 3.6+ is required.") setup( diff --git a/tests/test.py b/tests/test.py index eea5dc2..fd23282 100755 --- a/tests/test.py +++ b/tests/test.py @@ -4,7 +4,6 @@ import datetime import os from struct import pack -import sys import warnings from fitparse import FitFile @@ -12,10 +11,7 @@ from fitparse.records import BASE_TYPES, Crc from fitparse.utils import FitEOFError, FitCRCError, FitHeaderError, FitParseError -if sys.version_info >= (2, 7): - import unittest -else: - import unittest2 as unittest +import unittest def generate_messages(mesg_num, local_mesg_num, field_defs, endian='<', data=None): diff --git a/tests/test_records.py b/tests/test_records.py index 5e3b823..fd401f9 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,14 +1,8 @@ #!/usr/bin/env python -import sys - from fitparse.records import Crc -if sys.version_info >= (2, 7): - import unittest -else: - import unittest2 as unittest - +import unittest class RecordsTestCase(unittest.TestCase): def test_crc(self): diff --git a/tests/test_utils.py b/tests/test_utils.py index 01d3409..9d456aa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,21 +2,13 @@ import io import os -import sys import tempfile -try: - # Python 3.4+ - from pathlib import Path -except ImportError: - Path = None +from pathlib import Path from fitparse.utils import fileish_open, is_iterable -if sys.version_info >= (2, 7): - import unittest -else: - import unittest2 as unittest +import unittest def testfile(filename): @@ -44,8 +36,7 @@ def test_fopen(fileish): test_fopen(f.read()) with open(testfile("nametest.FIT"), 'rb') as f: test_fopen(io.BytesIO(f.read())) - if Path: - test_fopen(Path(testfile('nametest.FIT'))) + test_fopen(Path(testfile('nametest.FIT'))) def test_fileish_open_write(self): From 90d0aacad1d595f03368cb66ca7435a179672a85 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 22 Aug 2022 22:25:34 +0100 Subject: [PATCH 07/12] Pyupgrade tests scripts and tests --- scripts/fitdump | 19 +++++++++---------- scripts/generate_profile.py | 34 +++++++++++++++++----------------- scripts/unit_tool.py | 2 +- tests/test.py | 8 ++++---- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/scripts/fitdump b/scripts/fitdump index c907972..629f61b 100755 --- a/scripts/fitdump +++ b/scripts/fitdump @@ -1,5 +1,4 @@ #!/usr/bin/env python -from __future__ import print_function import argparse import codecs @@ -13,16 +12,16 @@ import fitparse def format_message(num, message, options): - s = ["{}. {}".format(num, message.name)] + s = [f"{num}. {message.name}"] if options.with_defs: - s.append(' [{}]'.format(message.type)) + s.append(f' [{message.type}]') s.append('\n') if message.type == 'data': for field_data in message: - s.append(' * {}: {}'.format(field_data.name, field_data.value)) + s.append(f' * {field_data.name}: {field_data.value}') if field_data.units: - s.append(' [{}]'.format(field_data.units)) + s.append(f' [{field_data.units}]') s.append('\n') s.append('\n') @@ -79,7 +78,7 @@ class RecordJSONEncoder(json.JSONEncoder): } } # Fall back to original to raise a TypeError - return super(RecordJSONEncoder, self).default(obj) + return super().default(obj) def generate_gpx(records, filename=None): @@ -100,7 +99,7 @@ def generate_gpx(records, filename=None): if message.name == "file_id": for field_data in message: if field_data.name == "time_created" and type(field_data.value) == datetime.datetime: - yield ' \n'.format(field_data.value.strftime(GPX_TIME_FMT)) + yield f' \n' break else: # No time found in the fields, check next record @@ -111,13 +110,13 @@ def generate_gpx(records, filename=None): break if filename: - yield ' {}\n'.format(filename) + yield f' {filename}\n' yield ' \n' yield ' \n' if filename: - yield ' {}\n'.format(filename) + yield f' {filename}\n' yield ' \n' @@ -191,7 +190,7 @@ def main(args=None): finally: try: options.output.close() - except IOError: + except OSError: pass if __name__ == '__main__': diff --git a/scripts/generate_profile.py b/scripts/generate_profile.py index c88ab60..5d5a5fb 100755 --- a/scripts/generate_profile.py +++ b/scripts/generate_profile.py @@ -28,14 +28,14 @@ def header(header, indent=0): - return '%s# %s' % (' ' * indent, (' %s ' % header).center(78 - indent, '*')) + return '{}# {}'.format(' ' * indent, (' %s ' % header).center(78 - indent, '*')) def scrub_symbol_name(symbol_name): return SYMBOL_NAME_SCRUBBER.sub('_', symbol_name) -PROFILE_HEADER_FIRST_PART = "%s\n%s" % ( +PROFILE_HEADER_FIRST_PART = "{}\n{}".format( header('BEGIN AUTOMATICALLY GENERATED FIT PROFILE'), header('DO NOT EDIT THIS FILE'), ) @@ -92,7 +92,7 @@ def scrub_symbol_name(symbol_name): def render_type(name): if name in BASE_TYPES: - return "BASE_TYPES[%s], # %s" % (BASE_TYPES[name], name) + return "BASE_TYPES[{}], # {}".format(BASE_TYPES[name], name) else: return "FIELD_TYPES['%s']," % name @@ -121,7 +121,7 @@ def get_mesg_num(self, name): def __str__(self): s = 'FIELD_TYPES = {\n' for type in sorted(self.types, key=lambda x: x.name): - s += " '%s': %s,\n" % (type.name, indent(type)) + s += " '{}': {},\n".format(type.name, indent(type)) s += '}' return s @@ -131,18 +131,18 @@ def get(self, value_name): for value in self.values: if value.name == value_name: return value - raise AssertionError("Invalid value name %s in type %s" % (value_name, self.name)) + raise AssertionError("Invalid value name {} in type {}".format(value_name, self.name)) def __str__(self): s = 'FieldType(%s\n' % render_comment(self.comment) s += " name='%s',\n" % (self.name) - s += " base_type=BASE_TYPES[%s], # %s\n" % ( + s += " base_type=BASE_TYPES[{}], # {}\n".format( BASE_TYPES[self.base_type], self.base_type, ) if self.values: s += " values={\n" for value in sorted(self.values, key=lambda x: x.value if isinstance(x.value, int) else int(x.value, 16)): - s += " %s\n" % (value,) + s += " {}\n".format(value) s += " },\n" s += ")" return s @@ -150,7 +150,7 @@ def __str__(self): class TypeValueInfo(namedtuple('TypeValueInfo', ('name', 'value', 'comment'))): def __str__(self): - return "%s: '%s',%s" % (self.value, self.name, render_comment(self.comment)) + return "{}: '{}',{}".format(self.value, self.name, render_comment(self.comment)) class MessageList(namedtuple('MessageList', ('messages'))): @@ -170,7 +170,7 @@ def __str__(self): s += '\n\n' s += "%s\n" % header(message.group_name, 4) last_group_name = message.group_name - s += " %s: %s,\n" % (message.num, indent(message)) + s += " {}: {},\n".format(message.num, indent(message)) s += '}' return s @@ -188,7 +188,7 @@ def get_field_by_name(self, mesg_name, field_name): if field.name == field_name: return mesg, field - raise ValueError('field "%s" not found in message "%s"' % (field_name, mesg_name)) + raise ValueError('field "{}" not found in message "{}"'.format(field_name, mesg_name)) class MessageInfo(namedtuple('MessageInfo', ('name', 'num', 'group_name', 'fields', 'comment'))): @@ -196,7 +196,7 @@ def get(self, field_name): for field in self.fields: if field.name == field_name: return field - raise AssertionError("Invalid field name %s in message %s" % (field_name, self.name)) + raise AssertionError("Invalid field name {} in message {}".format(field_name, self.name)) def __str__(self): s = "MessageType(%s\n" % render_comment(self.comment) @@ -391,7 +391,7 @@ def parse_types(types_rows): if value.name and value.value is not None: # Don't add ignore keyed types - if "%s:%s" % (type.name, value.name) not in IGNORE_TYPE_VALUES: + if "{}:{}".format(type.name, value.name) not in IGNORE_TYPE_VALUES: type.values.append(value) # Add missing boolean type if it's not there @@ -546,7 +546,7 @@ def get_xls_and_version_from_zip(path): def main(input_xls_or_zip, output_py_path=None): if output_py_path and os.path.exists(output_py_path): - if not open(output_py_path, 'r').read().strip().startswith(PROFILE_HEADER_FIRST_PART): + if not open(output_py_path).read().strip().startswith(PROFILE_HEADER_FIRST_PART): print("Python file doesn't begin with appropriate header. Exiting.") sys.exit(1) @@ -563,7 +563,7 @@ def main(input_xls_or_zip, output_py_path=None): for mesg_name in MESSAGE_NUM_DECLARATIONS: mesg_info = message_list.get_by_name(mesg_name) - mesg_num_declarations.append('MESG_NUM_%s = %s' % ( + mesg_num_declarations.append('MESG_NUM_{} = {}'.format( scrub_symbol_name(mesg_name).upper(), str(mesg_info.num) if mesg_info else 'None')) @@ -573,7 +573,7 @@ def main(input_xls_or_zip, output_py_path=None): mesg_name, field_name = field_fqn.split('.', maxsplit=1) mesg_info, field_info = message_list.get_field_by_name(mesg_name, field_name) - field_decl = 'FIELD_NUM_%s_%s = %s' % ( + field_decl = 'FIELD_NUM_{}_{} = {}'.format( scrub_symbol_name(mesg_name).upper(), scrub_symbol_name(field_name).upper(), str(field_info.num)) @@ -582,7 +582,7 @@ def main(input_xls_or_zip, output_py_path=None): output = '\n'.join([ "\n%s" % PROFILE_HEADER_FIRST_PART, - header('EXPORTED PROFILE FROM %s ON %s' % ( + header('EXPORTED PROFILE FROM {} ON {}'.format( ('SDK VERSION %s' % profile_version) if profile_version else 'SPREADSHEET', datetime.datetime.now().strftime('%Y-%m-%d'), )), @@ -609,7 +609,7 @@ def main(input_xls_or_zip, output_py_path=None): if output_py_path: open(output_py_path, 'w').write(output) - print('Profile version %s written to %s' % ( + print('Profile version {} written to {}'.format( profile_version if profile_version else '', output_py_path)) else: diff --git a/scripts/unit_tool.py b/scripts/unit_tool.py index ee1c876..0f66258 100755 --- a/scripts/unit_tool.py +++ b/scripts/unit_tool.py @@ -54,7 +54,7 @@ def do_fitparse_profile(): if __name__ == '__main__': if len(sys.argv) < 2: - print("Usage: {0} Profile.xls".format(os.path.basename(__file__))) + print(f"Usage: {os.path.basename(__file__)} Profile.xls") sys.exit(0) do_profile_xls() diff --git a/tests/test.py b/tests/test.py index fd23282..886f21f 100755 --- a/tests/test.py +++ b/tests/test.py @@ -35,7 +35,7 @@ def generate_messages(mesg_num, local_mesg_num, field_defs, endian='<', data=Non for mesg_data in data: s = pack('B', local_mesg_num) for value, base_type in zip(mesg_data, base_type_list): - s += pack("%s%s" % (endian, base_type.fmt), value) + s += pack("{}{}".format(endian, base_type.fmt), value) mesgs.append(s) return b''.join(mesgs) @@ -106,7 +106,7 @@ def test_basic_file_big_endian(self): def test_component_field_accumulaters(self): # TODO: abstract CSV parsing - csv_fp = open(testfile('compressed-speed-distance-records.csv'), 'r') + csv_fp = open(testfile('compressed-speed-distance-records.csv')) csv_file = csv.reader(csv_fp) next(csv_file) # Consume header @@ -252,7 +252,7 @@ def test_parsing_edge_820_fit_file(self): 'garmin-edge-820-bike-records.csv') def _csv_test_helper(self, fit_file, csv_file): - csv_fp = open(testfile(csv_file), 'r') + csv_fp = open(testfile(csv_file)) csv_messages = csv.reader(csv_fp) field_names = next(csv_messages) # Consume header @@ -298,7 +298,7 @@ def _csv_test_helper(self, fit_file, csv_file): self.assertAlmostEqual(fit_value, float(csv_value)) else: self.assertEqual(fit_value, csv_value, - msg="For %s, FIT value '%s' did not match CSV value '%s'" % (field_name, fit_value, csv_value)) + msg="For {}, FIT value '{}' did not match CSV value '{}'".format(field_name, fit_value, csv_value)) try: next(messages) From 4372b36cbfbe8661a2cb843a33f5435504ae0bbc Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Wed, 7 Sep 2022 08:57:31 +0100 Subject: [PATCH 08/12] Fix indexing --- fitparse/records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fitparse/records.py b/fitparse/records.py index 7d557fa..e9fcd6a 100644 --- a/fitparse/records.py +++ b/fitparse/records.py @@ -379,7 +379,7 @@ def calculate(cls, byte_arr, crc=0): def parse_string(string): try: - s = string[:string.index('\x00')] + s = string[:string.index(0x00)] except ValueError: # FIT specification defines the 'string' type as follows: "Null # terminated string encoded in UTF-8 format". From c18025756de7d87fe0ea805046fff0637f5254d7 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Wed, 22 Feb 2023 13:55:38 -0500 Subject: [PATCH 09/12] Update README.md Add link to fitdecode in absence of a maintainer --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 09c8882..d526522 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ python-fitparse > I am looking for a maintainer to help with issues and updating/releasing the package. > Please reach out via email at if you have interest in helping. > +> If you're having trouble using this package for whatever reason, might we suggest using +> an alternative library: [fitdecode](https://github.com/polyvertex/fitdecode) by +> [polyvertex](https://github.com/polyvertex). +> > Cheers, > > David From 245945113f164a8556e0d087203c725a1e6e6cbe Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Wed, 20 Oct 2021 07:43:12 -0700 Subject: [PATCH 10/12] Cleanup Python versions used for GitHub Actions GitHub Actions no longer supports Python versions prior to 3.7, including 2.7. Also, `docs/index.rst` has declared 3.6 to be the minimum supported version since 06b68d684, and python-fitparse has not compiled with Python 2.7 since f9ad9871c5df05af1aee9b0a2aeb2a871aaff559 (which uses Python 3.x-only syntax). --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d7ee4e..d176d94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['2.7', '3.5', '3.6', '3.x', 'pypy2', 'pypy3'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.x'] steps: - uses: actions/checkout@v2 From 4fc9cd813ea0b6a191c95fa31b194ac65ac129f0 Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Thu, 30 Sep 2021 13:50:38 -0700 Subject: [PATCH 11/12] Add strava_product subfield to identify FIT files generated by Strava apps --- fitparse/profile.py | 60 ++++++++++++++++++ .../strava-android-app-201.10-b1218918.fit | Bin 0 -> 78233 bytes 2 files changed, 60 insertions(+) create mode 100644 tests/files/strava-android-app-201.10-b1218918.fit diff --git a/fitparse/profile.py b/fitparse/profile.py index ce44c78..daaf579 100644 --- a/fitparse/profile.py +++ b/fitparse/profile.py @@ -1380,6 +1380,14 @@ 7: 'swiss_ball_dumbbell_flye', }, ), + 'strava_product': FieldType( + name='strava_product', + base_type=BASE_TYPES[0x84], # uint16 + values={ + 101: 'Strava iPhone App', # recent versions of Strava iPhone app + 102: 'Strava Android App', # recent versions of Strava Android app + } + ), 'garmin_product': FieldType( name='garmin_product', base_type=BASE_TYPES[0x84], # uint16 @@ -4009,6 +4017,19 @@ value='dynastream_oem', raw_value=13, ), + ) + ), + SubField( + name='strava_product', + def_num=2, + type=FIELD_TYPES['strava_product'], + ref_fields=( + ReferenceField( + name='manufacturer', + def_num=1, + value='strava', + raw_value=265, + ), ), ), ), @@ -7077,6 +7098,19 @@ ), ), ), + SubField( + name='strava_product', + def_num=4, + type=FIELD_TYPES['strava_product'], + ref_fields=( + ReferenceField( + name='manufacturer', + def_num=2, + value='strava', + raw_value=265, + ), + ), + ), ), ), 5: Field( @@ -8580,6 +8614,19 @@ ), ), ), + SubField( + name='strava_product', + def_num=1, + type=FIELD_TYPES['strava_product'], + ref_fields=( + ReferenceField( + name='manufacturer', + def_num=0, + value='strava', + raw_value=265, + ), + ), + ), ), ), }, @@ -11446,6 +11493,19 @@ ), ), ), + SubField( + name='strava_product', + def_num=1, + type=FIELD_TYPES['strava_product'], + ref_fields=( + ReferenceField( + name='manufacturer', + def_num=0, + value='strava', + raw_value=265, + ), + ), + ), ), ), 2: Field( # Corresponds to file_id of scheduled workout / course. diff --git a/tests/files/strava-android-app-201.10-b1218918.fit b/tests/files/strava-android-app-201.10-b1218918.fit new file mode 100644 index 0000000000000000000000000000000000000000..24977c71663f40c8ec8b2326eed9e1713ba1703a GIT binary patch literal 78233 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 literal 0 HcmV?d00001 From 9bd14c4575d7d549d6ec890748931959542ec904 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Sun, 14 Jan 2024 09:50:09 -0500 Subject: [PATCH 12/12] Fix typo in api docs (#145) * Fix typo in api docs * Fix test that checked warning output --- docs/api.rst | 2 +- tests/test.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8d64e0f..e5db642 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -76,7 +76,7 @@ The ``FitFile`` Object try: fitfile = FitFile('/path.to/fitfile.fit') fitfile.parse() - except FitParseError, e: + except FitParseError as e: print "Error while parsing .FIT file: %s" % e sys.exit(1) diff --git a/tests/test.py b/tests/test.py index 886f21f..77082fb 100755 --- a/tests/test.py +++ b/tests/test.py @@ -74,6 +74,7 @@ def testfile(filename): class FitFileTestCase(unittest.TestCase): + def test_basic_file_with_one_record(self, endian='<'): f = FitFile(generate_fitfile(endian=endian)) f.parse() @@ -414,7 +415,11 @@ def test_mismatched_field_size(self): with warnings.catch_warnings(record=True) as w: f.parse() assert w - assert all("falling back to byte encoding" in str(x) for x in w) + assert all( + "falling back to byte encoding" in str(x) + for x in w + if x.category == UserWarning + ) self.assertEqual(len(f.messages), 11293) def test_unterminated_file(self):