python-fitparse 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fitparse/records.py ADDED
@@ -0,0 +1,418 @@
1
+ import math
2
+ import struct
3
+
4
+ from itertools import zip_longest
5
+
6
+
7
+ class RecordBase:
8
+ # namedtuple-like base class. Subclasses should must __slots__
9
+ __slots__ = ()
10
+
11
+ # TODO: switch back to namedtuple, and don't use default arguments as None
12
+ # and see if that gives us any performance improvements
13
+
14
+ def __init__(self, *args, **kwargs):
15
+ for slot_name, value in zip_longest(self.__slots__, args, fillvalue=None):
16
+ setattr(self, slot_name, value)
17
+ for slot_name, value in kwargs.items():
18
+ setattr(self, slot_name, value)
19
+
20
+
21
+ class MessageHeader(RecordBase):
22
+ __slots__ = ('is_definition', 'is_developer_data', 'local_mesg_num', 'time_offset')
23
+
24
+ def __repr__(self):
25
+ return '<MessageHeader: %s%s -- local mesg: #%d%s>' % (
26
+ 'definition' if self.is_definition else 'data',
27
+ '(developer)' if self.is_developer_data else '',
28
+ self.local_mesg_num,
29
+ ', time offset: %d' % self.time_offset if self.time_offset else '',
30
+ )
31
+
32
+
33
+ class DefinitionMessage(RecordBase):
34
+ __slots__ = ('header', 'endian', 'mesg_type', 'mesg_num', 'field_defs', 'dev_field_defs')
35
+ type = 'definition'
36
+
37
+ @property
38
+ def name(self):
39
+ return self.mesg_type.name if self.mesg_type else 'unknown_%d' % self.mesg_num
40
+
41
+ def __repr__(self):
42
+ return '<DefinitionMessage: %s (#%d) -- local mesg: #%d, field defs: [%s], dev field defs: [%s]>' % (
43
+ self.name,
44
+ self.mesg_num,
45
+ self.header.local_mesg_num,
46
+ ', '.join([fd.name for fd in self.field_defs]),
47
+ ', '.join([fd.name for fd in self.dev_field_defs]),
48
+ )
49
+
50
+
51
+ class FieldDefinition(RecordBase):
52
+ __slots__ = ('field', 'def_num', 'base_type', 'size')
53
+
54
+ @property
55
+ def name(self):
56
+ return self.field.name if self.field else 'unknown_%d' % self.def_num
57
+
58
+ @property
59
+ def type(self):
60
+ return self.field.type if self.field else self.base_type
61
+
62
+ def __repr__(self):
63
+ return '<FieldDefinition: %s (#%d) -- type: %s (%s), size: %d byte%s>' % (
64
+ self.name,
65
+ self.def_num,
66
+ self.type.name, self.base_type.name,
67
+ self.size, 's' if self.size != 1 else '',
68
+ )
69
+
70
+
71
+ class DevFieldDefinition(RecordBase):
72
+ __slots__ = ('field', 'dev_data_index', 'base_type', 'def_num', 'size')
73
+
74
+ def __init__(self, **kwargs):
75
+ super().__init__(**kwargs)
76
+ # For dev fields, the base_type and type are always the same.
77
+ self.base_type = self.type
78
+
79
+ @property
80
+ def name(self):
81
+ return self.field.name if self.field else 'unknown_dev_%d_%d' % (self.dev_data_index, self.def_num)
82
+
83
+ @property
84
+ def type(self):
85
+ return self.field.type
86
+
87
+ def __repr__(self):
88
+ return '<DevFieldDefinition: %s:%s (#%d) -- type: %s, size: %d byte%s>' % (
89
+ self.name,
90
+ self.dev_data_index,
91
+ self.def_num,
92
+ self.type.name,
93
+ self.size, 's' if self.size != 1 else '',
94
+ )
95
+
96
+
97
+ class DataMessage(RecordBase):
98
+ __slots__ = ('header', 'def_mesg', 'fields')
99
+ type = 'data'
100
+
101
+ def get(self, field_name, as_dict=False):
102
+ # SIMPLIFY: get rid of as_dict
103
+ for field_data in self.fields:
104
+ if field_data.is_named(field_name):
105
+ return field_data.as_dict() if as_dict else field_data
106
+
107
+ def get_raw_value(self, field_name):
108
+ field_data = self.get(field_name)
109
+ if field_data:
110
+ return field_data.raw_value
111
+ return None
112
+
113
+ def get_value(self, field_name):
114
+ # SIMPLIFY: get rid of this completely
115
+ field_data = self.get(field_name)
116
+ if field_data:
117
+ return field_data.value
118
+
119
+ def get_values(self):
120
+ # SIMPLIFY: get rid of this completely
121
+ return {f.name if f.name else f.def_num: f.value for f in self.fields}
122
+
123
+ @property
124
+ def name(self):
125
+ return self.def_mesg.name
126
+
127
+ @property
128
+ def mesg_num(self):
129
+ # SIMPLIFY: get rid of this
130
+ return self.def_mesg.mesg_num
131
+
132
+ @property
133
+ def mesg_type(self):
134
+ # SIMPLIFY: get rid of this
135
+ return self.def_mesg.mesg_type
136
+
137
+ def as_dict(self):
138
+ # TODO: rethink this format
139
+ return {
140
+ 'name': self.name,
141
+ 'fields': [f.as_dict() for f in self.fields],
142
+ }
143
+
144
+ def __iter__(self):
145
+ # Sort by whether this is a known field, then its name
146
+ return iter(sorted(self.fields, key=lambda fd: (int(fd.field is None), fd.name)))
147
+
148
+ def __repr__(self):
149
+ return '<DataMessage: %s (#%d) -- local mesg: #%d, fields: [%s]>' % (
150
+ self.name, self.mesg_num, self.header.local_mesg_num,
151
+ ', '.join([f"{fd.name}: {fd.value}" for fd in self.fields]),
152
+ )
153
+
154
+ def __str__(self):
155
+ # SIMPLIFY: get rid of this
156
+ return '%s (#%d)' % (self.name, self.mesg_num)
157
+
158
+
159
+ class FieldData(RecordBase):
160
+ __slots__ = ('field_def', 'field', 'parent_field', 'value', 'raw_value', 'units')
161
+
162
+ def __init__(self, *args, **kwargs):
163
+ super().__init__(self, *args, **kwargs)
164
+ if not self.units and self.field:
165
+ # Default to units on field, otherwise None.
166
+ # NOTE:Not a property since you may want to override this in a data processor
167
+ self.units = self.field.units
168
+
169
+ @property
170
+ def name(self):
171
+ return self.field.name if self.field else 'unknown_%d' % self.def_num
172
+
173
+ # TODO: Some notion of flags
174
+
175
+ def is_named(self, name):
176
+ if self.field:
177
+ if name in (self.field.name, self.field.def_num):
178
+ return True
179
+ if self.parent_field:
180
+ if name in (self.parent_field.name, self.parent_field.def_num):
181
+ return True
182
+ if self.field_def:
183
+ if name == self.field_def.def_num:
184
+ return True
185
+ return False
186
+
187
+ @property
188
+ def def_num(self):
189
+ # Prefer to return the def_num on the field
190
+ # since field_def may be None if this field is dynamic
191
+ return self.field.def_num if self.field else self.field_def.def_num
192
+
193
+ @property
194
+ def base_type(self):
195
+ # Try field_def's base type, if it doesn't exist, this is a
196
+ # dynamically added field, so field doesn't be None
197
+ return self.field_def.base_type if self.field_def else self.field.base_type
198
+
199
+ @property
200
+ def is_base_type(self):
201
+ return self.field.is_base_type if self.field else True
202
+
203
+ @property
204
+ def type(self):
205
+ return self.field.type if self.field else self.base_type
206
+
207
+ @property
208
+ def field_type(self):
209
+ return self.field.field_type if self.field else 'field'
210
+
211
+ def as_dict(self):
212
+ return {
213
+ 'name': self.name, 'def_num': self.def_num, 'base_type': self.base_type.name,
214
+ 'type': self.type.name, 'units': self.units, 'value': self.value,
215
+ 'raw_value': self.raw_value,
216
+ }
217
+
218
+ def __repr__(self):
219
+ return '<FieldData: %s: %s%s, def num: %d, type: %s (%s), raw value: %s>' % (
220
+ self.name, self.value, ' [%s]' % self.units if self.units else '',
221
+ self.def_num, self.type.name, self.base_type.name, self.raw_value,
222
+ )
223
+
224
+ def __str__(self):
225
+ return '{}: {}{}'.format(
226
+ self.name, self.value, ' [%s]' % self.units if self.units else '',
227
+ )
228
+
229
+
230
+ class BaseType(RecordBase):
231
+ __slots__ = ('name', 'identifier', 'fmt', 'parse')
232
+ values = None # In case we're treated as a FieldType
233
+
234
+ @property
235
+ def size(self):
236
+ return struct.calcsize(self.fmt)
237
+
238
+ @property
239
+ def type_num(self):
240
+ return self.identifier & 0x1F
241
+
242
+ def __repr__(self):
243
+ return '<BaseType: %s (#%d [0x%X])>' % (
244
+ self.name, self.type_num, self.identifier,
245
+ )
246
+
247
+
248
+ class FieldType(RecordBase):
249
+ __slots__ = ('name', 'base_type', 'values')
250
+
251
+ def __repr__(self):
252
+ return f'<FieldType: {self.name} ({self.base_type})>'
253
+
254
+
255
+ class MessageType(RecordBase):
256
+ __slots__ = ('name', 'mesg_num', 'fields')
257
+
258
+ def __repr__(self):
259
+ return '<MessageType: %s (#%d)>' % (self.name, self.mesg_num)
260
+
261
+
262
+ class FieldAndSubFieldBase(RecordBase):
263
+ __slots__ = ()
264
+
265
+ @property
266
+ def base_type(self):
267
+ return self.type if self.is_base_type else self.type.base_type
268
+
269
+ @property
270
+ def is_base_type(self):
271
+ return isinstance(self.type, BaseType)
272
+
273
+ def render(self, raw_value):
274
+ if self.type.values and (raw_value in self.type.values):
275
+ return self.type.values[raw_value]
276
+ return raw_value
277
+
278
+
279
+ class Field(FieldAndSubFieldBase):
280
+ __slots__ = ('name', 'type', 'def_num', 'scale', 'offset', 'units', 'components', 'subfields')
281
+ field_type = 'field'
282
+
283
+
284
+ class SubField(FieldAndSubFieldBase):
285
+ __slots__ = ('name', 'def_num', 'type', 'scale', 'offset', 'units', 'components', 'ref_fields')
286
+ field_type = 'subfield'
287
+
288
+
289
+ class DevField(FieldAndSubFieldBase):
290
+ __slots__ = ('dev_data_index', 'def_num', 'type', 'name', 'units', 'native_field_num',
291
+ # The rest of these are just to be compatible with Field objects. They're always None
292
+ 'scale', 'offset', 'components', 'subfields')
293
+ field_type = 'devfield'
294
+
295
+
296
+ class ReferenceField(RecordBase):
297
+ __slots__ = ('name', 'def_num', 'value', 'raw_value')
298
+
299
+
300
+ class ComponentField(RecordBase):
301
+ __slots__ = ('name', 'def_num', 'scale', 'offset', 'units', 'accumulate', 'bits', 'bit_offset')
302
+ field_type = 'component'
303
+
304
+ def render(self, raw_value):
305
+ if raw_value is None:
306
+ return None
307
+
308
+ # If it's a tuple, then it's a byte array and unpack it as such
309
+ # (only type that uses this is compressed speed/distance)
310
+ if isinstance(raw_value, tuple):
311
+ # Profile.xls sometimes contains more components than the read raw
312
+ # value is able to hold (typically the *event_timestamp_12* field in
313
+ # *hr* messages).
314
+ # This test allows to ensure *unpacked_num* is not right-shifted
315
+ # more than necessary.
316
+ if self.bit_offset and self.bit_offset >= len(raw_value) << 3:
317
+ raise ValueError()
318
+
319
+ unpacked_num = 0
320
+
321
+ # Unpack byte array as little endian
322
+ for value in reversed(raw_value):
323
+ unpacked_num = (unpacked_num << 8) + value
324
+
325
+ raw_value = unpacked_num
326
+
327
+ # Mask and shift like a normal number
328
+ if isinstance(raw_value, int):
329
+ raw_value = (raw_value >> self.bit_offset) & ((1 << self.bits) - 1)
330
+
331
+ return raw_value
332
+
333
+
334
+ class Crc:
335
+ """FIT file CRC computation."""
336
+
337
+ CRC_TABLE = (
338
+ 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
339
+ 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
340
+ )
341
+
342
+ FMT = 'H'
343
+
344
+ def __init__(self, value=0, byte_arr=None):
345
+ self.value = value
346
+ if byte_arr:
347
+ self.update(byte_arr)
348
+
349
+ def __repr__(self):
350
+ return '<{} {}>'.format(self.__class__.__name__, self.value or "-")
351
+
352
+ def __str__(self):
353
+ return self.format(self.value)
354
+
355
+ def update(self, byte_arr):
356
+ """Read bytes and update the CRC computed."""
357
+ if byte_arr:
358
+ self.value = self.calculate(byte_arr, self.value)
359
+
360
+ @staticmethod
361
+ def format(value):
362
+ """Format CRC value to string."""
363
+ return '0x%04X' % value
364
+
365
+ @classmethod
366
+ def calculate(cls, byte_arr, crc=0):
367
+ """Compute CRC for input bytes."""
368
+ for byte in byte_arr:
369
+ # Taken verbatim from FIT SDK docs
370
+ tmp = cls.CRC_TABLE[crc & 0xF]
371
+ crc = (crc >> 4) & 0x0FFF
372
+ crc = crc ^ tmp ^ cls.CRC_TABLE[byte & 0xF]
373
+
374
+ tmp = cls.CRC_TABLE[crc & 0xF]
375
+ crc = (crc >> 4) & 0x0FFF
376
+ crc = crc ^ tmp ^ cls.CRC_TABLE[(byte >> 4) & 0xF]
377
+ return crc
378
+
379
+
380
+ def parse_string(string):
381
+ try:
382
+ s = string[:string.index(0x00)]
383
+ except ValueError:
384
+ # FIT specification defines the 'string' type as follows: "Null
385
+ # terminated string encoded in UTF-8 format".
386
+ #
387
+ # However 'string' values are not always null-terminated when encoded,
388
+ # according to FIT files created by Garmin devices (e.g. DEVICE.FIT file
389
+ # from a fenix3).
390
+ #
391
+ # So in order to be more flexible, in case index() could not find any
392
+ # null byte, we just decode the whole bytes-like object.
393
+ s = string
394
+
395
+ return s.decode(encoding='utf-8', errors='replace') or None
396
+
397
+ # The default base type
398
+ BASE_TYPE_BYTE = BaseType(name='byte', identifier=0x0D, fmt='B', parse=lambda x: None if all(b == 0xFF for b in x) else x)
399
+
400
+ BASE_TYPES = {
401
+ 0x00: BaseType(name='enum', identifier=0x00, fmt='B', parse=lambda x: None if x == 0xFF else x),
402
+ 0x01: BaseType(name='sint8', identifier=0x01, fmt='b', parse=lambda x: None if x == 0x7F else x),
403
+ 0x02: BaseType(name='uint8', identifier=0x02, fmt='B', parse=lambda x: None if x == 0xFF else x),
404
+ 0x83: BaseType(name='sint16', identifier=0x83, fmt='h', parse=lambda x: None if x == 0x7FFF else x),
405
+ 0x84: BaseType(name='uint16', identifier=0x84, fmt='H', parse=lambda x: None if x == 0xFFFF else x),
406
+ 0x85: BaseType(name='sint32', identifier=0x85, fmt='i', parse=lambda x: None if x == 0x7FFFFFFF else x),
407
+ 0x86: BaseType(name='uint32', identifier=0x86, fmt='I', parse=lambda x: None if x == 0xFFFFFFFF else x),
408
+ 0x07: BaseType(name='string', identifier=0x07, fmt='s', parse=parse_string),
409
+ 0x88: BaseType(name='float32', identifier=0x88, fmt='f', parse=lambda x: None if math.isnan(x) else x),
410
+ 0x89: BaseType(name='float64', identifier=0x89, fmt='d', parse=lambda x: None if math.isnan(x) else x),
411
+ 0x0A: BaseType(name='uint8z', identifier=0x0A, fmt='B', parse=lambda x: None if x == 0x0 else x),
412
+ 0x8B: BaseType(name='uint16z', identifier=0x8B, fmt='H', parse=lambda x: None if x == 0x0 else x),
413
+ 0x8C: BaseType(name='uint32z', identifier=0x8C, fmt='I', parse=lambda x: None if x == 0x0 else x),
414
+ 0x0D: BASE_TYPE_BYTE,
415
+ 0x8E: BaseType(name='sint64', identifier=0x8E, fmt='q', parse=lambda x: None if x == 0x7FFFFFFFFFFFFFFF else x),
416
+ 0x8F: BaseType(name='uint64', identifier=0x8F, fmt='Q', parse=lambda x: None if x == 0xFFFFFFFFFFFFFFFF else x),
417
+ 0x90: BaseType(name='uint64z', identifier=0x90, fmt='Q', parse=lambda x: None if x == 0 else x),
418
+ }
fitparse/utils.py ADDED
@@ -0,0 +1,67 @@
1
+ import io
2
+ import re
3
+ from collections.abc import Iterable
4
+
5
+ from pathlib import PurePath
6
+
7
+
8
+ class FitParseError(ValueError):
9
+ pass
10
+
11
+ class FitEOFError(FitParseError):
12
+ pass
13
+
14
+ class FitCRCError(FitParseError):
15
+ pass
16
+
17
+ class FitHeaderError(FitParseError):
18
+ pass
19
+
20
+
21
+ METHOD_NAME_SCRUBBER = re.compile(r'\W|^(?=\d)')
22
+ UNIT_NAME_TO_FUNC_REPLACEMENTS = (
23
+ ('/', ' per '),
24
+ ('%', 'percent'),
25
+ ('*', ' times '),
26
+ )
27
+
28
+ def scrub_method_name(method_name, convert_units=False):
29
+ if convert_units:
30
+ for replace_from, replace_to in UNIT_NAME_TO_FUNC_REPLACEMENTS:
31
+ method_name = method_name.replace(
32
+ replace_from, '%s' % replace_to,
33
+ )
34
+ return METHOD_NAME_SCRUBBER.sub('_', method_name)
35
+
36
+
37
+ def fileish_open(fileish, mode):
38
+ """
39
+ Convert file-ish object to BytesIO like object.
40
+ :param fileish: the file-ihs object (str, BytesIO, bytes, file contents)
41
+ :param str mode: mode for the open function.
42
+ :rtype: BytesIO
43
+ """
44
+ if mode is not None and any(m in mode for m in ['+', 'w', 'a', 'x']):
45
+ attr = 'write'
46
+ else:
47
+ attr = 'read'
48
+ if hasattr(fileish, attr) and hasattr(fileish, 'seek'):
49
+ # BytesIO-like object
50
+ return fileish
51
+ elif isinstance(fileish, str):
52
+ # file path
53
+ return open(fileish, mode)
54
+
55
+ # pathlib obj
56
+ if isinstance(fileish, PurePath):
57
+ return fileish.open(mode)
58
+
59
+ # file contents
60
+ return io.BytesIO(fileish)
61
+
62
+
63
+ def is_iterable(obj):
64
+ """Check, if the obj is iterable but not string or bytes.
65
+ :rtype bool"""
66
+ # Speed: do not use iter() although it's more robust, see also https://stackoverflow.com/questions/1952464/
67
+ return isinstance(obj, Iterable) and not isinstance(obj, (str, bytes))
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-fitparse
3
+ Version: 2.0.0
4
+ Summary: Python library to parse ANT/Garmin .FIT files
5
+ Project-URL: Homepage, https://www.github.com/nbr23/python-fitparse
6
+ Project-URL: Repository, https://www.github.com/nbr23/python-fitparse
7
+ Project-URL: Issues, https://github.com/nbr23/python-fitparse/issues
8
+ Author-email: David Cooper <dave@kupesoft.com>, nbr23 <max@23.tf>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2011-2025, David Cooper <david@dtcooper.com>
12
+ Copyright (c) 2017-2025, Carey Metcalfe <carey@cmetcalfe.ca>
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: ant,files,fit,garmin,parse
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.8
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.8
44
+ Provides-Extra: generate
45
+ Requires-Dist: openpyxl==3.1.5; extra == 'generate'
46
+ Requires-Dist: requests; extra == 'generate'
47
+ Description-Content-Type: text/markdown
48
+
49
+ python-fitparse
50
+ ===============
51
+
52
+ > :warning: **NOTE:** *I have **limited to no time** to work on this package
53
+ > these days!*
54
+ >
55
+ > I am looking for a maintainer to help with issues and updating/releasing the package.
56
+ > Please reach out via email at <david@dtcooper.com> if you have interest in helping.
57
+ >
58
+ > If you're having trouble using this package for whatever reason, might we suggest using
59
+ > an alternative library: [fitdecode](https://github.com/polyvertex/fitdecode) by
60
+ > [polyvertex](https://github.com/polyvertex).
61
+ >
62
+ > Cheers,
63
+ >
64
+ > David
65
+
66
+ Here's a Python library to parse ANT/Garmin `.FIT` files.
67
+ [![Build Status](https://github.com/dtcooper/python-fitparse/workflows/test/badge.svg)](https://github.com/dtcooper/python-fitparse/actions?query=workflow%3Atest)
68
+
69
+
70
+ Install from [![PyPI](https://img.shields.io/pypi/v/fitparse.svg)](https://pypi.python.org/pypi/fitparse/):
71
+ ```
72
+ pip install fitparse
73
+ ```
74
+
75
+ FIT files
76
+ ------------
77
+ - FIT files contain data stored in a binary file format.
78
+ - The FIT (Flexible and Interoperable Data Transfer) file protocol is specified
79
+ by [ANT](http://www.thisisant.com/).
80
+ - The SDK, code examples, and detailed documentation can be found in the
81
+ [ANT FIT SDK](http://www.thisisant.com/resources/fit).
82
+
83
+
84
+ Usage
85
+ -----
86
+ A simple example of printing records from a fit file:
87
+
88
+ ```python
89
+ import fitparse
90
+
91
+ # Load the FIT file
92
+ fitfile = fitparse.FitFile("my_activity.fit")
93
+
94
+ # Iterate over all messages of type "record"
95
+ # (other types include "device_info", "file_creator", "event", etc)
96
+ for record in fitfile.get_messages("record"):
97
+
98
+ # Records can contain multiple pieces of data (ex: timestamp, latitude, longitude, etc)
99
+ for data in record:
100
+
101
+ # Print the name and value of the data (and the units if it has any)
102
+ if data.units:
103
+ print(" * {}: {} ({})".format(data.name, data.value, data.units))
104
+ else:
105
+ print(" * {}: {}".format(data.name, data.value))
106
+
107
+ print("---")
108
+ ```
109
+
110
+ The library also provides a `fitdump` script for command line usage:
111
+ ```
112
+ $ fitdump --help
113
+ usage: fitdump [-h] [-v] [-o OUTPUT] [-t {readable,json}] [-n NAME] [--ignore-crc] FITFILE
114
+
115
+ Dump .FIT files to various formats
116
+
117
+ positional arguments:
118
+ FITFILE Input .FIT file (Use - for stdin)
119
+
120
+ optional arguments:
121
+ -h, --help show this help message and exit
122
+ -v, --verbose
123
+ -o OUTPUT, --output OUTPUT
124
+ File to output data into (defaults to stdout)
125
+ -t {readable,json}, --type {readable,json}
126
+ File type to output. (DEFAULT: readable)
127
+ -n NAME, --name NAME Message name (or number) to filter
128
+ --ignore-crc Some devices can write invalid crc's, ignore these.
129
+ ```
130
+
131
+ See the documentation for more: http://dtcooper.github.io/python-fitparse
132
+
133
+
134
+ Major Changes From Original Version
135
+ -----------------------------------
136
+
137
+ After a few years of laying dormant we are back to active development!
138
+ The old version is archived as
139
+ [`v1-archive`](https://github.com/dtcooper/python-fitparse/releases/tag/v1-archive).
140
+
141
+ * New, hopefully cleaner public API with a clear division between accessible
142
+ and internal parts. (Still unstable and partially complete.)
143
+
144
+ * Proper documentation!
145
+ [Available here](https://dtcooper.github.io/python-fitparse/).
146
+
147
+ * Unit tests and example programs.
148
+
149
+ * **(WIP)** Command line tools (eg a `.FIT` to `.CSV` converter).
150
+
151
+ * Component fields and compressed timestamp headers now supported and not
152
+ just an afterthought. Closes issues #6 and #7.
153
+
154
+ * FIT file parsing is generic enough to support all types. Going to have
155
+ specific `FitFile` subclasses for more popular file types like activities.
156
+
157
+ * **(WIP)** Converting field types to normalized values (for example,
158
+ `bool`, `date_time`, etc) done in a consistent way, that's easy to
159
+ customize by subclassing the converter class. I'm going to use something
160
+ like the Django form-style `convert_<field name>` idiom on this class.
161
+
162
+ * The FIT profile is its own complete python module, rather than using
163
+ `profile.def`.
164
+
165
+ * Bonus! The profile generation script is _less_ ugly (but still an
166
+ atrocity) and supports every
167
+ [ANT FIT SDK](http://www.thisisant.com/resources/fit) from version 1.00
168
+ up to 5.10.
169
+
170
+ * A working `setup.py` module. Closes issue #2, finally! I'll upload the
171
+ package to [PyPI](http://pypi.python.org/) when it's done.
172
+
173
+ * Support for parsing one record at a time. This can be done using
174
+ `<FitFile>.parse_one()` for now, but I'm not sure of the exact
175
+ implementation yet.
176
+
177
+
178
+ Updating to new FIT SDK versions
179
+ --------------------------------
180
+ - Download the latest [ANT FIT SDK](http://www.thisisant.com/resources/fit).
181
+ - Update the profile:
182
+ ```
183
+ python3 scripts/generate_profile.py /path/to/fit_sdk.zip fitparse/profile.py
184
+ ```
185
+
186
+
187
+ License
188
+ -------
189
+
190
+ This project is licensed under the MIT License - see the [`LICENSE`](LICENSE)
191
+ file for details.
@@ -0,0 +1,11 @@
1
+ fitparse/__init__.py,sha256=XigqA8TTG1j9B3PyV7PfAfJhVL2NQVGQrMtfmElEuNM,336
2
+ fitparse/base.py,sha256=9Prisb5xfPGM79kszvfATVIgYMC3gWnHWaX1kqoHmAQ,24202
3
+ fitparse/processors.py,sha256=gQNdFiT5xVn3R9uX-cjijL2JQ3cKtuygjAPKIZ7J53Q,5357
4
+ fitparse/profile.py,sha256=WVsU060VBVSQfJzMHojHG6GSmPK88OT4VGgRVAs0910,582980
5
+ fitparse/records.py,sha256=bjiuG6P2SrRHxmGWjcNbj29nnM6UAl9sw-toR2rFudk,14396
6
+ fitparse/utils.py,sha256=cYmAKRktsiTKpxAAvp9XwjfN2BKzEP9-0fAKoTTu_Sw,1750
7
+ python_fitparse-2.0.0.dist-info/METADATA,sha256=avMAmoxsvuC60-1pD8xXAbiPIHl9n6ugGReVW04ZbXE,7408
8
+ python_fitparse-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ python_fitparse-2.0.0.dist-info/entry_points.txt,sha256=cM-FrZMgY175Od7pYZX7yu48XDynph8o2w-FBOBIyRU,49
10
+ python_fitparse-2.0.0.dist-info/licenses/LICENSE,sha256=RK-rKfCYWql9q58dgvYwpDuo-N4g0L4iXxr8D5ubMIo,1157
11
+ python_fitparse-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fitdump = scripts.fitdump:main