openrvdas.logger.transforms.convert_fields_transform
Transform to convert fields in a DASRecord to specified types.
This is a thin wrapper around the convert_fields utility function, providing the Transform interface for use in listener pipelines.
1#!/usr/bin/env python3 2"""Transform to convert fields in a DASRecord to specified types. 3 4This is a thin wrapper around the convert_fields utility function, 5providing the Transform interface for use in listener pipelines. 6""" 7 8import copy 9import logging 10from typing import Union 11 12 13from logger.utils.das_record import DASRecord # noqa: E402 14from logger.utils.convert_fields import convert_fields # noqa: E402 15from logger.transforms.transform import Transform # noqa: E402 16 17 18################################################################################ 19class ConvertFieldsTransform(Transform): 20 """ 21 Converts fields in a DASRecord from strings (or other types) to specific types 22 defined in a configuration dictionary. Also handles NMEA-style latitude/longitude 23 conversions (e.g., combining a value and a cardinal direction field). 24 """ 25 26 def __init__(self, fields=None, delete_source_fields=False, 27 delete_unconverted_fields=False, **kwargs): 28 """ 29 Args: 30 fields (dict): A dictionary mapping field names to their target configuration. 31 This accepts two formats for the value: 32 1. A dictionary containing metadata (preferred). 33 keys: 34 'data_type': target type (float, int, str, bool, hex, 35 nmea_lat, nmea_lon) 36 'direction_field': (for nmea_*) name of direction field 37 Example: 38 {'Latitude': {'data_type': 'nmea_lat', 39 'direction_field': 'NorS'}} 40 2. A simple string specifying the data type (backward compatibility). 41 Example: 42 {'heave': 'float'} 43 44 delete_source_fields (bool): If True, source fields (e.g. 'raw_lat', 'lat_dir') 45 are removed after successful conversion. 46 Defaults to False. 47 48 delete_unconverted_fields (bool): If True, fields NOT involved in conversion 49 are removed. Defaults to False. 50 """ 51 super().__init__(**kwargs) # processes 'quiet' and type hints 52 53 self.field_specs = {} 54 self.lat_lon_specs = {} 55 self.delete_source_fields = delete_source_fields 56 self.delete_unconverted_fields = delete_unconverted_fields 57 58 if fields: 59 # Process fields to separate standard conversions from special NMEA ones 60 for f_name, f_def in fields.items(): 61 # Normalize definition 62 if isinstance(f_def, str): 63 f_def = {'data_type': f_def} 64 65 dtype = f_def.get('data_type') 66 67 # Check for declarative NMEA configuration 68 if dtype in ['nmea_lat', 'nmea_lon']: 69 dir_field = f_def.get('direction_field') 70 if dir_field: 71 # Format: target_field -> (value_field, direction_field) 72 self.lat_lon_specs[f_name] = (f_name, dir_field) 73 else: 74 logging.warning(f"Field '{f_name}' has type '{dtype}' but " 75 "missing 'direction_field'. Ignoring.") 76 else: 77 # Standard field 78 self.field_specs[f_name] = f_def 79 80 ############################ 81 def transform(self, record: Union[str, dict, DASRecord])\ 82 -> Union[str, dict, DASRecord]: 83 """ 84 Return a copy of the passed record with fields converted. 85 """ 86 # See if it's something we can process, and if not, try digesting 87 if not self.can_process_record(record): # BaseModule 88 return self.digest_record(record) # BaseModule 89 90 # We need to make a deep copy because we modify the record in place 91 new_record = copy.deepcopy(record) 92 93 # Handle list of records 94 if isinstance(new_record, list): 95 new_record_list = [] 96 for single_record in new_record: 97 result = self.transform(single_record) 98 if result: 99 new_record_list.append(result) 100 return new_record_list 101 102 # Identify the fields dictionary 103 if isinstance(new_record, DASRecord): 104 fields = new_record.fields 105 elif isinstance(new_record, dict): 106 if 'fields' in new_record and isinstance(new_record['fields'], dict): 107 fields = new_record['fields'] 108 else: 109 fields = new_record 110 else: 111 logging.warning('ConvertFieldsTransform received unknown record type: %s', 112 type(new_record)) 113 return None 114 115 # Delegate to the utility function 116 result = convert_fields( 117 fields, 118 self.field_specs, 119 self.lat_lon_specs, 120 delete_source_fields=self.delete_source_fields, 121 delete_unconverted_fields=self.delete_unconverted_fields, 122 quiet=self.quiet 123 ) 124 125 # If no fields remain, return None 126 if result is None: 127 return None 128 129 return new_record
20class ConvertFieldsTransform(Transform): 21 """ 22 Converts fields in a DASRecord from strings (or other types) to specific types 23 defined in a configuration dictionary. Also handles NMEA-style latitude/longitude 24 conversions (e.g., combining a value and a cardinal direction field). 25 """ 26 27 def __init__(self, fields=None, delete_source_fields=False, 28 delete_unconverted_fields=False, **kwargs): 29 """ 30 Args: 31 fields (dict): A dictionary mapping field names to their target configuration. 32 This accepts two formats for the value: 33 1. A dictionary containing metadata (preferred). 34 keys: 35 'data_type': target type (float, int, str, bool, hex, 36 nmea_lat, nmea_lon) 37 'direction_field': (for nmea_*) name of direction field 38 Example: 39 {'Latitude': {'data_type': 'nmea_lat', 40 'direction_field': 'NorS'}} 41 2. A simple string specifying the data type (backward compatibility). 42 Example: 43 {'heave': 'float'} 44 45 delete_source_fields (bool): If True, source fields (e.g. 'raw_lat', 'lat_dir') 46 are removed after successful conversion. 47 Defaults to False. 48 49 delete_unconverted_fields (bool): If True, fields NOT involved in conversion 50 are removed. Defaults to False. 51 """ 52 super().__init__(**kwargs) # processes 'quiet' and type hints 53 54 self.field_specs = {} 55 self.lat_lon_specs = {} 56 self.delete_source_fields = delete_source_fields 57 self.delete_unconverted_fields = delete_unconverted_fields 58 59 if fields: 60 # Process fields to separate standard conversions from special NMEA ones 61 for f_name, f_def in fields.items(): 62 # Normalize definition 63 if isinstance(f_def, str): 64 f_def = {'data_type': f_def} 65 66 dtype = f_def.get('data_type') 67 68 # Check for declarative NMEA configuration 69 if dtype in ['nmea_lat', 'nmea_lon']: 70 dir_field = f_def.get('direction_field') 71 if dir_field: 72 # Format: target_field -> (value_field, direction_field) 73 self.lat_lon_specs[f_name] = (f_name, dir_field) 74 else: 75 logging.warning(f"Field '{f_name}' has type '{dtype}' but " 76 "missing 'direction_field'. Ignoring.") 77 else: 78 # Standard field 79 self.field_specs[f_name] = f_def 80 81 ############################ 82 def transform(self, record: Union[str, dict, DASRecord])\ 83 -> Union[str, dict, DASRecord]: 84 """ 85 Return a copy of the passed record with fields converted. 86 """ 87 # See if it's something we can process, and if not, try digesting 88 if not self.can_process_record(record): # BaseModule 89 return self.digest_record(record) # BaseModule 90 91 # We need to make a deep copy because we modify the record in place 92 new_record = copy.deepcopy(record) 93 94 # Handle list of records 95 if isinstance(new_record, list): 96 new_record_list = [] 97 for single_record in new_record: 98 result = self.transform(single_record) 99 if result: 100 new_record_list.append(result) 101 return new_record_list 102 103 # Identify the fields dictionary 104 if isinstance(new_record, DASRecord): 105 fields = new_record.fields 106 elif isinstance(new_record, dict): 107 if 'fields' in new_record and isinstance(new_record['fields'], dict): 108 fields = new_record['fields'] 109 else: 110 fields = new_record 111 else: 112 logging.warning('ConvertFieldsTransform received unknown record type: %s', 113 type(new_record)) 114 return None 115 116 # Delegate to the utility function 117 result = convert_fields( 118 fields, 119 self.field_specs, 120 self.lat_lon_specs, 121 delete_source_fields=self.delete_source_fields, 122 delete_unconverted_fields=self.delete_unconverted_fields, 123 quiet=self.quiet 124 ) 125 126 # If no fields remain, return None 127 if result is None: 128 return None 129 130 return new_record
Converts fields in a DASRecord from strings (or other types) to specific types defined in a configuration dictionary. Also handles NMEA-style latitude/longitude conversions (e.g., combining a value and a cardinal direction field).
27 def __init__(self, fields=None, delete_source_fields=False, 28 delete_unconverted_fields=False, **kwargs): 29 """ 30 Args: 31 fields (dict): A dictionary mapping field names to their target configuration. 32 This accepts two formats for the value: 33 1. A dictionary containing metadata (preferred). 34 keys: 35 'data_type': target type (float, int, str, bool, hex, 36 nmea_lat, nmea_lon) 37 'direction_field': (for nmea_*) name of direction field 38 Example: 39 {'Latitude': {'data_type': 'nmea_lat', 40 'direction_field': 'NorS'}} 41 2. A simple string specifying the data type (backward compatibility). 42 Example: 43 {'heave': 'float'} 44 45 delete_source_fields (bool): If True, source fields (e.g. 'raw_lat', 'lat_dir') 46 are removed after successful conversion. 47 Defaults to False. 48 49 delete_unconverted_fields (bool): If True, fields NOT involved in conversion 50 are removed. Defaults to False. 51 """ 52 super().__init__(**kwargs) # processes 'quiet' and type hints 53 54 self.field_specs = {} 55 self.lat_lon_specs = {} 56 self.delete_source_fields = delete_source_fields 57 self.delete_unconverted_fields = delete_unconverted_fields 58 59 if fields: 60 # Process fields to separate standard conversions from special NMEA ones 61 for f_name, f_def in fields.items(): 62 # Normalize definition 63 if isinstance(f_def, str): 64 f_def = {'data_type': f_def} 65 66 dtype = f_def.get('data_type') 67 68 # Check for declarative NMEA configuration 69 if dtype in ['nmea_lat', 'nmea_lon']: 70 dir_field = f_def.get('direction_field') 71 if dir_field: 72 # Format: target_field -> (value_field, direction_field) 73 self.lat_lon_specs[f_name] = (f_name, dir_field) 74 else: 75 logging.warning(f"Field '{f_name}' has type '{dtype}' but " 76 "missing 'direction_field'. Ignoring.") 77 else: 78 # Standard field 79 self.field_specs[f_name] = f_def
Args: fields (dict): A dictionary mapping field names to their target configuration. This accepts two formats for the value: 1. A dictionary containing metadata (preferred). keys: 'data_type': target type (float, int, str, bool, hex, nmea_lat, nmea_lon) 'direction_field': (for nmea_*) name of direction field Example: {'Latitude': {'data_type': 'nmea_lat', 'direction_field': 'NorS'}} 2. A simple string specifying the data type (backward compatibility). Example: {'heave': 'float'}
delete_source_fields (bool): If True, source fields (e.g. 'raw_lat', 'lat_dir')
are removed after successful conversion.
Defaults to False.
delete_unconverted_fields (bool): If True, fields NOT involved in conversion
are removed. Defaults to False.
82 def transform(self, record: Union[str, dict, DASRecord])\ 83 -> Union[str, dict, DASRecord]: 84 """ 85 Return a copy of the passed record with fields converted. 86 """ 87 # See if it's something we can process, and if not, try digesting 88 if not self.can_process_record(record): # BaseModule 89 return self.digest_record(record) # BaseModule 90 91 # We need to make a deep copy because we modify the record in place 92 new_record = copy.deepcopy(record) 93 94 # Handle list of records 95 if isinstance(new_record, list): 96 new_record_list = [] 97 for single_record in new_record: 98 result = self.transform(single_record) 99 if result: 100 new_record_list.append(result) 101 return new_record_list 102 103 # Identify the fields dictionary 104 if isinstance(new_record, DASRecord): 105 fields = new_record.fields 106 elif isinstance(new_record, dict): 107 if 'fields' in new_record and isinstance(new_record['fields'], dict): 108 fields = new_record['fields'] 109 else: 110 fields = new_record 111 else: 112 logging.warning('ConvertFieldsTransform received unknown record type: %s', 113 type(new_record)) 114 return None 115 116 # Delegate to the utility function 117 result = convert_fields( 118 fields, 119 self.field_specs, 120 self.lat_lon_specs, 121 delete_source_fields=self.delete_source_fields, 122 delete_unconverted_fields=self.delete_unconverted_fields, 123 quiet=self.quiet 124 ) 125 126 # If no fields remain, return None 127 if result is None: 128 return None 129 130 return new_record
Return a copy of the passed record with fields converted.