openrvdas.server.in_memory_server_api

API implementation for interacting with an in-memory data store.

See server/server_api_command_line.py for a sample script that exercises this class. Also see server/server_api.py for full documentation on the ServerAPI.

  1#!/usr/bin/env python3
  2"""API implementation for interacting with an in-memory data store.
  3
  4See server/server_api_command_line.py for a sample script that
  5exercises this class. Also see server/server_api.py for full
  6documentation on the ServerAPI.
  7"""
  8
  9import datetime
 10import logging
 11import pprint
 12import sys
 13import time
 14
 15
 16from server.server_api import ServerAPI  # noqa: E402
 17
 18DEFAULT_MAX_TRIES = 3
 19ID_SEPARATOR = ':'
 20
 21################################################################################
 22
 23
 24class InMemoryServerAPI(ServerAPI):
 25    ############################
 26    def __init__(self):
 27        super().__init__()
 28        self.config = {}
 29        self.mode = "n/a"
 30        self.logger_config = {}
 31        self.callbacks = []
 32        self.status = []
 33        self.server_messages = []
 34
 35    #############################
 36    # API methods below are used in querying/modifying the API for the
 37    # record of the running state of loggers.
 38    ############################
 39    # def get_cruises(self):
 40    #   """Return list of cruise id's."""
 41    #   return list(self.cruise_configs)
 42
 43    ############################
 44    def get_configuration(self):
 45        """Return cruise config for specified cruise id."""
 46        return self.config or None
 47
 48    ############################
 49    def get_modes(self):
 50        """Return list of modes defined for given cruise."""
 51        return list(self.config.get('modes', []))
 52
 53    ############################
 54    def get_active_mode(self):
 55        """Return cruise config for specified cruise id."""
 56        return self.config.get('active_mode')
 57
 58    ############################
 59    def get_default_mode(self):
 60        """Get the name of the default mode for the specified cruise
 61        from the data store."""
 62        return self.config.get('default_mode')
 63
 64    ############################
 65    def get_logger(self, logger):
 66        """Retrieve the logger spec for the specified logger id."""
 67        loggers = self.get_loggers()
 68        if logger not in loggers:
 69            raise ValueError('No logger "%s" found' %
 70                             (logger))
 71        return loggers.get(logger)
 72
 73    ############################
 74    def get_loggers(self):
 75        """Get a dict of
 76            {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...}
 77        for all loggers.
 78        """
 79        config = self.get_configuration()
 80        if not config:
 81            return {}
 82
 83        if 'loggers' not in config:
 84            raise ValueError('No loggers found')
 85        logger_configs = config.get('loggers')
 86        if not logger_configs:
 87            raise ValueError('No logger configurations found')
 88
 89        # Fetch and insert the currently active config for each logger
 90        for logger in logger_configs:
 91            logger_configs[logger]['active'] = self.get_logger_config_name(logger)
 92        return logger_configs
 93
 94    ############################
 95    def get_logger_config(self, config_name):
 96        """Retrieve the config associated with the specified name."""
 97        cruise_config = self.get_configuration()
 98        if cruise_config is None:
 99            return {}
