ssscoring.flysight

Functions and logic for detecting, validating and manipulating FlySight CSV files, including detection in the file system. The functions in this module assume that a data lake exists somewhere in the file system (whether local or cloud-based).

  1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
  2
  3"""
  4Functions and logic for detecting, validating and manipulating
  5FlySight CSV files, including detection in the file system.  The functions in
  6this module assume that a data lake exists somewhere in the file system (whether
  7local or cloud-based).
  8"""
  9
 10
 11from collections import OrderedDict
 12from io import StringIO
 13from pathlib import Path
 14
 15from ssscoring.constants import FLYSIGHT_1_HEADER
 16from ssscoring.constants import FLYSIGHT_2_HEADER
 17from ssscoring.constants import FLYSIGHT_FILE_ENCODING
 18from ssscoring.constants import INSIGHT_1_HEADER
 19from ssscoring.constants import IGNORE_LIST
 20from ssscoring.constants import MIN_JUMP_FILE_SIZE
 21from ssscoring.datatypes import FlySightVersion
 22from ssscoring.errors import SSScoringError
 23
 24import csv
 25import os
 26import shutil
 27import tempfile
 28
 29import pandas as pd
 30
 31
 32# +++ functions +++
 33
 34def _isInsightHeader(header: list) -> bool:
 35    return INSIGHT_1_HEADER.issubset(set(header)) and 'cAcc' not in header
 36
 37
 38def isCRMangledCSV(fileThing) -> bool:
 39    """
 40    Tests if `fileThing` is an Excel or Dropbox DOS file with lines terminated
 41    in CRCRLF.  These occur when someone opens the file with Excel or some other
 42    tool in a Windows system and saves the file back to the file system,
 43    mangling the original format.
 44
 45    Arguments
 46    ---------
 47        fileThing
 48    A string or `pathlib.Path` object associated with what looks like a FlySight
 49    CR mangled file.
 50
 51    Returns
 52    -------
 53    `True` if the file has one or more lines ending in CRCRLF within the first
 54    512 bytes of data.
 55    """
 56    with open (fileThing, 'rb') as file:
 57        rawData = file.read()
 58        return b'\r\r\n' in rawData
 59
 60
 61def fixCRMangledCSV(fileThing):
 62    """
 63    Open the file associated with `fileThing` and repleace all`\r\r\b` with
 64    `\r\n` EOL markers.
 65
 66    Arguments
 67    ---------
 68        fileThing
 69    A string or `pathlib.Path` object associated with what looks like a FlySight
 70    CR mangled file.
 71
 72    See
 73    ---
 74    `ssscoring.flysight.isCRMangledCSV`
 75    """
 76    with open(fileThing, 'rb') as inputFile:
 77        fileContents = inputFile.read()
 78    fileContents = fileContents.replace(b'\r\r\n', b'\r\n')
 79    with tempfile.NamedTemporaryFile(delete = False) as outputFile:
 80        outputFile.write(fileContents)
 81        tempFileName = outputFile.name
 82    shutil.copy(tempFileName, fileThing)
 83    os.unlink(tempFileName)
 84
 85
 86def skipOverFS2MetadataRowsIn(data: pd.DataFrame) -> pd.DataFrame:
 87    """
 88    Returns a clean dataframe on which any metadata rows within the first 100
 89    are skipped.  This function uses the `time` column to detect valid rows.  A
 90    `time == NaN` is considered invalid and skipped.
 91
 92    Arguments
 93    ---------
 94        data
 95    A FlySight 2 dataframe suspected of having dirty N first rows with metadata
 96
 97    Returns
 98    -------
 99    A FlySight 2 clean dataframe without any leading metadata rows.
100    """
101    for ref in range(0,100):
102        if pd.notnull(data.iloc[ref].time):
103            break
104    return data.iloc[ref:]
105
106
107def validFlySightHeaderIn(fileThingCSV) -> bool:
108    """
109    Checks if a file is a CSV in FlySight 1 or FlySight 2 formats.  The checks
110    include:
111
112    - Whether the file is a CSV, using a comma delimiter
113    - Checks for the presence of all the documented FlySight 1 headers
114    - Checks for the presence of the FlySight 2 line 1 identifier
115
116    Arguments
117    ---------
118        fileThingCSV
119    A file thing to verify as a valid FlySight file; can be a string, an
120    instance of `libpath.Path`, or a buffer of `bytes`.
121
122    Returns
123    -------
124    `True` if `fileThingCSV` is a FlySight CSV file, otherwise `False`.
125    """
126    delimiters = [',']
127
128    if isinstance(fileThingCSV, bytes):
129        stream = StringIO(fileThingCSV.decode(FLYSIGHT_FILE_ENCODING))
130    else:
131        stream = open(fileThingCSV, 'r')
132
133    with stream:
134        try:
135            dialect = csv.Sniffer().sniff(stream.readline(), delimiters=delimiters)
136        except csv.Error:
137            return False
138
139        if dialect.delimiter not in delimiters:
140            return False
141        stream.seek(0)
142        try:
143            header = next(csv.reader(stream))
144        except StopIteration:
145            return False
146    return header[0] == '$FLYS' or FLYSIGHT_1_HEADER.issubset(header) or _isInsightHeader(header)
147
148
149def getAllSpeedJumpFilesFrom(dataLake: Path) -> dict:
150    """
151    Get a list of all the speed jump files from a data lake, where data lake is
152    defined as a reachable path that contains one or more FlySight CSV files.
153    This function tests each file to ensure that it's a speed skydive FlySight
154    file in a valid format and length.  It doesn't validate data like versions
155    prior to 1.9.0.
156
157    Arguments
158    ---------
159        dataLake: str
160    A valid (absolute or relative) path name to the top level directory where
161    the data lake starts.
162
163    Returns
164    -------
165    A dictionary of speed jump file names for later SSScoring processing:
166        - keys are the file names
167        - values are a FlySight version string tag
168    """
169    jumpFiles = OrderedDict()
170    for root, dirs, files in os.walk(dataLake):
171        if any(name in root for name in IGNORE_LIST):
172            continue
173        for fileName in files:
174            data = None
175            if '.swp' in fileName: # Ignore Vim, other editors swap file
176                continue
177            if '.CSV' in fileName.upper():
178                version = '1'
179                jumpFileName = Path(root) / fileName
180                stat = os.stat(jumpFileName)
181                if all(x not in fileName for x in ('EVENT', 'SENSOR', 'TRACK')):
182                    # FlySight 1 or Insight track format
183                    data = pd.read_csv(jumpFileName, skiprows = (1, 1), index_col = False)
184                    if data is not None and 'headAcc' in data.columns:
185                        version = 'i'
186                elif 'TRACK' in fileName:
187                    # FlySight 2 track custom format
188                    data = pd.read_csv(jumpFileName, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False, na_values = ['NA', ])
189                    data = skipOverFS2MetadataRowsIn(data)
190                    data.drop('GNSS', inplace = True, axis = 1)
191                    version = '2'
192                if data is not None and stat.st_size >= MIN_JUMP_FILE_SIZE and validFlySightHeaderIn(jumpFileName):
193                    # explicit because `not data` is ambiguous for dataframes
194                    jumpFiles[jumpFileName] = version
195    jumpFiles = OrderedDict(sorted(jumpFiles.items()))
196    return jumpFiles
197
198
199def detectFlySightFileVersionOf(fileThing) -> FlySightVersion:
200    """
201    Detects the FlySight file version based on its file name and format.
202
203    Arguments
204    ---------
205        fileThing
206    A string, `bytes` buffer or `pathlib.Path` object corresponding to track
207    file.  If string or `pathlib.Path`, it'll be treated as a file.
208
209    Returns
210    -------
211    An instance of `ssscoring.flysight.FlySightVersion` with a valid version
212    symbolic value.
213
214    Errors
215    ------
216    `ssscoring.errors.SSScoringError` if the file is not a CSV and it's some
217    other invalid format.
218    """
219    match fileThing:
220        case Path():
221            fileName = fileThing.as_posix()
222        case str():
223            fileName = fileThing
224            fileThing = Path(fileThing)
225        case bytes():
226            fileName = '00-00-00.CSV'
227        case _:
228            raise SSScoringError('fileThing must be a Path, str, or bytes')
229
230    delimiters =  [',', ]
231    stream = None
232    if not '.CSV' in fileName.upper():
233        raise SSScoringError('Invalid file extension type')
234    if any(x in fileName for x in ('EVENT.CSV', 'SENSOR.CSV')):
235        raise SSScoringError('Only TRACK.CSV v2 files can be processed at this time')
236    if isinstance(fileThing, Path) or isinstance(fileThing, str):
237        if not fileThing.is_file():
238            raise SSScoringError('%s - file not found in data lake' % fileName)
239        if not validFlySightHeaderIn(fileName):
240            raise SSScoringError('CSV is not a valid FlySight file')
241        stream = open(fileName, 'r')
242    elif isinstance(fileThing, bytes):
243        stream = StringIO(fileThing.decode(FLYSIGHT_FILE_ENCODING))
244
245    try:
246        dialect = csv.Sniffer().sniff(stream.readline(), delimiters = delimiters)
247    except:
248        raise SSScoringError('Error while trying to validate %s file format' % fileName)
249    if dialect.delimiter in delimiters:
250        stream.seek(0)
251        header = next(csv.reader(stream))
252    else:
253        raise SSScoringError('CSV uses a different delimiter from FlySigh')
254    if header[0] == '$FLYS':
255        return FlySightVersion.V2
256    elif FLYSIGHT_1_HEADER.issubset(header):
257        return FlySightVersion.V1
258    elif _isInsightHeader(header):
259        return FlySightVersion.INSIGHT
260    else:
261        raise SSScoringError('%s file is not a FlySight v1 or v2 file')
262
263
264def readVersion1CSV(fileThing: object) -> pd.DataFrame:
265    """
266    Read a FlySight file version 1 into a dataframe.  It scrubes blank rows that
267    get in the way of correct parsing.
268
269    Arguments
270    ---------
271        fileThing
272    A string or a `pathlib.Path` object.  It can be a relative or an absolute
273    path.
274
275    Returns
276    -------
277    A FlySight dataframe with the original column names, normalized for
278    manipulation as a dataframe instead of a file or CSV object.
279    """
280    return pd.read_csv(fileThing, skiprows = (1, 1), index_col = False)
281
282
283def _tagVersion1From(fileThing: str) -> str:
284    return fileThing.replace('.CSV', '').replace('.csv', '').replace('/data', '').replace('/', ' ').strip()+':v1'
285
286
287def _tagFromFirstTimestampIn(rawData: pd.DataFrame, suffix: str) -> str:
288    firstTimestamp = str(rawData.iloc[0]['time'])
289    return firstTimestamp.split('T')[1].split('.')[0].replace(':', '-')+':'+suffix
290
291
292def _tagVersion2From(rawData: pd.DataFrame) -> str:
293    return _tagFromFirstTimestampIn(rawData, 'v2')
294
295
296def _tagInsightFrom(rawData: pd.DataFrame) -> str:
297    return _tagFromFirstTimestampIn(rawData, 'i')
298
299
300def readInsightCSV(fileThing: object) -> pd.DataFrame:
301    rawData = pd.read_csv(fileThing, skiprows=(1, 1), index_col=False)
302    rawData.drop('headAcc', inplace=True, axis=1)
303    return rawData
304
305
306def readVersion2CSV(jumpFile: str) -> pd.DataFrame:
307    """
308    Read a FlySight file version 2 into a dataframe.  It scrubes blank rows that
309    get in the way of correct parsing and drops the `GNSS` column because it
310    just makes dataframe management murkier.
311
312    Arguments
313    ---------
314        fileThing
315    A string or a `pathlib.Path` object.  It can be a relative or an absolute
316    path.
317
318    Returns
319    -------
320    A FlySight dataframe with the original column names, normalized for
321    manipulation as a dataframe instead of a file or CSV object.
322    """
323
324    rawData = pd.read_csv(jumpFile, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False, na_values=['NA',])
325    rawData = skipOverFS2MetadataRowsIn(rawData)
326    rawData.drop('GNSS', inplace = True, axis = 1)
327    return rawData
328
329
330def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple:
331    """
332    Ingress a buffer with known FlySight or SkyTrax file data for SSScoring
333    processing.
334
335    Arguments
336    ---------
337        buffer
338    A binary data buffer, bag of bytes, containing a known FlySight track file.
339
340        bufferName
341    An arbitrary name for the buffer of type `str`.  Used to construct the tag
342    for FlySight 1 buffers; ignored for FlySight 2 and Insight buffers, whose
343    tags are derived from the first row's timestamp.
344
345    Returns
346    -------
347    A `tuple` with two items:
348        - `rawData` - a dataframe representation of the CSV with the original
349          headers but without the data type header
350        - `tag` - an identifying string for the track.  Shape depends on the
351          device:
352            - FlySight 1: `<bufferName>:v1` - derived from `bufferName`
353            - FlySight 2: `HH-MM-ss:v2` - derived from the first GNSS row's
354              timestamp
355            - Insight: `HH-MM-ss:i` - derived from the first row's timestamp
356          Invalid files produce `<bufferName>:INVALID`.
357
358    Raises
359    ------
360    `SSScoringError` if the CSV file is invalid in any way.
361    """
362    if not isinstance(buffer, bytes):
363        raise SSScoringError('buffer must be an instance of bytes, a bytes buffer')
364    try:
365        stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING))
366    except Exception as e:
367        raise SSScoringError('invalid buffer endcoding - %s' % str(e))
368    try:
369        version = detectFlySightFileVersionOf(buffer)
370    except Exception:
371        tag = '%s:INVALID' % bufferName
372        rawData = None
373    else:
374        if version == FlySightVersion.V1:
375            rawData = readVersion1CSV(stringIO)
376            tag = _tagVersion1From(bufferName)
377        elif version == FlySightVersion.V2:
378            rawData = readVersion2CSV(stringIO)
379            tag = _tagVersion2From(rawData)
380        elif version == FlySightVersion.INSIGHT:
381            rawData = readInsightCSV(stringIO)
382            tag = _tagInsightFrom(rawData)
383    return (rawData, tag)
384
385
386def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
387    """
388    Ingress a known FlySight or SkyTrax file into memory for SSScoring
389    processing.
390
391    Arguments
392    ---------
393        jumpFile
394    A string or `pathlib.Path` object; can be a relative or an asbolute path.
395
396    Returns
397    -------
398    A `tuple` with two items:
399        - `rawData` - a dataframe representation of the CSV with the original
400          headers but without the data type header
401        - `tag` - an identifying string for the track.  Shape depends on the
402          device:
403            - FlySight 1: `<path slug>:v1` - derived from the file path
404            - FlySight 2: `HH-MM-ss:v2` - derived from the first GNSS row's
405              timestamp
406            - Insight: `HH-MM-ss:i` - derived from the first row's timestamp
407          `'NA'` if version detection fails.
408
409    Raises
410    ------
411    `SSScoringError` if the CSV file is invalid in any way.
412    """
413    if isinstance(jumpFile, Path):
414        jumpFile = jumpFile.as_posix()
415    elif isinstance(jumpFile, str):
416        pass
417    else:
418        raise SSScoringError('jumpFile must be a string or a Path object')
419    if not validFlySightHeaderIn(jumpFile):
420        raise SSScoringError('%s is an invalid speed skydiving file')
421    try:
422        version = detectFlySightFileVersionOf(jumpFile)
423    except Exception:
424        tag = 'NA'
425        rawData = None
426    else:
427        if version == FlySightVersion.V1:
428            rawData = readVersion1CSV(jumpFile)
429            tag = _tagVersion1From(jumpFile)
430        elif version == FlySightVersion.V2:
431            rawData = readVersion2CSV(jumpFile)
432            tag = _tagVersion2From(rawData)
433        elif version == FlySightVersion.INSIGHT:
434            rawData = readInsightCSV(jumpFile)
435            tag = _tagInsightFrom(rawData)
436    return (rawData, tag)
def isCRMangledCSV(fileThing) -> bool:
39def isCRMangledCSV(fileThing) -> bool:
40    """
41    Tests if `fileThing` is an Excel or Dropbox DOS file with lines terminated
42    in CRCRLF.  These occur when someone opens the file with Excel or some other
43    tool in a Windows system and saves the file back to the file system,
44    mangling the original format.
45
46    Arguments
47    ---------
48        fileThing
49    A string or `pathlib.Path` object associated with what looks like a FlySight
50    CR mangled file.
51
52    Returns
53    -------
54    `True` if the file has one or more lines ending in CRCRLF within the first
55    512 bytes of data.
56    """
57    with open (fileThing, 'rb') as file:
58        rawData = file.read()
59        return b'\r\r\n' in rawData

