openrvdas.logger.utils.record_parser_formats

Custom format definitions for RecordParser.

To use, import the 'extra_format_types' dict and pass to the parser:

import parse from logger.utils.record_parser_formats import extra_format_types

pattern = parse.compile(format=record_format, extra_types=extra_format_types) parsed_values = pattern.parse(record)

Why We Have This

We want to expand the default repertoire of the parse() function to be able to handle ints/floats/strings that might be omitted. To do that, we define some additional named types for it that we can use in format definitions.

These might be used, for example, in the following pattern where we might have either, both or neither of speed in knots and/or km/hour.

"{SpeedKt:of},N,{SpeedKm:of},K"

The recognized format types we add are: od = optional integer of = optional generalized float og = optional generalized number - also handles '#VALUE!' as None ow = optional sequence of letters, numbers, underscores os = optional sequence of any characters - will match everything on line

nlat = NMEA-formatted latitude or longitude, converted to decimal degrees

nc = any ASCII text that is not a comma ns = any ASCII text that is not an asterisk ("star")

See 'Custom Type Conversions' in https://pypi.org/project/parse/ for a discussion of how format types work.

TODO: allow device_type definitions to hand in their own format types.

  1#!/usr/bin/env python3
  2
  3"""Custom format definitions for RecordParser.
  4
  5To use, import the 'extra_format_types' dict and pass to the parser:
  6
  7import parse
  8from logger.utils.record_parser_formats import extra_format_types
  9
 10pattern = parse.compile(format=record_format, extra_types=extra_format_types)
 11parsed_values = pattern.parse(record)
 12
 13# Why We Have This
 14
 15We want to expand the default repertoire of the parse() function to be
 16able to handle ints/floats/strings that *might* be omitted. To do
 17that, we define some additional named types for it that we can use in
 18format definitions.
 19
 20These might be used, for example, in the following pattern where we
 21might have either, both or neither of speed in knots and/or km/hour.
 22
 23  "{SpeedKt:of},N,{SpeedKm:of},K"
 24
 25The recognized format types we add are:
 26  od   = optional integer
 27  of   = optional generalized float
 28  og   = optional generalized number - also handles '#VALUE!' as None
 29  ow   = optional sequence of letters, numbers, underscores
 30  os   = optional sequence of any characters - will match everything on line
 31
 32  nlat = NMEA-formatted latitude or longitude, converted to decimal degrees
 33
 34  nc = any ASCII text that is not a comma
 35  ns = any ASCII text that is not an asterisk ("star")
 36
 37See 'Custom Type Conversions' in https://pypi.org/project/parse/ for a
 38discussion of how format types work.
 39
 40TODO: allow device_type definitions to hand in their own format types.
 41"""
 42import logging
 43
 44
 45def optional_d(text):
 46    """Method for parsing an 'optional' integer."""
 47    if text:
 48        return int(text)
 49    else:
 50        return None
 51
 52
 53optional_d.pattern = r'\s*[-+]?\d*'
 54
 55
 56def optional_f(text):
 57    """Method for parsing an 'optional' generalized float."""
 58    if text:
 59        return float(text)
 60    else:
 61        return None
 62
 63
 64optional_f.pattern = r'(\s*[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?|)'
 65
 66
 67def optional_g(text):
 68    """Method for parsing an 'optional' generalized number."""
 69    if text == '#VALUE!':
 70        return None
 71    if text:
 72        return float(text)
 73    else:
 74        return None
 75
 76
 77optional_g.pattern = r'(#VALUE!|\s*[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?|\d*)'
 78
 79
 80def optional_w(text):
 81    """Method for parsing an 'optional' letters/numbers/underscore
 82    string."""
 83    if text:
 84        return text
 85    else:
 86        return None
 87
 88
 89optional_w.pattern = r'\w*'
 90
 91
 92def optional_s(text):
 93    """Method for parsing any sequence of zero or more characters. Will absorb
 94    everything in the string.
 95    """
 96    if text:
 97        return text
 98    else:
 99        return ''