100        logger_configs = cruise_config.get('configs')
101        if logger_configs is None:
102            raise ValueError('No "config" found')
103        logger_config = logger_configs.get(config_name)
104        if logger_config is None:
105            raise ValueError('No logger config "%s" in config' % config_name)
106        return logger_config
107
108    ############################
109    def get_logger_configs(self, mode=None):
110        """Retrieve the configs associated with a cruise id and mode from the
111        data store. If mode is omitted, retrieve configs associated with
112        the cruise's current logger configs."""
113        loggers = self.get_loggers()
114        if not loggers:
115            return None
116
117        output = {}
118        for logger in loggers:
119            logger_config_name = self.get_logger_config_name(logger, mode)
120            output[logger] = self.get_logger_config(logger_config_name)
121
122        return output
123
124    ############################
125    def get_logger_config_name(self, logger_id, mode=None):
126        """Retrieve name of the config associated with the specified logger
127        in the specified mode. If mode is omitted, retrieve name of logger's
128        current config."""
129
130        if not self.config:
131            raise ValueError('No configuration loaded')
132
133        # If mode is not specified, get logger's current config name
134        if mode is None:
135            config_name = self.logger_config.get(logger_id)
136            if config_name is None:
137                raise ValueError(f'Logger id "{logger_id}" has no mode!')
138            return config_name
139
140        # Otherwise, we return the config_name for the specifed mode
141        modes = self.config.get('modes')
142        mode_configs = modes.get(mode)
143        if mode_configs is None:
144            raise ValueError('Requested mode %s is not defined (modes are: %s)' %
145                             (mode, [m for m in modes]))
146        logger_config_name = mode_configs.get(logger_id)
147        if logger_config_name is None:
148            raise ValueError('Logger %s has no config defined in mode %s' %
149                             (logger_id, mode))
150        return logger_config_name
151
152    #############################
153    def get_logger_config_names(self, logger_id):
154        """Retrieve list of config names that are valid for the specified logger .
155        > api.get_logger_config_names('NBP1406', 'knud')
156              ["off", "knud->net", "knud->net/file", "knud->net/file/db"]
157        """
158        logger = self.get_logger(logger_id)
159        return logger.get('configs', [])
160
161    ############################
162    # Methods for manipulating the desired state via API to indicate
163    # current mode and which loggers should be in which configs.
164    ############################
165    def set_active_mode(self, mode):
166        """Set the current mode of the specified cruise in the data store."""
167        modes = self.config.get('modes')
168        if not modes:
169            raise ValueError('Config has no modes??')
170        if mode not in modes:
171            raise ValueError('Config has no mode "%s"' % (mode))
172
173        self.config['active_mode'] = mode
174
175        # Update the stored {logger:config_name} dict to match new mode
176        # Here's a quick one-liner that doesn't do any checking:
177        self.logger_config = modes[mode].copy()
178
179        # Here's s slow carefully-checked way of setting configs:
180        # for logger, config in modes[mode].items():
181        #  self.set_logger_config_name(cruise_id, logger, config)
182
183        # Q: At this point should we could signal an update. Or we could
184        # count on the API calling signal_update(). Or count on the update
185        # being picked up by polling. For now, don't signal the update.
186
187        logging.info('Signaling update')
188        self.signal_update()
189
190    ############################
191    def set_active_logger_config(self, logger, config_name):
192        """Set specified logger to new config. NOTE: we have no way to check
193        whether logger is compatible with config, so we rely on whoever is
194        calling us to have made that determination."""
195        # if not cruise_id in self.logger_config:
196        #   self.logger_config[cruise_id] = {}
197        self.logger_config[logger] = config_name
198
199        logging.info('Signaling update')
200        self.signal_update()
201
202    ############################
203    # Methods for feeding data from LoggerServer back into the API
204    ############################
205    def update_status(self, status):
206        """Save/register the loggers' retrieved status report with the API."""
207        self.status.append((time.time(), status))
208
209    ############################
210    # Methods for getting status data from API
211    ############################
212
213    def get_status(self, since_timestamp=None):
214        """Retrieve a dict of the most-recent status report from each
215        logger. If since_timestamp is specified, retrieve all status reports
216        since that time."""
217
218        # Start by getting set of loggers for cruise. Store as
219        # cruise_id:logger for ease of lookup.
220        try:
221            logger_set = set([logger
222                              for logger in self.get_loggers()])
223        except ValueError:
224            logger_set = set()
225
226        logging.debug('logger_set: %s', logger_set)
227
228        # Step backwards through status messages until we run out of
229        # status messages or reach termination condition. If
230        # since_timestamp==None, our termination is when we have a status
231        # for each of our loggers. If since_timestamp is a number, our
232        # termination is when we've grabbed all the statuses with a
233        # timestamp greater than the specified number.
234        status = {}
235
236        status_index = len(self.status) - 1
237        logging.debug('starting at status index %d', status_index)
238        while logger_set and status_index >= 0:
239            # record is a dict of 'cruise_id:logger' : {fields}
240            (timestamp, record) = self.status[status_index]
241            logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record))
242
243            # If we've been given a numeric timestamp and we've stepped back
244            # in time to or before that timestamp, we're done - break out.
245            if since_timestamp is not None and timestamp <= since_timestamp:
246                break
247
248            # Otherwise, examine ids in this record to see if they're for
249            # the cruise in question.
250            for id, fields in record.items():
251                # If id is cruise_id:logger that we're interested in, grab it.
252                logging.debug('Is %s in %s?', id, logger_set)
253                if id in logger_set:
254                    if timestamp not in status:
255                        status[timestamp] = {}
256                    status[timestamp][id] = fields
257
258                    # If since_timestamp==None, we only want the latest status
259                    # for each logger. So once we've found it, remove the id
260                    # from the logger_set we're lookings. We'll drop out of the
261                    # loop when the set is empty.
262                    if since_timestamp is None:
263                        logger_set.discard(id)
264            status_index -= 1
265
266        return status
267
268    ############################
269    # Methods for storing/retrieving messages from servers/loggers/etc.
270    ############################
271    def message_log(self, source, user, log_level, message):
272        """Timestamp and store the passed message."""
273        self.server_messages.append((time.time(), source, user,
274                                     log_level, message))
275
276    ############################
277    def get_message_log(self, source=None, user=None, log_level=sys.maxsize,
278                        since_timestamp=None):
279        """Retrieve log messages from source at or above log_level since
280        timestamp. If source is omitted, retrieve from all sources. If
281        log_level is omitted, retrieve at all levels. If since_timestamp is
282        omitted, only retrieve most recent message.
283        """
284        index = len(self.server_messages) - 1
285        messages = []
286        while index >= 0:
287            message = self.server_messages[index]
288            (timestamp, mesg_source, mesg_user,
289             mesg_log_level, mesg_message) = message
290            # Have we gone back too far? If so, we're done.
291            if since_timestamp is not None and timestamp <= since_timestamp:
292                break
293
294            if mesg_log_level < log_level:
295                continue
296            if user and not mesg_user == user:
297                continue
298            if source and not mesg_source == source:
299                continue
300
301            messages.insert(0, message)
302
303            # Are we only looking for last message, and do we have a message?
304            if since_timestamp is None and messages:
305                break
306            index -= 1
307
308        return messages
309
310    #############################
311    # Methods to modify the data store
312    ############################
313    def load_configuration(self, config):
314        """Add a complete cruise configuration (id, modes, configs,
315        default) to the data store."""
316        self.config = config
317        self.config['loaded_time'] = datetime.datetime.utcnow()
318
319        # Some syntactic sugar to simplify config definitions
320        configs = self.config.get('configs')
321        for config_name, config in configs.items():
322            if config is None:
323                raise ValueError(f'No logger for "{config_name}" in cruise definition')
324            if 'name' not in config:
325                self.config['configs'][config_name]['name'] = config_name
326
327        # Set cruise into default mode, if one is defined
328        if 'default_mode' in self.config:
329            active_mode = self.config['default_mode']
330            self.set_active_mode(active_mode)
331
332        # Let anyone who's interested know that we've got new configurations.
333        self.signal_load()
334
335    ############################
336    def delete_configuration(self):
337        """Remove the specified cruise from the data store."""
338        self.config = {}
339        self.mode = "n/a"
340        self.logger_config = {}
341        self.callbacks = []
342        self.status = []
343
344    ############################
345    # Methods for manually constructing/modifying a cruise spec via API
346    # def add_cruise(self, cruise_id, start=None, end=None)
347    # def add_mode(self, cruise_id, mode)
348    # def delete_mode(self, cruise_id, mode)
349    # def add_logger(self, cruise_id, logger_id, logger_spec)
350    # def delete_logger(self, cruise_id, logger_id)
351    # def add_config(self, cruise_id, config, config_spec)
352    # def add_config_to_logger(self, cruise_id, config, logger_id)
353    # def add_config_to_mode(self, cruise_id, config, logger_id, mode)
354    # def delete_config(self, cruise_id, config_id)
DEFAULT_MAX_TRIES = 3
ID_SEPARATOR = ':'
class InMemoryServerAPI(server.server_api.ServerAPI):
 25class InMemoryServerAPI(ServerAPI):
 26    ############################
 27    def __init__(self):
 28        super().__init__()
 29        self.config = {}
 30        self.mode = "n/a"
 31        self.logger_config = {}
 32        self.callbacks = []
 33        self.status = []
 34        self.server_messages = []
 35
 36    #############################
 37    # API methods below are used in querying/modifying the API for the
 38    # record of the running state of loggers.
 39    ############################
 40    # def get_cruises(self):
 41    #   """Return list of cruise id's."""
 42    #   return list(self.cruise_configs)
 43
 44    ############################
 45    def get_configuration(self):
 46        """Return cruise config for specified cruise id."""
 47        return self.config or None
 48
 49    ############################
 50    def get_modes(self):
 51        """Return list of modes defined for given cruise."""
 52        return list(self.config.get('modes', []))
 53
 54    ############################
 55    def get_active_mode(self):
 56        """Return cruise config for specified cruise id."""
 57        return self.config.get('active_mode')
 58
 59    ############################
 60    def get_default_mode(self):
 61        """Get the name of the default mode for the specified cruise
 62        from the data store."""
 63        return self.config.get('default_mode')
 64
 65    ############################
 66    def get_logger(self, logger):
 67        """Retrieve the logger spec for the specified logger id."""
 68        loggers = self.get_loggers()
 69        if logger not in loggers:
 70            raise ValueError('No logger "%s" found' %
 71                             (logger))
 72        return loggers.get(logger)
 73
 74    ############################
 75    def get_loggers(self):
 76        """Get a dict of
 77            {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...}
 78        for all loggers.
 79        """
 80        config = self.get_configuration()
 81        if not config:
 82            return {}
 83
 84        if 'loggers' not in config:
 85            raise ValueError('No loggers found')
 86        logger_configs = config.get('loggers')
 87        if not logger_configs:
 88            raise ValueError('No logger configurations found')
 89
 90        # Fetch and insert the currently active config for each logger
 91        for logger in logger_configs:
 92            logger_configs[logger]['active'] = self.get_logger_config_name(logger)
 93        return logger_configs
 94
 95    ############################
 96    def get_logger_config(self, config_name):
 97        """Retrieve the config associated with the specified name."""
 98        cruise_config = self.get_configuration()
 99        if cruise_config is None:
100            return {}
101        logger_configs = cruise_config.get('configs')
102        if logger_configs is None:
103            raise ValueError('No "config" found')
104        logger_config = logger_configs.get(config_name)
105        if logger_config is None:
106            raise ValueError('No logger config "%s" in config' % config_name)
107        return logger_config
108
109    ############################
110    def get_logger_configs(self, mode=None):
111        """Retrieve the configs associated with a cruise id and mode from the
112        data store. If mode is omitted, retrieve configs associated with
113        the cruise's current logger configs."""
114        loggers = self.get_loggers()
115        if not loggers:
116            return None
117
118        output = {}
119        for logger in loggers:
120            logger_config_name = self.get_logger_config_name(logger, mode)
121            output[logger] = self.get_logger_config(logger_config_name)
122
123        return output
124
125    ############################
126    def get_logger_config_name(self, logger_id, mode=None):
127        """Retrieve name of the config associated with the specified logger
128        in the specified mode. If mode is omitted, retrieve name of logger's
129        current config."""
130
131        if not self.config:
132            raise ValueError('No configuration loaded')
133
134        # If mode is not specified, get logger's current config name
135        if mode is None:
136            config_name = self.logger_config.get(logger_id)
137            if config_name is None:
138                raise ValueError(f'Logger id "{logger_id}" has no mode!')
139            return config_name
140
141        # Otherwise, we return the config_name for the specifed mode
142        modes = self.config.get('modes')
143        mode_configs = modes.get(mode)
144        if mode_configs is None:
145            raise ValueError('Requested mode %s is not defined (modes are: %s)' %
146                             (mode, [m for m in modes]))
147        logger_config_name = mode_configs.get(logger_id)
148        if logger_config_name is None:
149            raise ValueError('Logger %s has no config defined in mode %s' %
150                             (logger_id, mode))
151        return logger_config_name
152
153    #############################
154    def get_logger_config_names(self, logger_id):
155        """Retrieve list of config names that are valid for the specified logger .
156        > api.get_logger_config_names('NBP1406', 'knud')
157              ["off", "knud->net", "knud->net/file", "knud->net/file/db"]
158        """
159        logger = self.get_logger(logger_id)
160        return logger.get('configs', [])
161
162    ############################
163    # Methods for manipulating the desired state via API to indicate
164    # current mode and which loggers should be in which configs.
165    ############################
166    def set_active_mode(self, mode):
167        """Set the current mode of the specified cruise in the data store."""
168        modes = self.config.get('modes')
169        if not modes:
170            raise ValueError('Config has no modes??')
171        if mode not in modes:
172            raise ValueError('Config has no mode "%s"' % (mode))
173
174        self.config['active_mode'] = mode
175
176        # Update the stored {logger:config_name} dict to match new mode
177        # Here's a quick one-liner that doesn't do any checking:
178        self.logger_config = modes[mode].copy()
179
180        # Here's s slow carefully-checked way of setting configs:
181        # for logger, config in modes[mode].items():
182        #  self.set_logger_config_name(cruise_id, logger, config)
183
184        # Q: At this point should we could signal an update. Or we could
185        # count on the API calling signal_update(). Or count on the update
186        # being picked up by polling. For now, don't signal the update.
187
188        logging.info('Signaling update')
189        self.signal_update()
190
191    ############################
192    def set_active_logger_config(self, logger, config_name):
193        """Set specified logger to new config. NOTE: we have no way to check
194        whether logger is compatible with config, so we rely on whoever is
195        calling us to have made that determination."""
196        # if not cruise_id in self.logger_config:
197        #   self.logger_config[cruise_id] = {}
198        self.logger_config[logger] = config_name
199
200        logging.info('Signaling update')
201        self.signal_update()
202
203    ############################
204    # Methods for feeding data from LoggerServer back into the API
205    ############################
206    def update_status(self, status):
207        """Save/register the loggers' retrieved status report with the API."""
208        self.status.append((time.time(), status))
209
210    ############################
211    # Methods for getting status data from API
212    ############################
213
214    def get_status(self, since_timestamp=None):
215        """Retrieve a dict of the most-recent status report from each
216        logger. If since_timestamp is specified, retrieve all status reports
217        since that time."""
218
219        # Start by getting set of loggers for cruise. Store as
220        # cruise_id:logger for ease of lookup.
221        try:
222            logger_set = set([logger
223                              for logger in self.get_loggers()])
224        except ValueError:
225            logger_set = set()
226
227        logging.debug('logger_set: %s', logger_set)
228
229        # Step backwards through status messages until we run out of
230        # status messages or reach termination condition. If
231        # since_timestamp==None, our termination is when we have a status
232        # for each of our loggers. If since_timestamp is a number, our
233        # termination is when we've grabbed all the statuses with a
234        # timestamp greater than the specified number.
235        status = {}
236
237        status_index = len(self.status) - 1
238        logging.debug('starting at status index %d', status_index)
239        while logger_set and status_index >= 0:
240            # record is a dict of 'cruise_id:logger' : {fields}
241            (timestamp, record) = self.status[status_index]
242            logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record))
243
244            # If we've been given a numeric timestamp and we've stepped back
245            # in time to or before that timestamp, we're done - break out.
246            if since_timestamp is not None and timestamp <= since_timestamp:
247                break
248
249            # Otherwise, examine ids in this record to see if they're for
250            # the cruise in question.
251            for id, fields in record.items():
252                # If id is cruise_id:logger that we're interested in, grab it.
253                logging.debug('Is %s in %s?', id, logger_set)
254                if id in logger_set:
255                    if timestamp not in status:
256                        status[timestamp] = {}
257                    status[timestamp][id] = fields
258
259                    # If since_timestamp==None, we only want the latest status
260                    # for each logger. So once we've found it, remove the id
261                    # from the logger_set we're lookings. We'll drop out of the
262                    # loop when the set is empty.
263                    if since_timestamp is None:
264                        logger_set.discard(id)
265            status_index -= 1
266
267        return status
268
269    ############################
270    # Methods for storing/retrieving messages from servers/loggers/etc.
271    ############################
272    def message_log(self, source, user, log_level, message):
273        """Timestamp and store the passed message."""
274        self.server_messages.append((time.time(), source, user,
275                                     log_level, message))
276
277    ############################
278    def get_message_log(self, source=None, user=None, log_level=sys.maxsize,
279                        since_timestamp=None):
280        """Retrieve log messages from source at or above log_level since
281        timestamp. If source is omitted, retrieve from all sources. If
282        log_level is omitted, retrieve at all levels. If since_timestamp is
283        omitted, only retrieve most recent message.
284        """
285        index = len(self.server_messages) - 1
286        messages = []
287        while index >= 0:
288            message = self.server_messages[index]
289            (timestamp, mesg_source, mesg_user,
290             mesg_log_level, mesg_message) = message
291            # Have we gone back too far? If so, we're done.
292            if since_timestamp is not None and timestamp <= since_timestamp:
293                break
294
295            if mesg_log_level < log_level:
296                continue
297            if user and not mesg_user == user:
298                continue
299            if source and not mesg_source == source:
300                continue
301
302            messages.insert(0, message)
303
304            # Are we only looking for last message, and do we have a message?
305            if since_timestamp is None and messages:
306                break
307            index -= 1
308
309        return messages
310
311    #############################
312    # Methods to modify the data store
313    ############################
314    def load_configuration(self, config):
315        """Add a complete cruise configuration (id, modes, configs,
316        default) to the data store."""
317        self.config = config
318        self.config['loaded_time'] = datetime.datetime.utcnow()
319
320        # Some syntactic sugar to simplify config definitions
321        configs = self.config.get('configs')
322        for config_name, config in configs.items():
323            if config is None:
324                raise ValueError(f'No logger for "{config_name}" in cruise definition')
325            if 'name' not in config:
326                self.config['configs'][config_name]['name'] = config_name
327
328        # Set cruise into default mode, if one is defined
329        if 'default_mode' in self.config:
330            active_mode = self.config['default_mode']
331            self.set_active_mode(active_mode)
332
333        # Let anyone who's interested know that we've got new configurations.
334        self.signal_load()
335
336    ############################
337    def delete_configuration(self):
338        """Remove the specified cruise from the data store."""
339        self.config = {}
340        self.mode = "n/a"
341        self.logger_config = {}
342        self.callbacks = []
343        self.status = []
344
345    ############################
346    # Methods for manually constructing/modifying a cruise spec via API
347    # def add_cruise(self, cruise_id, start=None, end=None)
348    # def add_mode(self, cruise_id, mode)
349    # def delete_mode(self, cruise_id, mode)
350    # def add_logger(self, cruise_id, logger_id, logger_spec)
351    # def delete_logger(self, cruise_id, logger_id)
352    # def add_config(self, cruise_id, config, config_spec)
353    # def add_config_to_logger(self, cruise_id, config, logger_id)
354    # def add_config_to_mode(self, cruise_id, config, logger_id, mode)
355    # def delete_config(self, cruise_id, config_id)