Tests if fileThing is an Excel or Dropbox DOS file with lines terminated in CRCRLF. These occur when someone opens the file with Excel or some other tool in a Windows system and saves the file back to the file system, mangling the original format.

Arguments

fileThing

A string or pathlib.Path object associated with what looks like a FlySight CR mangled file.

Returns

True if the file has one or more lines ending in CRCRLF within the first 512 bytes of data.

def fixCRMangledCSV(fileThing):
62def fixCRMangledCSV(fileThing):
63    """
64    Open the file associated with `fileThing` and repleace all`\r\r\b` with
65    `\r\n` EOL markers.
66
67    Arguments
68    ---------
69        fileThing
70    A string or `pathlib.Path` object associated with what looks like a FlySight
71    CR mangled file.
72
73    See
74    ---
75    `ssscoring.flysight.isCRMangledCSV`
76    """
77    with open(fileThing, 'rb') as inputFile:
78        fileContents = inputFile.read()
79    fileContents = fileContents.replace(b'\r\r\n', b'\r\n')
80    with tempfile.NamedTemporaryFile(delete = False) as outputFile:
81        outputFile.write(fileContents)
82        tempFileName = outputFile.name
83    shutil.copy(tempFileName, fileThing)
84    os.unlink(tempFileName)

Open the file associated with fileThing and repleace all`

with ` EOL markers.

