openrvdas.logger.transforms.modify_value_transform
Modify values according to simple formula
1#!/usr/bin/env python3 2"""Modify values according to simple formula 3""" 4 5import copy 6import logging 7import time 8from typing import Union 9 10from logger.utils.das_record import DASRecord # noqa: E402 11from logger.transforms.transform import Transform # noqa: E402 12 13 14################################################################################ 15class ModifyValueTransform(Transform): 16 """Modify the value of specified fields according to simple formulae. 17 """ 18 def __init__(self, fields, data_id=None, delete_unmatched=False, quiet=False, 19 metadata_interval=None, **kwargs): 20 """ 21 fields 22 A dict of fields to match. Key of each is the field to match. Values are 23 a dict of what to do with the field value. E.g.: 24 25 fields: 26 FieldName: 27 mult_factor: 1.5 # default 1.0 28 add_factor: 3.44 # default 0.0 29 output_name: CorrectedFieldName # default is FieldName 30 metadata: Field name with linear foobar correction applied 31 delete_original: true # default false 32 FieldName2: 33 mapping_function: my_magic_function # God knows how we'd implement this, but... 34 output_name: CorrectedFieldName2 35 .... 36 37 Currently implemented manipulations for a field are: 38 mult_factor (default=1), add_factor (default=0) 39 result = mult_factor * value + add_factor 40 41 output_name (default is original field name) 42 Add the result to the record as a new field 43 44 delete_original (default=False) 45 If true, and if output_name is specified, delete original field from record 46 47 metadata (default=None) 48 If specified, any metadata associated with the new value 49 50 data_id (default None) 51 If not None, the data_id to substitute into the record. Otherwise use 52 data_id found in the original record, if available. 53 54 delete_unmatched (default=False) 55 If true, delete any unmatched fields from the record 56 57 quiet (default=False) 58 If True, don't warn if can't convert, or if overwriting existing values 59 60 metadata_interval (default=None) 61 If not None, how frequently, in seconds to attach field metadata to records 62 (NOTE: need to address which fields' metadata is sent along - all specified, 63 or only fields that have appeared since last send, or...?) 64 """ 65 super().__init__(**kwargs) # processes 'quiet' and type hints 66 67 self.fields = fields 68 self.data_id = data_id 69 self.delete_unmatched = delete_unmatched 70 self.metadata_interval = metadata_interval or 0 71 72 self._validate_fields() 73 self.last_metadata_send = 0 74 self.metadata = { 75 field: spec.get('metadata') 76 for field, spec in fields.items() if spec.get('metadata') 77 } 78 79 ############################ 80 def _validate_fields(self): 81 """Validate the configuration of fields.""" 82 allowed_keys = {'mult_factor', 'add_factor', 'output_name', 'metadata', 'delete_original'} 83 84 for field, spec in self.fields.items(): 85 extraneous_keys = set(spec) - allowed_keys 86 if extraneous_keys: 87 raise ValueError(f'Unexpected keys in field "{field}": {extraneous_keys}. ' 88 f'Allowed keys are {allowed_keys}') 89 90 if not isinstance(spec.get('mult_factor', 1), (int, float)): 91 raise ValueError(f'Invalid "mult_factor" for field "{field}". Must be numeric.') 92 if not isinstance(spec.get('add_factor', 0), (int, float)): 93 raise ValueError(f'Invalid "add_factor" for field "{field}". Must be numeric.') 94 95 ############################ 96 def transform(self, record: Union[DASRecord, dict]): 97 """ 98 Transform a record or list of records. 99 100 Args: 101 record (DASRecord or list): The input record(s). 102 103 Returns: 104 Transformed record(s) or None if the input is invalid. 105 """ 106 # See if it's something we can process, and if not, try digesting 107 if not self.can_process_record(record): # inherited from BaseModule() 108 return self.digest_record(record) # inherited from BaseModule() 109 110 # If we've got a dict, convert it to a DASRecord for uniform handling 111 if isinstance(record, dict): 112 record = DASRecord(record, data_id=self.data_id) 113 114 # Make a copy of the original record we're going to munge 115 result = copy.deepcopy(record) 116 117 # More efficient, but doesn't allow use to do delete_unmatched: 118 # for field in record.fields.keys() & self.fields.keys(): 119 for field in record.fields: 120 field_spec = self.fields.get(field) 121 122 # If there isn't a rule for this field 123 if field_spec is None: 124 if self.delete_unmatched: 125 del result[field] 126 continue 127 128 # Get the field value, make sure we can work with it 129 value = record.get(field) 130 try: 131 value = float(value) 132 except ValueError: 133 if not self.quiet: 134 logging.warning(f'ModifyValueTransform could not convert field {field} value ' 135 f'"{value}" to float for modification. Type: {type(value)}') 136 continue 137 138 # Do the actual computation 139 value *= field_spec.get('mult_factor', 1.0) 140 value += field_spec.get('add_factor', 0.0) 141 142 # Where are we going to write the value? Check if it already exists. If there is no 143 # target field name, use original field name. 144 target_field = field_spec.get('output_name') 145 if target_field and target_field in record.fields and not self.quiet: 146 logging.warning(f'ModifyValueTransform overwriting existing field: {target_field}') 147 if not target_field: 148 target_field = field 149 150 # Are we getting rid of the original field? If so, do that now, to avoid 151 # the semantic question of deleting original value when we're writing back 152 # to original field 153 if field_spec.get('delete_original'): 154 del result[field] 155 156 result[target_field] = value 157 158 # We're now done going through the record fields. If it's time to send metadata, 159 # add it to any existing metadata for record, overwriting existing fields. 160 if self._should_attach_metadata(record): 161 result.metadata.update(self.metadata) 162 163 return result 164 165 ############################ 166 def _should_attach_metadata(self, record): 167 """Determine if metadata should be attached to the record.""" 168 now = record.timestamp or time.time() 169 if self.metadata_interval and now > self.last_metadata_send + self.metadata_interval: 170 self.last_metadata_send = now 171 return True 172 return False
16class ModifyValueTransform(Transform): 17 """Modify the value of specified fields according to simple formulae. 18 """ 19 def __init__(self, fields, data_id=None, delete_unmatched=False, quiet=False, 20 metadata_interval=None, **kwargs): 21 """ 22 fields 23 A dict of fields to match. Key of each is the field to match. Values are 24 a dict of what to do with the field value. E.g.: 25 26 fields: 27 FieldName: 28 mult_factor: 1.5 # default 1.0 29 add_factor: 3.44 # default 0.0 30 output_name: CorrectedFieldName # default is FieldName 31 metadata: Field name with linear foobar correction applied 32 delete_original: true # default false 33 FieldName2: 34 mapping_function: my_magic_function # God knows how we'd implement this, but... 35 output_name: CorrectedFieldName2 36 .... 37 38 Currently implemented manipulations for a field are: 39 mult_factor (default=1), add_factor (default=0) 40 result = mult_factor * value + add_factor 41 42 output_name (default is original field name) 43 Add the result to the record as a new field 44 45 delete_original (default=False) 46 If true, and if output_name is specified, delete original field from record 47 48 metadata (default=None) 49 If specified, any metadata associated with the new value 50 51 data_id (default None) 52 If not None, the data_id to substitute into the record. Otherwise use 53 data_id found in the original record, if available. 54 55 delete_unmatched (default=False) 56 If true, delete any unmatched fields from the record 57 58 quiet (default=False) 59 If True, don't warn if can't convert, or if overwriting existing values 60 61 metadata_interval (default=None) 62 If not None, how frequently, in seconds to attach field metadata to records 63 (NOTE: need to address which fields' metadata is sent along - all specified, 64 or only fields that have appeared since last send, or...?) 65 """ 66 super().__init__(**kwargs) # processes 'quiet' and type hints 67 68 self.fields = fields 69 self.data_id = data_id 70 self.delete_unmatched = delete_unmatched 71 self.metadata_interval = metadata_interval or 0 72 73 self._validate_fields() 74 self.last_metadata_send = 0 75 self.metadata = { 76 field: spec.get('metadata') 77 for field, spec in fields.items() if spec.get('metadata') 78 } 79 80 ############################ 81 def _validate_fields(self): 82 """Validate the configuration of fields.""" 83 allowed_keys = {'mult_factor', 'add_factor', 'output_name', 'metadata', 'delete_original'} 84 85 for field, spec in self.fields.items(): 86 extraneous_keys = set(spec) - allowed_keys 87 if extraneous_keys: 88 raise ValueError(f'Unexpected keys in field "{field}": {extraneous_keys}. ' 89 f'Allowed keys are {allowed_keys}') 90 91 if not isinstance(spec.get('mult_factor', 1), (int, float)): 92 raise ValueError(f'Invalid "mult_factor" for field "{field}". Must be numeric.') 93 if not isinstance(spec.get('add_factor', 0), (int, float)): 94 raise ValueError(f'Invalid "add_factor" for field "{field}". Must be numeric.') 95 96 ############################ 97 def transform(self, record: Union[DASRecord, dict]): 98 """ 99 Transform a record or list of records. 100 101 Args: 102 record (DASRecord or list): The input record(s). 103 104 Returns: 105 Transformed record(s) or None if the input is invalid. 106 """ 107 # See if it's something we can process, and if not, try digesting 108 if not self.can_process_record(record): # inherited from BaseModule() 109 return self.digest_record(record) # inherited from BaseModule() 110 111 # If we've got a dict, convert it to a DASRecord for uniform handling 112 if isinstance(record, dict): 113 record = DASRecord(record, data_id=self.data_id) 114 115 # Make a copy of the original record we're going to munge 116 result = copy.deepcopy(record) 117 118 # More efficient, but doesn't allow use to do delete_unmatched: 119 # for field in record.fields.keys() & self.fields.keys(): 120 for field in record.fields: 121 field_spec = self.fields.get(field) 122 123 # If there isn't a rule for this field 124 if field_spec is None: 125 if self.delete_unmatched: 126 del result[field] 127 continue 128 129 # Get the field value, make sure we can work with it 130 value = record.get(field) 131 try: 132 value = float(value) 133 except ValueError: 134 if not self.quiet: 135 logging.warning(f'ModifyValueTransform could not convert field {field} value ' 136 f'"{value}" to float for modification. Type: {type(value)}') 137 continue 138 139 # Do the actual computation 140 value *= field_spec.get('mult_factor', 1.0) 141 value += field_spec.get('add_factor', 0.0) 142 143 # Where are we going to write the value? Check if it already exists. If there is no 144 # target field name, use original field name. 145 target_field = field_spec.get('output_name') 146 if target_field and target_field in record.fields and not self.quiet: 147 logging.warning(f'ModifyValueTransform overwriting existing field: {target_field}') 148 if not target_field: 149 target_field = field 150 151 # Are we getting rid of the original field? If so, do that now, to avoid 152 # the semantic question of deleting original value when we're writing back 153 # to original field 154 if field_spec.get('delete_original'): 155 del result[field] 156 157 result[target_field] = value 158 159 # We're now done going through the record fields. If it's time to send metadata, 160 # add it to any existing metadata for record, overwriting existing fields. 161 if self._should_attach_metadata(record): 162 result.metadata.update(self.metadata) 163 164 return result 165 166 ############################ 167 def _should_attach_metadata(self, record): 168 """Determine if metadata should be attached to the record.""" 169 now = record.timestamp or time.time() 170 if self.metadata_interval and now > self.last_metadata_send + self.metadata_interval: 171 self.last_metadata_send = now 172 return True 173 return False
Modify the value of specified fields according to simple formulae.
19 def __init__(self, fields, data_id=None, delete_unmatched=False, quiet=False, 20 metadata_interval=None, **kwargs): 21 """ 22 fields 23 A dict of fields to match. Key of each is the field to match. Values are 24 a dict of what to do with the field value. E.g.: 25 26 fields: 27 FieldName: 28 mult_factor: 1.5 # default 1.0 29 add_factor: 3.44 # default 0.0 30 output_name: CorrectedFieldName # default is FieldName 31 metadata: Field name with linear foobar correction applied 32 delete_original: true # default false 33 FieldName2: 34 mapping_function: my_magic_function # God knows how we'd implement this, but... 35 output_name: CorrectedFieldName2 36 .... 37 38 Currently implemented manipulations for a field are: 39 mult_factor (default=1), add_factor (default=0) 40 result = mult_factor * value + add_factor 41 42 output_name (default is original field name) 43 Add the result to the record as a new field 44 45 delete_original (default=False) 46 If true, and if output_name is specified, delete original field from record 47 48 metadata (default=None) 49 If specified, any metadata associated with the new value 50 51 data_id (default None) 52 If not None, the data_id to substitute into the record. Otherwise use 53 data_id found in the original record, if available. 54 55 delete_unmatched (default=False) 56 If true, delete any unmatched fields from the record 57 58 quiet (default=False) 59 If True, don't warn if can't convert, or if overwriting existing values 60 61 metadata_interval (default=None) 62 If not None, how frequently, in seconds to attach field metadata to records 63 (NOTE: need to address which fields' metadata is sent along - all specified, 64 or only fields that have appeared since last send, or...?) 65 """ 66 super().__init__(**kwargs) # processes 'quiet' and type hints 67 68 self.fields = fields 69 self.data_id = data_id 70 self.delete_unmatched = delete_unmatched 71 self.metadata_interval = metadata_interval or 0 72 73 self._validate_fields() 74 self.last_metadata_send = 0 75 self.metadata = { 76 field: spec.get('metadata') 77 for field, spec in fields.items() if spec.get('metadata') 78 }
fields A dict of fields to match. Key of each is the field to match. Values are a dict of what to do with the field value. E.g.:
fields:
FieldName:
mult_factor: 1.5 # default 1.0
add_factor: 3.44 # default 0.0
output_name: CorrectedFieldName # default is FieldName
metadata: Field name with linear foobar correction applied
delete_original: true # default false
FieldName2:
mapping_function: my_magic_function # God knows how we'd implement this, but...
output_name: CorrectedFieldName2
....
Currently implemented manipulations for a field are:
mult_factor (default=1), add_factor (default=0)
result = mult_factor * value + add_factor
output_name (default is original field name)
Add the result to the record as a new field
delete_original (default=False)
If true, and if output_name is specified, delete original field from record
metadata (default=None)
If specified, any metadata associated with the new value
data_id (default None) If not None, the data_id to substitute into the record. Otherwise use data_id found in the original record, if available.
delete_unmatched (default=False) If true, delete any unmatched fields from the record
quiet (default=False) If True, don't warn if can't convert, or if overwriting existing values
metadata_interval (default=None) If not None, how frequently, in seconds to attach field metadata to records (NOTE: need to address which fields' metadata is sent along - all specified, or only fields that have appeared since last send, or...?)
97 def transform(self, record: Union[DASRecord, dict]): 98 """ 99 Transform a record or list of records. 100 101 Args: 102 record (DASRecord or list): The input record(s). 103 104 Returns: 105 Transformed record(s) or None if the input is invalid. 106 """ 107 # See if it's something we can process, and if not, try digesting 108 if not self.can_process_record(record): # inherited from BaseModule() 109 return self.digest_record(record) # inherited from BaseModule() 110 111 # If we've got a dict, convert it to a DASRecord for uniform handling 112 if isinstance(record, dict): 113 record = DASRecord(record, data_id=self.data_id) 114 115 # Make a copy of the original record we're going to munge 116 result = copy.deepcopy(record) 117 118 # More efficient, but doesn't allow use to do delete_unmatched: 119 # for field in record.fields.keys() & self.fields.keys(): 120 for field in record.fields: 121 field_spec = self.fields.get(field) 122 123 # If there isn't a rule for this field 124 if field_spec is None: 125 if self.delete_unmatched: 126 del result[field] 127 continue 128 129 # Get the field value, make sure we can work with it 130 value = record.get(field) 131 try: 132 value = float(value) 133 except ValueError: 134 if not self.quiet: 135 logging.warning(f'ModifyValueTransform could not convert field {field} value ' 136 f'"{value}" to float for modification. Type: {type(value)}') 137 continue 138 139 # Do the actual computation 140 value *= field_spec.get('mult_factor', 1.0) 141 value += field_spec.get('add_factor', 0.0) 142 143 # Where are we going to write the value? Check if it already exists. If there is no 144 # target field name, use original field name. 145 target_field = field_spec.get('output_name') 146 if target_field and target_field in record.fields and not self.quiet: 147 logging.warning(f'ModifyValueTransform overwriting existing field: {target_field}') 148 if not target_field: 149 target_field = field 150 151 # Are we getting rid of the original field? If so, do that now, to avoid 152 # the semantic question of deleting original value when we're writing back 153 # to original field 154 if field_spec.get('delete_original'): 155 del result[field] 156 157 result[target_field] = value 158 159 # We're now done going through the record fields. If it's time to send metadata, 160 # add it to any existing metadata for record, overwriting existing fields. 161 if self._should_attach_metadata(record): 162 result.metadata.update(self.metadata) 163 164 return result
Transform a record or list of records.
Args: record (DASRecord or list): The input record(s).
Returns: Transformed record(s) or None if the input is invalid.