Abstract base class defining an API through which a LoggerServer can interact with a data store.

Parameters below have the following semantics:

configuration - dict definition of a OpenRVDAS configuration
mode          - dict name of mode, and logger_config_names associated with that mode
default_mode  - name of mode to use at startup or when returning to a default state

logger_config_name - string name of a logger configuration, unique within configration
logger_config - dict definition of a logger configuration

logger_id     - string name of logger, unique within cruise
logger        - dict definition of logger, including list of names of
                valid configs and optional host restriction

logger_configs - dict of {logger_config_name:logger_config,...}

For the purposes of documentation below, assume a sample cruise_config as follows:

{
  "loggers": {
    "knud": {
      "host": "knud.pi",
      "configs": ["off", "knud->net", "knud->file/net/db"]
    },
    "gyr1": {
      "configs": ["off", "gyr1->net", "gyr1->file/net/db"]
    },
  "modes": {
    "off": {"knud": "off", "gyr1": "off"},
    "port": {"knud": "off", "gyr1": "gyr1->net"},
    "underway": { "knud": "knud->file/net/db",
                  "gyr1": "gyr1->file/net/db"
                }
  },
  "default_mode": "off",
  "configs": {
    "off": {},
    "knud->net": { config_spec },
    "knud->file/net/db": { config_spec },
    "gyr1->net": { config_spec },
    "gyr1->file/net/dbnet": { config_spec }
  }
}
config
mode
logger_config
callbacks
status
server_messages
def get_configuration(self):
45    def get_configuration(self):
46        """Return cruise config for specified cruise id."""
47        return self.config or None

Return cruise config for specified cruise id.

def get_modes(self):
50    def get_modes(self):
51        """Return list of modes defined for given cruise."""
52        return list(self.config.get('modes', []))

Return list of modes defined for given cruise.

def get_active_mode(self):
55    def get_active_mode(self):
56        """Return cruise config for specified cruise id."""
57        return self.config.get('active_mode')

Return cruise config for specified cruise id.

def get_default_mode(self):
60    def get_default_mode(self):
61        """Get the name of the default mode for the specified cruise
62        from the data store."""
63        return self.config.get('default_mode')

Get the name of the default mode for the specified cruise from the data store.

def get_logger(self, logger):
66    def get_logger(self, logger):
67        """Retrieve the logger spec for the specified logger id."""
68        loggers = self.get_loggers()
69        if logger not in loggers:
70            raise ValueError('No logger "%s" found' %
71                             (logger))
72        return loggers.get(logger)

Retrieve the logger spec for the specified logger id.

def get_loggers(self):
75    def get_loggers(self):
76        """Get a dict of
77            {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...}
78        for all loggers.
79        """
80        config = self.get_configuration()
81        if not config:
82            return {}
83
84        if 'loggers' not in config:
85            raise ValueError('No loggers found')
86        logger_configs = config.get('loggers')
87        if not logger_configs:
88            raise ValueError('No logger configurations found')
89
90        # Fetch and insert the currently active config for each logger
91        for logger in logger_configs:
92            logger_configs[logger]['active'] = self.get_logger_config_name(logger)
93        return logger_configs

Get a dict of {logger_id:{'configs':[,,...], 'active':},...} for all loggers.

def get_logger_config(self, config_name):
 96    def get_logger_config(self, config_name):
 97        """Retrieve the config associated with the specified name."""
 98        cruise_config = self.get_configuration()
 99        if cruise_config is None:
100            return {}
101        logger_configs = cruise_config.get('configs')
102        if logger_configs is None:
103            raise ValueError('No "config" found')
104        logger_config = logger_configs.get(config_name)
105        if logger_config is None:
106            raise ValueError('No logger config "%s" in config' % config_name)
107        return logger_config

Retrieve the config associated with the specified name.

def get_logger_configs(self, mode=None):
110    def get_logger_configs(self, mode=None):
111        """Retrieve the configs associated with a cruise id and mode from the
112        data store. If mode is omitted, retrieve configs associated with
113        the cruise's current logger configs."""
114        loggers = self.get_loggers()
115        if not loggers:
116            return None
117
118        output = {}
119        for logger in loggers:
120            logger_config_name = self.get_logger_config_name(logger, mode)
121            output[logger] = self.get_logger_config(logger_config_name)
122
123        return output

Retrieve the configs associated with a cruise id and mode from the data store. If mode is omitted, retrieve configs associated with the cruise's current logger configs.

def get_logger_config_name(self, logger_id, mode=None):
126    def get_logger_config_name(self, logger_id, mode=None):
127        """Retrieve name of the config associated with the specified logger
128        in the specified mode. If mode is omitted, retrieve name of logger's
129        current config."""
130
131        if not self.config:
132            raise ValueError('No configuration loaded')
133
134        # If mode is not specified, get logger's current config name
135        if mode is None:
136            config_name = self.logger_config.get(logger_id)
137            if config_name is None:
138                raise ValueError(f'Logger id "{logger_id}" has no mode!')
139            return config_name
140
141        # Otherwise, we return the config_name for the specifed mode
142        modes = self.config.get('modes')
143        mode_configs = modes.get(mode)
144        if mode_configs is None:
145            raise ValueError('Requested mode %s is not defined (modes are: %s)' %
146                             (mode, [m for m in modes]))
147        logger_config_name = mode_configs.get(logger_id)
148        if logger_config_name is None:
149            raise ValueError('Logger %s has no config defined in mode %s' %
150                             (logger_id, mode))
151        return logger_config_name

Retrieve name of the config associated with the specified logger in the specified mode. If mode is omitted, retrieve name of logger's current config.

def get_logger_config_names(self, logger_id):
154    def get_logger_config_names(self, logger_id):
155        """Retrieve list of config names that are valid for the specified logger .
156        > api.get_logger_config_names('NBP1406', 'knud')
157              ["off", "knud->net", "knud->net/file", "knud->net/file/db"]
158        """
159        logger = self.get_logger(logger_id)
160        return logger.get('configs', [])

Retrieve list of config names that are valid for the specified logger .

api.get_logger_config_names('NBP1406', 'knud') ["off", "knud->net", "knud->net/file", "knud->net/file/db"]

def set_active_mode(self, mode):
166    def set_active_mode(self, mode):
167        """Set the current mode of the specified cruise in the data store."""
168        modes = self.config.get('modes')
169        if not modes:
170            raise ValueError('Config has no modes??')
171        if mode not in modes:
172            raise ValueError('Config has no mode "%s"' % (mode))
173
174        self.config['active_mode'] = mode
175
176        # Update the stored {logger:config_name} dict to match new mode
177        # Here's a quick one-liner that doesn't do any checking:
178        self.logger_config = modes[mode].copy()
179
180        # Here's s slow carefully-checked way of setting configs:
181        # for logger, config in modes[mode].items():
182        #  self.set_logger_config_name(cruise_id, logger, config)
183
184        # Q: At this point should we could signal an update. Or we could
185        # count on the API calling signal_update(). Or count on the update
186        # being picked up by polling. For now, don't signal the update.
187
188        logging.info('Signaling update')
189        self.signal_update()

Set the current mode of the specified cruise in the data store.

def set_active_logger_config(self, logger, config_name):
192    def set_active_logger_config(self, logger, config_name):
193        """Set specified logger to new config. NOTE: we have no way to check
194        whether logger is compatible with config, so we rely on whoever is
195        calling us to have made that determination."""
196        # if not cruise_id in self.logger_config:
197        #   self.logger_config[cruise_id] = {}
198        self.logger_config[logger] = config_name
199
200        logging.info('Signaling update')
201        self.signal_update()

Set specified logger to new config. NOTE: we have no way to check whether logger is compatible with config, so we rely on whoever is calling us to have made that determination.

def update_status(self, status):
206    def update_status(self, status):
207        """Save/register the loggers' retrieved status report with the API."""
208        self.status.append((time.time(), status))

Save/register the loggers' retrieved status report with the API.

def get_status(self, since_timestamp=None):
214    def get_status(self, since_timestamp=None):
215        """Retrieve a dict of the most-recent status report from each
216        logger. If since_timestamp is specified, retrieve all status reports
217        since that time."""
218
219        # Start by getting set of loggers for cruise. Store as
220        # cruise_id:logger for ease of lookup.
221        try:
222            logger_set = set([logger
223                              for logger in self.get_loggers()])
224        except ValueError:
225            logger_set = set()
226
227        logging.debug('logger_set: %s', logger_set)
228
229        # Step backwards through status messages until we run out of
230        # status messages or reach termination condition. If
231        # since_timestamp==None, our termination is when we have a status
232        # for each of our loggers. If since_timestamp is a number, our
233        # termination is when we've grabbed all the statuses with a
234        # timestamp greater than the specified number.
235        status = {}
236
237        status_index = len(self.status) - 1
238        logging.debug('starting at status index %d', status_index)
239        while logger_set and status_index >= 0:
240            # record is a dict of 'cruise_id:logger' : {fields}
241            (timestamp, record) = self.status[status_index]
242            logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record))
243
244            # If we've been given a numeric timestamp and we've stepped back
245            # in time to or before that timestamp, we're done - break out.
246            if since_timestamp is not None and timestamp <= since_timestamp:
247                break
248
249            # Otherwise, examine ids in this record to see if they're for
250            # the cruise in question.
251            for id, fields in record.items():
252                # If id is cruise_id:logger that we're interested in, grab it.
253                logging.debug('Is %s in %s?', id, logger_set)
254                if id in logger_set:
255                    if timestamp not in status:
256                        status[timestamp] = {}
257                    status[timestamp][id] = fields
258
259                    # If since_timestamp==None, we only want the latest status
260                    # for each logger. So once we've found it, remove the id
261                    # from the logger_set we're lookings. We'll drop out of the
262                    # loop when the set is empty.
263                    if since_timestamp is None:
264                        logger_set.discard(id)
265            status_index -= 1
266
267        return status