100
101
102optional_s.pattern = r'.*'
103
104
105def nmea_lat_lon(text):
106    """Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) and
107    converting it into decimal degrees. Only handles the numeric part, not
108    any E/W or N/S component."""
109    if text:
110        nmea_value = float(text)
111        normalized_value = nmea_value / 100
112        degrees = int(normalized_value)
113        if abs(degrees) >= 180.0:
114            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
115            return None
116        fractional_degrees = (normalized_value - degrees) / 0.60
117        if abs(fractional_degrees) >= 1.0:
118            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
119            return None
120        return degrees + fractional_degrees
121    else:
122        return None
123
124
125nmea_lat_lon.pattern = r'(\s*[-]?(\d+(\.\d*)?|\.\d+)?|)'
126
127
128def nmea_lat_lon_dir(text):
129    """Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) along
130    with the hemisphere (E/W/N/S) and converting it into signed decimal
131    degrees. South and West are considered negative, North and East
132    positive.
133    """
134    if text:
135        nmea_str, dir = text.split(',')
136        nmea_value = float(nmea_str)
137        normalized_value = nmea_value / 100
138        degrees = int(normalized_value)
139        if abs(degrees) >= 180.0:
140            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
141            return None
142        fractional_degrees = (normalized_value - degrees) / 0.60
143        if abs(fractional_degrees) >= 1.0:
144            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
145            return None
146        decimal_degrees = degrees + fractional_degrees
147        if dir in ['W', 'S']:
148            decimal_degrees = -decimal_degrees
149        return decimal_degrees
150    else:
151        return None
152
153
154nmea_lat_lon_dir.pattern = r'(\s*(\d+(\.\d*)?|\.\d+),[NEWS]?|)'
155
156
157def not_comma(text):
158    """Method for parsing a string (or anything) between commas
159    string."""
160    if text:
161        return text
162    else:
163        return None
164
165
166not_comma.pattern = r'[^,]*'
167
168
169def not_star(text):
170    """Method for parsing a string (or anything) terminated by a "*"
171    """
172    if text:
173        return text
174    else:
175        return None
176
177
178not_star.pattern = r'[^\*]*'
179
180
181extra_format_types = dict(
182    od=optional_d,
183    of=optional_f,
184    og=optional_g,
185    ow=optional_w,
186    os=optional_s,
187
188    nlat=nmea_lat_lon,
189    nlat_dir=nmea_lat_lon_dir,
190
191    nc=not_comma,
192    ns=not_star)
def optional_d(text):
46def optional_d(text):
47    """Method for parsing an 'optional' integer."""
48    if text:
49        return int(text)
50    else:
51        return None

Method for parsing an 'optional' integer.

def optional_f(text):
57def optional_f(text):
58    """Method for parsing an 'optional' generalized float."""
59    if text:
60        return float(text)
61    else:
62        return None

Method for parsing an 'optional' generalized float.

def optional_g(text):
68def optional_g(text):
69    """Method for parsing an 'optional' generalized number."""
70    if text == '#VALUE!':
71        return None
72    if text:
73        return float(text)
74    else:
75        return None

Method for parsing an 'optional' generalized number.

def optional_w(text):
81def optional_w(text):
82    """Method for parsing an 'optional' letters/numbers/underscore
83    string."""
84    if text:
85        return text
86    else:
87        return None

Method for parsing an 'optional' letters/numbers/underscore string.

def optional_s(text):
 93def optional_s(text):
 94    """Method for parsing any sequence of zero or more characters. Will absorb
 95    everything in the string.
 96    """
 97    if text:
 98        return text
 99    else:
100        return ''

Method for parsing any sequence of zero or more characters. Will absorb everything in the string.

def nmea_lat_lon(text):
106def nmea_lat_lon(text):
107    """Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) and
108    converting it into decimal degrees. Only handles the numeric part, not
109    any E/W or N/S component."""
110    if text:
111        nmea_value = float(text)
112        normalized_value = nmea_value / 100
113        degrees = int(normalized_value)
114        if abs(degrees) >= 180.0:
115            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
116            return None
117        fractional_degrees = (normalized_value - degrees) / 0.60
118        if abs(fractional_degrees) >= 1.0:
119            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
120            return None
121        return degrees + fractional_degrees
122    else:
123        return None

Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) and converting it into decimal degrees. Only handles the numeric part, not any E/W or N/S component.

def nmea_lat_lon_dir(text):
129def nmea_lat_lon_dir(text):
130    """Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) along
131    with the hemisphere (E/W/N/S) and converting it into signed decimal
132    degrees. South and West are considered negative, North and East
133    positive.
134    """
135    if text:
136        nmea_str, dir = text.split(',')
137        nmea_value = float(nmea_str)
138        normalized_value = nmea_value / 100
139        degrees = int(normalized_value)
140        if abs(degrees) >= 180.0:
141            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
142            return None
143        fractional_degrees = (normalized_value - degrees) / 0.60
144        if abs(fractional_degrees) >= 1.0:
145            logging.warning('Improper NMEA-style latitude/longitude: "%s"', text)
146            return None
147        decimal_degrees = degrees + fractional_degrees
148        if dir in ['W', 'S']:
149            decimal_degrees = -decimal_degrees
150        return decimal_degrees
151    else:
152        return None

Method for parsing an NMEA latitude or longitude (DDDMM.MMMM) along with the hemisphere (E/W/N/S) and converting it into signed decimal degrees. South and West are considered negative, North and East positive.

def not_comma(text):
158def not_comma(text):
159    """Method for parsing a string (or anything) between commas
160    string."""
161    if text:
162        return text
163    else:
164        return None

Method for parsing a string (or anything) between commas string.

def not_star(text):
170def not_star(text):
171    """Method for parsing a string (or anything) terminated by a "*"
172    """
173    if text:
174        return text
175    else:
176        return None

Method for parsing a string (or anything) terminated by a "*"

extra_format_types = {'od': <function optional_d>, 'of': <function optional_f>, 'og': <function optional_g>, 'ow': <function optional_w>, 'os': <function optional_s>, 'nlat': <function nmea_lat_lon>, 'nlat_dir': <function nmea_lat_lon_dir>, 'nc': <function not_comma>, 'ns': <function not_star>}