Arguments
---------
    fileThing
A string or `pathlib.Path` object associated with what looks like a FlySight
CR mangled file.

See
---
`ssscoring.flysight.isCRMangledCSV`
def skipOverFS2MetadataRowsIn(data: pandas.DataFrame) -> pandas.DataFrame:
 87def skipOverFS2MetadataRowsIn(data: pd.DataFrame) -> pd.DataFrame:
 88    """
 89    Returns a clean dataframe on which any metadata rows within the first 100
 90    are skipped.  This function uses the `time` column to detect valid rows.  A
 91    `time == NaN` is considered invalid and skipped.
 92
 93    Arguments
 94    ---------
 95        data
 96    A FlySight 2 dataframe suspected of having dirty N first rows with metadata
 97
 98    Returns
 99    -------
100    A FlySight 2 clean dataframe without any leading metadata rows.
101    """
102    for ref in range(0,100):
103        if pd.notnull(data.iloc[ref].time):
104            break
105    return data.iloc[ref:]

Returns a clean dataframe on which any metadata rows within the first 100 are skipped. This function uses the time column to detect valid rows. A time == NaN is considered invalid and skipped.

Arguments

data

A FlySight 2 dataframe suspected of having dirty N first rows with metadata

Returns

A FlySight 2 clean dataframe without any leading metadata rows.