Retrieve a dict of the most-recent status report from each logger. If since_timestamp is specified, retrieve all status reports since that time.

def message_log(self, source, user, log_level, message):
272    def message_log(self, source, user, log_level, message):
273        """Timestamp and store the passed message."""
274        self.server_messages.append((time.time(), source, user,
275                                     log_level, message))

Timestamp and store the passed message.

def get_message_log( self, source=None, user=None, log_level=9223372036854775807, since_timestamp=None):
278    def get_message_log(self, source=None, user=None, log_level=sys.maxsize,
279                        since_timestamp=None):
280        """Retrieve log messages from source at or above log_level since
281        timestamp. If source is omitted, retrieve from all sources. If
282        log_level is omitted, retrieve at all levels. If since_timestamp is
283        omitted, only retrieve most recent message.
284        """
285        index = len(self.server_messages) - 1
286        messages = []
287        while index >= 0:
288            message = self.server_messages[index]
289            (timestamp, mesg_source, mesg_user,
290             mesg_log_level, mesg_message) = message
291            # Have we gone back too far? If so, we're done.
292            if since_timestamp is not None and timestamp <= since_timestamp:
293                break
294
295            if mesg_log_level < log_level:
296                continue
297            if user and not mesg_user == user:
298                continue
299            if source and not mesg_source == source:
300                continue
301
302            messages.insert(0, message)
303
304            # Are we only looking for last message, and do we have a message?
305            if since_timestamp is None and messages:
306                break
307            index -= 1
308
309        return messages

