openrvdas.logger.utils.das_record
1#!/usr/bin/env python3 2 3import json 4import pprint 5import logging 6 7from logger.utils.timestamp import timestamp as timestamp_method # noqa: E402 8 9 10class DASRecord: 11 """DASRecord is a structured representation of the field names and 12 values (and metadata) contained in a sensor record. 13 """ 14 ############################ 15 16 def __init__(self, json_str=None, data_id=None, message_type=None, 17 timestamp=0, fields=None, metadata=None): 18 """ 19 If a json string is passed, it is parsed into a dictionary and its 20 values for timestamp, fields and metadata are copied in. Otherwise, 21 the DASRecord object is initialized with the passed-in values for 22 instrument, timestamp, fields (a dictionary of fieldname-value pairs) 23 and metadata. 24 25 If timestamp is not specified, the instance will use the current time. 26 """ 27 if json_str: 28 parsed = json.loads(json_str) 29 self.data_id = parsed.get('data_id') 30 self.message_type = parsed.get('message_type') 31 self.timestamp = parsed.get('timestamp') 32 self.fields = parsed.get('fields', {}) 33 self.metadata = parsed.get('metadata', {}) 34 else: 35 # self.source = 36 self.data_id = data_id 37 self.message_type = message_type 38 self.timestamp = timestamp or timestamp_method() 39 if fields is None: 40 self.fields = {} 41 else: 42 self.fields = fields 43 if metadata is None: 44 self.metadata = {} 45 else: 46 self.metadata = metadata 47 48 ############################ 49 def as_json(self, pretty=False): 50 """Return DASRecord as a JSON string.""" 51 json_dict = { 52 'data_id': self.data_id, 53 'message_type': self.message_type, 54 'timestamp': self.timestamp, 55 'fields': self.fields, 56 'metadata': self.metadata 57 } 58 if pretty: 59 return json.dumps(json_dict, sort_keys=True, indent=4) 60 else: 61 return json.dumps(json_dict) 62 63 ############################ 64 def __str__(self): 65 das_dict = { 66 'data_id': self.data_id, 67 'message_type': self.message_type, 68 'timestamp': self.timestamp, 69 'fields': self.fields, 70 'metadata': self.metadata 71 } 72 return pprint.pformat(das_dict) 73 74 ############################ 75 def __eq__(self, other): 76 return (other and 77 self.data_id == other.data_id and 78 self.message_type == other.message_type and 79 self.timestamp == other.timestamp and 80 self.fields == other.fields and 81 self.metadata == other.metadata) 82 83 ############################ 84 def __setitem__(self, field, value): 85 self.fields[field] = value 86 87 ############################ 88 def __getitem__(self, field): 89 try: 90 return self.fields[field] 91 except KeyError: 92 logging.error(f'No field "{field}" found in DASRecord {self}') 93 raise 94 95 ############################ 96 def __delitem__(self, field): 97 try: 98 del self.fields[field] 99 except KeyError: 100 logging.error(f'Attempt to delete non-existent field "{field}" in DASRecord: {self}') 101 102 ############################ 103 def get(self, field, default=None): 104 return self.fields.get(field, default) 105 106 107def to_das_record_list(record, data_id=None): 108 """Utility function to normalize different types of records into a 109 list of DASRecords. 110 111 Take input in one of these three formats: 112 - DASRecord 113 - a single record dict with keys 'timestamp' and 'fields' 114 - a field dict of format 115 ``` {field_name: [(timestamp, value), (timestamp, value),...], 116 field_name: [(timestamp, value), (timestamp, value),...], 117 } 118 ``` 119 and convert it into a list of zero or more DASRecords. 120 """ 121 # What type of record is this? 122 if not record: 123 return [] 124 125 # If it's a list, assume it's already a list of DASRecords 126 if isinstance(record, list): 127 return record 128 129 # If it's a single DASRecord, it's easy 130 if isinstance(record, DASRecord): 131 return [record] 132 133 # At this point, if it's not a dict, we don't know *what* it is 134 if not isinstance(record, dict): 135 logging.error('Unknown type of input passed to to_das_record_list: %s: %s', 136 type(record), record) 137 return [] 138 139 # If it's a single timestamp dict, it's easy 140 elif 'timestamp' in record and 'fields' in record: 141 return [DASRecord(data_id=data_id, 142 timestamp=record['timestamp'], 143 fields=record['fields'], 144 metadata=record.get('metadata'))] 145 146 # If here, we believe we've received a field dict, in which each 147 # field may have multiple [timestamp, value] pairs. First thing we 148 # do is reformat the data into a map of 149 # {timestamp: {field:value, field:value},...}} 150 try: 151 by_timestamp = {} 152 for field, ts_value_list in record.items(): 153 if not isinstance(ts_value_list, list): 154 logging.warning('Expected field_name: [(timestamp, value),...] pairs, ' 155 'found %s: %s', field, ts_value_list) 156 continue 157 for (timestamp, value) in ts_value_list: 158 if timestamp not in by_timestamp: 159 by_timestamp[timestamp] = {} 160 by_timestamp[timestamp][field] = value 161 162 # Now copy the entries into an ordered-by-timestamp list. 163 results = [DASRecord(data_id=data_id, timestamp=ts, fields=by_timestamp[ts]) 164 for ts in sorted(by_timestamp)] 165 return results 166 except ValueError: 167 logging.error('Badly-structured field dictionary: %s: %s', 168 field, pprint.pformat(ts_value_list)) 169 return [] 170 171 172def collect_metadata_for_fields(field_names, timestamp, metadata, 173 metadata_interval, metadata_last_sent): 174 """ 175 Collect metadata for fields that are due to be sent based on the interval. 176 177 This function is shared between RecordParser and RegexParser to avoid 178 code duplication. 179 180 Args: 181 field_names: Iterable of field names to check for metadata. 182 timestamp: Current record timestamp (numeric). 183 metadata: Dict mapping field names to their metadata dicts. 184 metadata_interval: Minimum seconds between metadata sends per field. 185 metadata_last_sent: Dict tracking last send time per field (modified in place). 186 187 Returns: 188 Dict with 'fields' key containing metadata to inject, or None if no 189 metadata is due to be sent. 190 """ 191 if not metadata or not metadata_interval: 192 return None 193 194 metadata_fields = {} 195 for field_name in field_names: 196 last_sent = metadata_last_sent.get(field_name, 0) 197 if timestamp - last_sent > metadata_interval: 198 field_metadata = metadata.get(field_name) 199 if field_metadata: 200 metadata_fields[field_name] = field_metadata 201 metadata_last_sent[field_name] = timestamp 202 203 return {'fields': metadata_fields} if metadata_fields else None
11class DASRecord: 12 """DASRecord is a structured representation of the field names and 13 values (and metadata) contained in a sensor record. 14 """ 15 ############################ 16 17 def __init__(self, json_str=None, data_id=None, message_type=None, 18 timestamp=0, fields=None, metadata=None): 19 """ 20 If a json string is passed, it is parsed into a dictionary and its 21 values for timestamp, fields and metadata are copied in. Otherwise, 22 the DASRecord object is initialized with the passed-in values for 23 instrument, timestamp, fields (a dictionary of fieldname-value pairs) 24 and metadata. 25 26 If timestamp is not specified, the instance will use the current time. 27 """ 28 if json_str: 29 parsed = json.loads(json_str) 30 self.data_id = parsed.get('data_id') 31 self.message_type = parsed.get('message_type') 32 self.timestamp = parsed.get('timestamp') 33 self.fields = parsed.get('fields', {}) 34 self.metadata = parsed.get('metadata', {}) 35 else: 36 # self.source = 37 self.data_id = data_id 38 self.message_type = message_type 39 self.timestamp = timestamp or timestamp_method() 40 if fields is None: 41 self.fields = {} 42 else: 43 self.fields = fields 44 if metadata is None: 45 self.metadata = {} 46 else: 47 self.metadata = metadata 48 49 ############################ 50 def as_json(self, pretty=False): 51 """Return DASRecord as a JSON string.""" 52 json_dict = { 53 'data_id': self.data_id, 54 'message_type': self.message_type, 55 'timestamp': self.timestamp, 56 'fields': self.fields, 57 'metadata': self.metadata 58 } 59 if pretty: 60 return json.dumps(json_dict, sort_keys=True, indent=4) 61 else: 62 return json.dumps(json_dict) 63 64 ############################ 65 def __str__(self): 66 das_dict = { 67 'data_id': self.data_id, 68 'message_type': self.message_type, 69 'timestamp': self.timestamp, 70 'fields': self.fields, 71 'metadata': self.metadata 72 } 73 return pprint.pformat(das_dict) 74 75 ############################ 76 def __eq__(self, other): 77 return (other and 78 self.data_id == other.data_id and 79 self.message_type == other.message_type and 80 self.timestamp == other.timestamp and 81 self.fields == other.fields and 82 self.metadata == other.metadata) 83 84 ############################ 85 def __setitem__(self, field, value): 86 self.fields[field] = value 87 88 ############################ 89 def __getitem__(self, field): 90 try: 91 return self.fields[field] 92 except KeyError: 93 logging.error(f'No field "{field}" found in DASRecord {self}') 94 raise 95 96 ############################ 97 def __delitem__(self, field): 98 try: 99 del self.fields[field] 100 except KeyError: 101 logging.error(f'Attempt to delete non-existent field "{field}" in DASRecord: {self}') 102 103 ############################ 104 def get(self, field, default=None): 105 return self.fields.get(field, default)
DASRecord is a structured representation of the field names and values (and metadata) contained in a sensor record.
17 def __init__(self, json_str=None, data_id=None, message_type=None, 18 timestamp=0, fields=None, metadata=None): 19 """ 20 If a json string is passed, it is parsed into a dictionary and its 21 values for timestamp, fields and metadata are copied in. Otherwise, 22 the DASRecord object is initialized with the passed-in values for 23 instrument, timestamp, fields (a dictionary of fieldname-value pairs) 24 and metadata. 25 26 If timestamp is not specified, the instance will use the current time. 27 """ 28 if json_str: 29 parsed = json.loads(json_str) 30 self.data_id = parsed.get('data_id') 31 self.message_type = parsed.get('message_type') 32 self.timestamp = parsed.get('timestamp') 33 self.fields = parsed.get('fields', {}) 34 self.metadata = parsed.get('metadata', {}) 35 else: 36 # self.source = 37 self.data_id = data_id 38 self.message_type = message_type 39 self.timestamp = timestamp or timestamp_method() 40 if fields is None: 41 self.fields = {} 42 else: 43 self.fields = fields 44 if metadata is None: 45 self.metadata = {} 46 else: 47 self.metadata = metadata
If a json string is passed, it is parsed into a dictionary and its values for timestamp, fields and metadata are copied in. Otherwise, the DASRecord object is initialized with the passed-in values for instrument, timestamp, fields (a dictionary of fieldname-value pairs) and metadata.
If timestamp is not specified, the instance will use the current time.
50 def as_json(self, pretty=False): 51 """Return DASRecord as a JSON string.""" 52 json_dict = { 53 'data_id': self.data_id, 54 'message_type': self.message_type, 55 'timestamp': self.timestamp, 56 'fields': self.fields, 57 'metadata': self.metadata 58 } 59 if pretty: 60 return json.dumps(json_dict, sort_keys=True, indent=4) 61 else: 62 return json.dumps(json_dict)
Return DASRecord as a JSON string.
108def to_das_record_list(record, data_id=None): 109 """Utility function to normalize different types of records into a 110 list of DASRecords. 111 112 Take input in one of these three formats: 113 - DASRecord 114 - a single record dict with keys 'timestamp' and 'fields' 115 - a field dict of format 116 ``` {field_name: [(timestamp, value), (timestamp, value),...], 117 field_name: [(timestamp, value), (timestamp, value),...], 118 } 119 ``` 120 and convert it into a list of zero or more DASRecords. 121 """ 122 # What type of record is this? 123 if not record: 124 return [] 125 126 # If it's a list, assume it's already a list of DASRecords 127 if isinstance(record, list): 128 return record 129 130 # If it's a single DASRecord, it's easy 131 if isinstance(record, DASRecord): 132 return [record] 133 134 # At this point, if it's not a dict, we don't know *what* it is 135 if not isinstance(record, dict): 136 logging.error('Unknown type of input passed to to_das_record_list: %s: %s', 137 type(record), record) 138 return [] 139 140 # If it's a single timestamp dict, it's easy 141 elif 'timestamp' in record and 'fields' in record: 142 return [DASRecord(data_id=data_id, 143 timestamp=record['timestamp'], 144 fields=record['fields'], 145 metadata=record.get('metadata'))] 146 147 # If here, we believe we've received a field dict, in which each 148 # field may have multiple [timestamp, value] pairs. First thing we 149 # do is reformat the data into a map of 150 # {timestamp: {field:value, field:value},...}} 151 try: 152 by_timestamp = {} 153 for field, ts_value_list in record.items(): 154 if not isinstance(ts_value_list, list): 155 logging.warning('Expected field_name: [(timestamp, value),...] pairs, ' 156 'found %s: %s', field, ts_value_list) 157 continue 158 for (timestamp, value) in ts_value_list: 159 if timestamp not in by_timestamp: 160 by_timestamp[timestamp] = {} 161 by_timestamp[timestamp][field] = value 162 163 # Now copy the entries into an ordered-by-timestamp list. 164 results = [DASRecord(data_id=data_id, timestamp=ts, fields=by_timestamp[ts]) 165 for ts in sorted(by_timestamp)] 166 return results 167 except ValueError: 168 logging.error('Badly-structured field dictionary: %s: %s', 169 field, pprint.pformat(ts_value_list)) 170 return []
Utility function to normalize different types of records into a list of DASRecords.
Take input in one of these three formats:
- DASRecord
- a single record dict with keys 'timestamp' and 'fields'
- a field dict of format
{field_name: [(timestamp, value), (timestamp, value),...], field_name: [(timestamp, value), (timestamp, value),...], }and convert it into a list of zero or more DASRecords.
173def collect_metadata_for_fields(field_names, timestamp, metadata, 174 metadata_interval, metadata_last_sent): 175 """ 176 Collect metadata for fields that are due to be sent based on the interval. 177 178 This function is shared between RecordParser and RegexParser to avoid 179 code duplication. 180 181 Args: 182 field_names: Iterable of field names to check for metadata. 183 timestamp: Current record timestamp (numeric). 184 metadata: Dict mapping field names to their metadata dicts. 185 metadata_interval: Minimum seconds between metadata sends per field. 186 metadata_last_sent: Dict tracking last send time per field (modified in place). 187 188 Returns: 189 Dict with 'fields' key containing metadata to inject, or None if no 190 metadata is due to be sent. 191 """ 192 if not metadata or not metadata_interval: 193 return None 194 195 metadata_fields = {} 196 for field_name in field_names: 197 last_sent = metadata_last_sent.get(field_name, 0) 198 if timestamp - last_sent > metadata_interval: 199 field_metadata = metadata.get(field_name) 200 if field_metadata: 201 metadata_fields[field_name] = field_metadata 202 metadata_last_sent[field_name] = timestamp 203 204 return {'fields': metadata_fields} if metadata_fields else None
Collect metadata for fields that are due to be sent based on the interval.
This function is shared between RecordParser and RegexParser to avoid code duplication.
Args: field_names: Iterable of field names to check for metadata. timestamp: Current record timestamp (numeric). metadata: Dict mapping field names to their metadata dicts. metadata_interval: Minimum seconds between metadata sends per field. metadata_last_sent: Dict tracking last send time per field (modified in place).
Returns: Dict with 'fields' key containing metadata to inject, or None if no metadata is due to be sent.