def validFlySightHeaderIn(fileThingCSV) -> bool:
108def validFlySightHeaderIn(fileThingCSV) -> bool:
109    """
110    Checks if a file is a CSV in FlySight 1 or FlySight 2 formats.  The checks
111    include:
112
113    - Whether the file is a CSV, using a comma delimiter
114    - Checks for the presence of all the documented FlySight 1 headers
115    - Checks for the presence of the FlySight 2 line 1 identifier
116
117    Arguments
118    ---------
119        fileThingCSV
120    A file thing to verify as a valid FlySight file; can be a string, an
121    instance of `libpath.Path`, or a buffer of `bytes`.
122
123    Returns
124    -------
125    `True` if `fileThingCSV` is a FlySight CSV file, otherwise `False`.
126    """
127    delimiters = [',']
128
129    if isinstance(fileThingCSV, bytes):
130        stream = StringIO(fileThingCSV.decode(FLYSIGHT_FILE_ENCODING))
131    else:
132        stream = open(fileThingCSV, 'r')
133
134    with stream:
135        try:
136            dialect = csv.Sniffer().sniff(stream.readline(), delimiters=delimiters)
137        except csv.Error:
138            return False
139
140        if dialect.delimiter not in delimiters:
141            return False
142        stream.seek(0)
143        try:
144            header = next(csv.reader(stream))
145        except StopIteration:
146            return False
147    return header[0] == '$FLYS' or FLYSIGHT_1_HEADER.issubset(header) or _isInsightHeader(header)

