#!/usr/bin/env python # -*- coding: utf-8 -*- # """ helpers (Helpers) ================= **Author:** * Dirk Alders **Description:** This module supports functions and classes which don't have an own Module (yet). **Submodules:** * :class:`helpers.continues_statistic` * :class:`helpers.continues_statistic_multivalue` * :class:`helpers.direct_socket_client` * :class:`helpers.direct_socket_server` * :class:`helpers.ringbuffer` **Unittest:** See also the :download:`unittest ` documentation. **Module Documentation:** """ import logging import numbers import os import time import stringtools import task __DEPENDENCIES__ = ['stringtools', 'task', ] try: from config import APP_NAME as ROOT_LOGGER_NAME except ImportError: ROOT_LOGGER_NAME = 'root' logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) __DESCRIPTION__ = """The Module {\\tt %s} is designed to help with some function which don't have an own Module (yet). For more Information read the documentation.""" % __name__.replace('_', '\_') """The Module Description""" __INTERPRETER__ = (2, 3) """The Tested Interpreter-Versions""" class continues_statistic(dict): """ This class stores a statistic for a stream of values. The statistic includes: mean, min, max, quantifier. .. note:: You can use the mathematic operators "+, +=" to add a value to the statistic. :param mean: The mean start value, the start value or None. :type mean: numeric :param min_val: The min start value or None :type min_val: numeric :param max_val: The max start value or None :type max_val: numeric :param quantifier: The quantifier start value or 0 :type quantifier: int .. note:: You are able to initialise this class in the following ways: * Without arguments to get an empty instance * With one single value to get an starting instance * With mean, min_val, max_val, quantifier to initialise an existing statistic **Example:** .. literalinclude:: helpers/_examples_/continues_statistic.py Will result to the following output: .. literalinclude:: helpers/_examples_/continues_statistic.log """ def __init__(self, mean=None, min_val=None, max_val=None, quantifier=0): dict.__init__(self) if mean is None: self.__init_data__(None, None, None, 0) elif min_val is not None and max_val is not None and quantifier > 0: self.__init_data__(mean, min_val, max_val, quantifier) else: self.__init_data__(mean, mean, mean, 1) def __init_data__(self, mean, min_val, max_val, quantifier): self['quantifier'] = quantifier self['max_val'] = max_val self['min_val'] = min_val self['mean'] = mean def __add__(self, other): if self.quantifier == 0: return continues_statistic(**other) elif other.quantifier == 0: return continues_statistic(**self) else: return continues_statistic( mean=(self.mean * self.quantifier + other.mean * other.quantifier) / (self.quantifier + other.quantifier), min_val=min(self.min, other.min), max_val=max(self.max, other.max), quantifier=self.quantifier + other.quantifier, ) @property def mean(self): """ The current mean value. """ return self.get('mean') @property def min(self): """ The current min value. """ return self.get('min_val') @property def max(self): """ The current max value. """ return self.get('max_val') @property def quantifier(self): """ The current quantifier (number of values for continues statistic). """ return self.get('quantifier') def pop(self): """ This pops out the current statistic data and returns these as an instance of :class:`helpers.continues_statistic`. :returns: The current statistic or None, if no value is available. :rtype: :class:`helpers.continues_statistic` or None """ if self.quantifier == 0: return continues_statistic() else: rv = continues_statistic(self.mean, self.min, self.max, self.quantifier) self.__init_data__(None, None, None, 0) return rv def __str__(self): return "mean=%(mean)s, min=%(min_val)s, max=%(max_val)s, quantifier=%(quantifier)s" % self class continues_statistic_multivalue(dict): """ This class stores multiple statistics of stream values. The statistic of each value is stored as :class:`helpers.continues_statistic`. .. note:: You can use the mathematic operators "+, +=" to add instances. .. note:: You are able to initialise this class in the following ways: * Without arguments to get an empty instance * With a keword argument(s) which will be passed to :class:`continues_statistic` * With a dict equivalent of :class:`continues_statistic_multivalue` to initialise an existing statistic **Example:** .. literalinclude:: helpers/_examples_/continues_statistic_multivalue.py Will result to the following output: .. literalinclude:: helpers/_examples_/continues_statistic_multivalue.log """ def __init__(self, *args, **kwargs): dict.__init__(self) if len(args) == 0 and len(kwargs) >= 0: for key in kwargs: if type(kwargs[key]) is continues_statistic: self[key] = kwargs[key] elif type(kwargs[key]) is dict: self[key] = continues_statistic(**kwargs[key]) else: self[key] = continues_statistic(kwargs[key]) elif len(args) == 1 and len(kwargs) == 0: for key in args[0]: self[key] = continues_statistic(**args[0][key]) else: raise TypeError("No valid initialisation value(s)!") def __add__(self, other): rv_dict = {} for key in set(list(self.keys()) + list(other.keys())): rv_dict[key] = self.get(key, continues_statistic()) + other.get(key, continues_statistic()) return continues_statistic_multivalue(**rv_dict) def pop(self, key=None): """ This pops out the current statistic data for a given key and returns these as an instance of :class:`helpers.continues_statistic`. If no key is given it pops out the current statistic data and returns these as an instance of :class:`helpers.continues_statistic_multiple`. :param key: The key to get data for :type key: str or NoneType :returns: The current statistic or None, if no value is available. :rtype: :class:`helpers.continues_statistic` or None """ if key is None: if len(self) == 0: return continues_statistic_multivalue() else: rv = continues_statistic_multivalue(**self) self.clear() return rv else: return self[key].pop() def __str__(self): if len(self) == 0: return '-' else: return '\n'.join([key + ': ' + str(self[key]) for key in self]) class direct_socket_base(object): """ This is the base class for other classes in this module. """ DEFAULT_CHANNEL_NAME = 'all_others' IS_CLIENT = False def __init__(self, max_len=None, virtual_rate_bps=None): self.__max_length__ = max_len self.__rate_bps__ = virtual_rate_bps # self.init_channel_name() self.__queue__ = task.threaded_queue() self.__queue__.run() # self.__remote_socket__ = None self.__last_remote_socket__ = None self.__data_callback__ = None self.__connect_callback__ = None self.__disconnect_callback__ = None # self.__clean_buffer__() def __chunks__(self, data): chunks = [] if self.__max_length__ is None: chunks.append(data) else: for i in range(0, len(data), self.__max_length__): chunks.append(data[i:i + self.__max_length__]) return chunks def __clean_buffer__(self): self.__rx_buffer__ = b'' self.logger.debug('%s Cleaning up receive-buffer', self.__log_prefix__()) def __connect__(self, remote_socket): if self.__remote_socket__ is None: self.__remote_socket__ = remote_socket self.logger.info('%s Connection established...', self.__log_prefix__()) self.__clean_buffer__() if self.__connect_callback__ is not None: self.__connect_callback__() remote_socket.__connect__(self) def __log_prefix__(self): return 'comm-client:' if self.IS_CLIENT else 'comm-server:' def __rx__(self, data): self.__rx_buffer__ += data self.logger.info('%s RX <- %s', self.__log_prefix__(), stringtools.hexlify(data)) if self.__data_callback__ is not None: self.__data_callback__(self) def __tx__(self, q, data): self.logger.info('%s TX -> %s', self.__log_prefix__(), stringtools.hexlify(data)) if self.__rate_bps__ is not None: time.sleep(len(data) / self.__rate_bps__) self.__remote_socket__.__rx__(data) def disconnect(self): """ Method to disconnect client and server. """ if self.__remote_socket__ is not None: self.__last_remote_socket__ = self.__remote_socket__ self.__remote_socket__ = None self.logger.info('%s Connection Lost...', self.__log_prefix__()) if self.__disconnect_callback__ is not None: self.__disconnect_callback__() self.__last_remote_socket__.disconnect() def init_channel_name(self, channel_name=None): """ With this Method, the channel name for logging can be changed. :param channel_name: The name for the logging channel :type channel_name: str """ if channel_name is None: self.logger = logger.getChild(self.DEFAULT_CHANNEL_NAME) else: self.logger = logger.getChild(channel_name) def is_connected(self): """ With this Method the connection status can be identified. :return: True, if a connection is established, otherwise False. :rtype: bool """ return self.__remote_socket__ is not None def receive(self, timeout=1.0, num=None): """ This method returns received data. :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None). :type timeout: float :param num: the number of bytes to receive (use None to get all available data). :type num: int or None :return: The received data. :rtype: bytes """ i = 0 while len(self.__rx_buffer__) < (num or 1) and i < timeout * 10: i += 1 time.sleep(.1) if len(self.__rx_buffer__) < (num or 1): return self.__rx_buffer__[0:0] else: if num is None: rv = self.__rx_buffer__ self.__rx_buffer__ = rv[0:0] else: rv = self.__rx_buffer__[:num] self.__rx_buffer__ = self.__rx_buffer__[num:] return rv def register_callback(self, callback): """ This method stores the callback which is executed, if data is available. You need to execute :func:`receive` of this instance given as first argument. :param callback: The callback which will be executed, when data is available. :type callback: """ self.__data_callback__ = callback def register_connect_callback(self, callback): """ This method stores the callback which is executed, if a connection is established. :param callback: The callback which will be executed, when a connection is established. :type callback: """ self.__connect_callback__ = callback def register_disconnect_callback(self, callback): """ This method stores the callback which is executed, after the connection is lost. :param callback: The callback which will be executed, after the connection is lost. :type callback: """ self.__disconnect_callback__ = callback def send(self, data, timeout=1.0): """ This method sends data via the initiated communication channel. :param data: The data to be send over the communication channel. :type data: bytes :param timeout: The timeout for sending data (e.g. time to establish new connection). :type timeout: float :return: True if data had been sent, otherwise False. :rtype: bool """ i = 0 while not self.is_connected() and i < timeout * 10: i += 1 time.sleep(.1) if not self.is_connected(): return False else: for tx_data in self.__chunks__(data): self.__queue__.enqueue(1, self.__tx__, tx_data) return True class direct_socket_stp_base(direct_socket_base): IS_CLIENT = False def __init__(self, *args, **kwargs): direct_socket_base.__init__(self, *args, **kwargs) self.__stp_rx__ = stringtools.stp.stp() def __chunks__(self, data): return direct_socket_base.__chunks__(self, stringtools.stp.build_frame(data)) def __clean_buffer__(self): self.__rx_buffer__ = [] self.logger.debug('%s Cleaning up receive-buffer', self.__log_prefix__()) def __rx__(self, data): self.logger.debug('%s RX <- %s', self.__log_prefix__(), stringtools.hexlify(data)) msg = self.__stp_rx__.process(data) if len(msg) > 0: self.__rx_buffer__.extend(msg) if len(self.__rx_buffer__) > 0: if self.__data_callback__ is not None: self.__data_callback__(self) def receive(self, timeout=1): """ This method returns one received messages via the initiated communication channel. :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None). :type timeout: float :return: The received data. :rtype: bytes """ try: return direct_socket_base.receive(self, timeout=timeout, num=1)[0] except TypeError: return None class direct_socket_client(direct_socket_base): """ Class to create a direct client socket. See also parent :class:`helpers.direct_socket_base`. **Example:** .. literalinclude:: helpers/_examples_/direct_socket.py Will result to the following output: .. literalinclude:: helpers/_examples_/direct_socket.log """ IS_CLIENT = True def connect(self, remote_socket): """ Method to create a connection between this client and a :class:`helpers.direct_socket_server` instance. :param remote_socket: The remote socket to connect to. :type remote_socket: :class:`helpers.direct_socket_server` """ self.__connect__(remote_socket) def reconnect(self): """ Method to do a reconnect. .. note:: The :const:`remote_socket` of the prefious :func:`connect` call will be used. """ if self.__last_remote_socket__ is not None and self.__remote_socket__ is None: self.connect(self.__last_remote_socket__) return True return False class direct_socket_server(direct_socket_base): """ Class to create a direct server socket. See also parent :class:`helpers.direct_socket_base`. **Example:** .. literalinclude:: helpers/_examples_/direct_socket.py Will result to the following output: .. literalinclude:: helpers/_examples_/direct_socket.log """ IS_CLIENT = False def __init__(self, *args, **kwargs): direct_socket_base.__init__(self, *args, **kwargs) self.logger.info('%s Waiting for incomming connection', self.__log_prefix__()) class direct_socket_stp_client(direct_socket_stp_base): IS_CLIENT = True def connect(self, remote_socket): """ Method to create a connection between this client and a :class:`helpers.direct_socket_server` instance. :param remote_socket: The remote socket to connect to. :type remote_socket: :class:`helpers.direct_socket_server` """ self.__connect__(remote_socket) def reconnect(self): """ Method to do a reconnect. .. note:: The :const:`remote_socket` of the prefious :func:`connect` call will be used. """ if self.__last_remote_socket__ is not None and self.__remote_socket__ is None: self.connect(self.__last_remote_socket__) return True return False class direct_socket_stp_server(direct_socket_stp_base): IS_CLIENT = False def __init__(self, *args, **kwargs): direct_socket_stp_base.__init__(self, *args, **kwargs) self.logger.info('%s Waiting for incomming connection', self.__log_prefix__()) class ringbuffer(list): """ Class for a list with a limited number of elements. .. note:: On adding Objects, the list will be reduced to the maximum length by deleting entries starting from index 0. **Example:** .. literalinclude:: helpers/_examples_/ringbuffer.py Will result to the following output: .. literalinclude:: helpers/_examples_/ringbuffer.log """ def __init__(self, *args, **kwargs): self.__max_length__ = kwargs.pop('length') list.__init__(self, *args, **kwargs) self.__reduce_list__() def __reduce_list__(self): while len(self) > self.__max_length__: self.pop(0) def append(self, obj): rv = list.append(self, obj) self.__reduce_list__() return rv def extend(self, iterable): rv = list.extend(self, iterable) self.__reduce_list__() return rv