openrvdas.logger.utils.das_record

No module-level documentation available.
  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
class DASRecord:
 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.

DASRecord( json_str=None, data_id=None, message_type=None, timestamp=0, fields=None, metadata=None)
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.

def as_json(self, pretty=False):
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.

def get(self, field, default=None):
104    def get(self, field, default=None):
105        return self.fields.get(field, default)
def to_das_record_list(record, data_id=None):
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.
def collect_metadata_for_fields( field_names, timestamp, metadata, metadata_interval, metadata_last_sent):
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.