Checks if a file is a CSV in FlySight 1 or FlySight 2 formats. The checks include:

  • Whether the file is a CSV, using a comma delimiter
  • Checks for the presence of all the documented FlySight 1 headers
  • Checks for the presence of the FlySight 2 line 1 identifier

Arguments

fileThingCSV

A file thing to verify as a valid FlySight file; can be a string, an instance of libpath.Path, or a buffer of bytes.

Returns

True if fileThingCSV is a FlySight CSV file, otherwise False.

def getAllSpeedJumpFilesFrom(dataLake: pathlib.Path) -> dict:
150def getAllSpeedJumpFilesFrom(dataLake: Path) -> dict:
151    """
152    Get a list of all the speed jump files from a data lake, where data lake is
153    defined as a reachable path that contains one or more FlySight CSV files.
154    This function tests each file to ensure that it's a speed skydive FlySight
155    file in a valid format and length.  It doesn't validate data like versions
156    prior to 1.9.0.
157
158    Arguments
159    ---------
160        dataLake: str
161    A valid (absolute or relative) path name to the top level directory where
162    the data lake starts.
163
164    Returns
165    -------
166    A dictionary of speed jump file names for later SSScoring processing:
167        - keys are the file names
168        - values are a FlySight version string tag
169    """
170    jumpFiles = OrderedDict()
171    for root, dirs, files in os.walk(dataLake):
172        if any(name in root for name in IGNORE_LIST):
173            continue
174        for fileName in files:
175            data = None
176            if '.swp' in fileName: # Ignore Vim, other editors swap file
177                continue
178            if '.CSV' in fileName.upper():
179                version = '1'
180                jumpFileName = Path(root) / fileName
181                stat = os.stat(jumpFileName)
182                if all(x not in fileName for x in ('EVENT', 'SENSOR', 'TRACK')):
183                    # FlySight 1 or Insight track format
184                    data = pd.read_csv(jumpFileName, skiprows = (1, 1), index_col = False)
185                    if data is not None and 'headAcc' in data.columns:
186                        version = 'i'
187                elif 'TRACK' in fileName:
188                    # FlySight 2 track custom format
189                    data = pd.read_csv(jumpFileName, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False, na_values = ['NA', ])
190                    data = skipOverFS2MetadataRowsIn(data)
191                    data.drop('GNSS', inplace = True, axis = 1)
192                    version = '2'
193                if data is not None and stat.st_size >= MIN_JUMP_FILE_SIZE and validFlySightHeaderIn(jumpFileName):
194                    # explicit because `not data` is ambiguous for dataframes
195                    jumpFiles[jumpFileName] = version
196    jumpFiles = OrderedDict(sorted(jumpFiles.items()))
197    return jumpFiles