Retrieve log messages from source at or above log_level since timestamp. If source is omitted, retrieve from all sources. If log_level is omitted, retrieve at all levels. If since_timestamp is omitted, only retrieve most recent message.

def load_configuration(self, config):
314    def load_configuration(self, config):
315        """Add a complete cruise configuration (id, modes, configs,
316        default) to the data store."""
317        self.config = config
318        self.config['loaded_time'] = datetime.datetime.utcnow()
319
320        # Some syntactic sugar to simplify config definitions
321        configs = self.config.get('configs')
322        for config_name, config in configs.items():
323            if config is None:
324                raise ValueError(f'No logger for "{config_name}" in cruise definition')
325            if 'name' not in config:
326                self.config['configs'][config_name]['name'] = config_name
327
328        # Set cruise into default mode, if one is defined
329        if 'default_mode' in self.config:
330            active_mode = self.config['default_mode']
331            self.set_active_mode(active_mode)
332
333        # Let anyone who's interested know that we've got new configurations.
334        self.signal_load()

Add a complete cruise configuration (id, modes, configs, default) to the data store.

def delete_configuration(self):
337    def delete_configuration(self):
338        """Remove the specified cruise from the data store."""
339        self.config = {}
340        self.mode = "n/a"
341        self.logger_config = {}
342        self.callbacks = []
343        self.status = []

Remove the specified cruise from the data store.