555 linhas
18 KiB
Python
555 linhas
18 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
"""
|
|
helpers (Helpers)
|
|
=================
|
|
|
|
**Author:**
|
|
|
|
* Dirk Alders <sudo-dirk@mount-mockery.de>
|
|
|
|
**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 <helpers/_testresults_/unittest.pdf>` 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
|