Get a list of all the speed jump files from a data lake, where data lake is defined as a reachable path that contains one or more FlySight CSV files. This function tests each file to ensure that it's a speed skydive FlySight file in a valid format and length. It doesn't validate data like versions prior to 1.9.0.

Arguments

dataLake: str

A valid (absolute or relative) path name to the top level directory where the data lake starts.

Returns

A dictionary of speed jump file names for later SSScoring processing: - keys are the file names - values are a FlySight version string tag

def detectFlySightFileVersionOf(fileThing) -> ssscoring.datatypes.FlySightVersion:
200def detectFlySightFileVersionOf(fileThing) -> FlySightVersion:
201    """
202    Detects the FlySight file version based on its file name and format.
203
204    Arguments
205    ---------
206        fileThing
207    A string, `bytes` buffer or `pathlib.Path` object corresponding to track
208    file.  If string or `pathlib.Path`, it'll be treated as a file.
209
210    Returns
211    -------
212    An instance of `ssscoring.flysight.FlySightVersion` with a valid version
213    symbolic value.
214
215    Errors
216    ------
217    `ssscoring.errors.SSScoringError` if the file is not a CSV and it's some
218    other invalid format.
219    """
220    match fileThing:
221        case Path():
222            fileName = fileThing.as_posix()
223        case str():
224            fileName = fileThing
225            fileThing = Path(fileThing)
226        case bytes():
227            fileName = '00-00-00.CSV'
228        case _:
229            raise SSScoringError('fileThing must be a Path, str, or bytes')
230
231    delimiters =  [',', ]
232    stream = None
233    if not '.CSV' in fileName.upper():
234        raise SSScoringError('Invalid file extension type')
235    if any(x in fileName for x in ('EVENT.CSV', 'SENSOR.CSV')):
236        raise SSScoringError('Only TRACK.CSV v2 files can be processed at this time')
237    if isinstance(fileThing, Path) or isinstance(fileThing, str):
238        if not fileThing.is_file():
239            raise SSScoringError('%s - file not found in data lake' % fileName)
240        if not validFlySightHeaderIn(fileName):
241            raise SSScoringError('CSV is not a valid FlySight file')
242        stream = open(fileName, 'r')
243    elif isinstance(fileThing, bytes):
244        stream = StringIO(fileThing.decode(FLYSIGHT_FILE_ENCODING))
245
246    try:
247        dialect = csv.Sniffer().sniff(stream.readline(), delimiters = delimiters)
248    except:
249        raise SSScoringError('Error while trying to validate %s file format' % fileName)
250    if dialect.delimiter in delimiters:
251        stream.seek(0)
252        header = next(csv.reader(stream))
253    else:
254        raise SSScoringError('CSV uses a different delimiter from FlySigh')
255    if header[0] == '$FLYS':
256        return FlySightVersion.V2
257    elif FLYSIGHT_1_HEADER.issubset(header):
258        return FlySightVersion.V1
259    elif _isInsightHeader(header):
260        return FlySightVersion.INSIGHT
261    else:
262        raise SSScoringError('%s file is not a FlySight v1 or v2 file')

Detects the FlySight file version based on its file name and format.

Arguments

fileThing

A string, bytes buffer or pathlib.Path object corresponding to track file. If string or pathlib.Path, it'll be treated as a file.

Returns

An instance of ssscoring.flysight.FlySightVersion with a valid version symbolic value.

Errors

ssscoring.errors.SSScoringError if the file is not a CSV and it's some other invalid format.

def readVersion1CSV(fileThing: object) -> pandas.DataFrame:
265def readVersion1CSV(fileThing: object) -> pd.DataFrame:
266    """
267    Read a FlySight file version 1 into a dataframe.  It scrubes blank rows that
268    get in the way of correct parsing.
269
270    Arguments
271    ---------
272        fileThing
273    A string or a `pathlib.Path` object.  It can be a relative or an absolute
274    path.
275
276    Returns
277    -------
278    A FlySight dataframe with the original column names, normalized for
279    manipulation as a dataframe instead of a file or CSV object.
280    """
281    return pd.read_csv(fileThing, skiprows = (1, 1), index_col = False)

Read a FlySight file version 1 into a dataframe. It scrubes blank rows that get in the way of correct parsing.

Arguments

fileThing

A string or a pathlib.Path object. It can be a relative or an absolute path.

Returns

A FlySight dataframe with the original column names, normalized for manipulation as a dataframe instead of a file or CSV object.

def readInsightCSV(fileThing: object) -> pandas.DataFrame:
301def readInsightCSV(fileThing: object) -> pd.DataFrame:
302    rawData = pd.read_csv(fileThing, skiprows=(1, 1), index_col=False)
303    rawData.drop('headAcc', inplace=True, axis=1)
304    return rawData
def readVersion2CSV(jumpFile: str) -> pandas.DataFrame:
307def readVersion2CSV(jumpFile: str) -> pd.DataFrame:
308    """
309    Read a FlySight file version 2 into a dataframe.  It scrubes blank rows that
310    get in the way of correct parsing and drops the `GNSS` column because it
311    just makes dataframe management murkier.
312
313    Arguments
314    ---------
315        fileThing
316    A string or a `pathlib.Path` object.  It can be a relative or an absolute
317    path.
318
319    Returns
320    -------
321    A FlySight dataframe with the original column names, normalized for
322    manipulation as a dataframe instead of a file or CSV object.
323    """
324
325    rawData = pd.read_csv(jumpFile, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False, na_values=['NA',])
326    rawData = skipOverFS2MetadataRowsIn(rawData)
327    rawData.drop('GNSS', inplace = True, axis = 1)
328    return rawData

Read a FlySight file version 2 into a dataframe. It scrubes blank rows that get in the way of correct parsing and drops the GNSS column because it just makes dataframe management murkier.

Arguments

fileThing

A string or a pathlib.Path object. It can be a relative or an absolute path.

Returns

A FlySight dataframe with the original column names, normalized for manipulation as a dataframe instead of a file or CSV object.

def getFlySightDataFromCSVBuffer(buffer: bytes, bufferName: str) -> tuple:
331def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple:
332    """
333    Ingress a buffer with known FlySight or SkyTrax file data for SSScoring
334    processing.
335
336    Arguments
337    ---------
338        buffer
339    A binary data buffer, bag of bytes, containing a known FlySight track file.
340
341        bufferName
342    An arbitrary name for the buffer of type `str`.  Used to construct the tag
343    for FlySight 1 buffers; ignored for FlySight 2 and Insight buffers, whose
344    tags are derived from the first row's timestamp.
345
346    Returns
347    -------
348    A `tuple` with two items:
349        - `rawData` - a dataframe representation of the CSV with the original
350          headers but without the data type header
351        - `tag` - an identifying string for the track.  Shape depends on the
352          device:
353            - FlySight 1: `<bufferName>:v1` - derived from `bufferName`
354            - FlySight 2: `HH-MM-ss:v2` - derived from the first GNSS row's
355              timestamp
356            - Insight: `HH-MM-ss:i` - derived from the first row's timestamp
357          Invalid files produce `<bufferName>:INVALID`.
358
359    Raises
360    ------
361    `SSScoringError` if the CSV file is invalid in any way.
362    """
363    if not isinstance(buffer, bytes):
364        raise SSScoringError('buffer must be an instance of bytes, a bytes buffer')
365    try:
366        stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING))
367    except Exception as e:
368        raise SSScoringError('invalid buffer endcoding - %s' % str(e))
369    try:
370        version = detectFlySightFileVersionOf(buffer)
371    except Exception:
372        tag = '%s:INVALID' % bufferName
373        rawData = None
374    else:
375        if version == FlySightVersion.V1:
376            rawData = readVersion1CSV(stringIO)
377            tag = _tagVersion1From(bufferName)
378        elif version == FlySightVersion.V2:
379            rawData = readVersion2CSV(stringIO)
380            tag = _tagVersion2From(rawData)
381        elif version == FlySightVersion.INSIGHT:
382            rawData = readInsightCSV(stringIO)
383            tag = _tagInsightFrom(rawData)
384    return (rawData, tag)

Ingress a buffer with known FlySight or SkyTrax file data for SSScoring processing.

Arguments

buffer

A binary data buffer, bag of bytes, containing a known FlySight track file.

bufferName

An arbitrary name for the buffer of type str. Used to construct the tag for FlySight 1 buffers; ignored for FlySight 2 and Insight buffers, whose tags are derived from the first row's timestamp.

Returns

A tuple with two items: - rawData - a dataframe representation of the CSV with the original headers but without the data type header - tag - an identifying string for the track. Shape depends on the device: - FlySight 1: <bufferName>:v1 - derived from bufferName - FlySight 2: HH-MM-ss:v2 - derived from the first GNSS row's timestamp - Insight: HH-MM-ss:i - derived from the first row's timestamp Invalid files produce <bufferName>:INVALID.

Raises

SSScoringError if the CSV file is invalid in any way.

def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
387def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
388    """
389    Ingress a known FlySight or SkyTrax file into memory for SSScoring
390    processing.
391
392    Arguments
393    ---------
394        jumpFile
395    A string or `pathlib.Path` object; can be a relative or an asbolute path.
396
397    Returns
398    -------
399    A `tuple` with two items:
400        - `rawData` - a dataframe representation of the CSV with the original
401          headers but without the data type header
402        - `tag` - an identifying string for the track.  Shape depends on the
403          device:
404            - FlySight 1: `<path slug>:v1` - derived from the file path
405            - FlySight 2: `HH-MM-ss:v2` - derived from the first GNSS row's
406              timestamp
407            - Insight: `HH-MM-ss:i` - derived from the first row's timestamp
408          `'NA'` if version detection fails.
409
410    Raises
411    ------
412    `SSScoringError` if the CSV file is invalid in any way.
413    """
414    if isinstance(jumpFile, Path):
415        jumpFile = jumpFile.as_posix()
416    elif isinstance(jumpFile, str):
417        pass
418    else:
419        raise SSScoringError('jumpFile must be a string or a Path object')
420    if not validFlySightHeaderIn(jumpFile):
421        raise SSScoringError('%s is an invalid speed skydiving file')
422    try:
423        version = detectFlySightFileVersionOf(jumpFile)
424    except Exception:
425        tag = 'NA'
426        rawData = None
427    else:
428        if version == FlySightVersion.V1:
429            rawData = readVersion1CSV(jumpFile)
430            tag = _tagVersion1From(jumpFile)
431        elif version == FlySightVersion.V2:
432            rawData = readVersion2CSV(jumpFile)
433            tag = _tagVersion2From(rawData)
434        elif version == FlySightVersion.INSIGHT:
435            rawData = readInsightCSV(jumpFile)
436            tag = _tagInsightFrom(rawData)
437    return (rawData, tag)

Ingress a known FlySight or SkyTrax file into memory for SSScoring processing.

Arguments

jumpFile

A string or pathlib.Path object; can be a relative or an asbolute path.

Returns

A tuple with two items: - rawData - a dataframe representation of the CSV with the original headers but without the data type header - tag - an identifying string for the track. Shape depends on the device: - FlySight 1: <path slug>:v1 - derived from the file path - FlySight 2: HH-MM-ss:v2 - derived from the first GNSS row's timestamp - Insight: HH-MM-ss:i - derived from the first row's timestamp 'NA' if version detection fails.

Raises

SSScoringError if the CSV file is invalid in any way.