Some helpers without having a specific module
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. helpers (Helpers)
  6. =================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This module supports functions and classes which don't have an own Module (yet).
  11. **Submodules:**
  12. * :class:`helpers.continues_statistic`
  13. * :class:`helpers.continues_statistic_multivalue`
  14. * :class:`helpers.direct_socket_client`
  15. * :class:`helpers.direct_socket_server`
  16. * :class:`helpers.ringbuffer`
  17. **Unittest:**
  18. See also the :download:`unittest <helpers/_testresults_/unittest.pdf>` documentation.
  19. **Module Documentation:**
  20. """
  21. import logging
  22. import numbers
  23. import os
  24. import time
  25. import stringtools
  26. import task
  27. __DEPENDENCIES__ = ['stringtools', 'task', ]
  28. try:
  29. from config import APP_NAME as ROOT_LOGGER_NAME
  30. except ImportError:
  31. ROOT_LOGGER_NAME = 'root'
  32. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  33. __DESCRIPTION__ = """The Module {\\tt %s} is designed to help with some function which don't have an own Module (yet).
  34. For more Information read the documentation.""" % __name__.replace('_', '\_')
  35. """The Module Description"""
  36. __INTERPRETER__ = (2, 3)
  37. """The Tested Interpreter-Versions"""
  38. class continues_statistic(dict):
  39. """
  40. This class stores a statistic for a stream of values. The statistic includes: mean, min, max, quantifier.
  41. .. note:: You can use the mathematic operators "+, +=" to add a value to the statistic.
  42. :param mean: The mean start value, the start value or None.
  43. :type mean: numeric
  44. :param min_val: The min start value or None
  45. :type min_val: numeric
  46. :param max_val: The max start value or None
  47. :type max_val: numeric
  48. :param quantifier: The quantifier start value or 0
  49. :type quantifier: int
  50. .. note:: You are able to initialise this class in the following ways:
  51. * Without arguments to get an empty instance
  52. * With one single value to get an starting instance
  53. * With mean, min_val, max_val, quantifier to initialise an existing statistic
  54. **Example:**
  55. .. literalinclude:: helpers/_examples_/continues_statistic.py
  56. Will result to the following output:
  57. .. literalinclude:: helpers/_examples_/continues_statistic.log
  58. """
  59. def __init__(self, mean=None, min_val=None, max_val=None, quantifier=0):
  60. dict.__init__(self)
  61. if mean is None:
  62. self.__init_data__(None, None, None, 0)
  63. elif min_val is not None and max_val is not None and quantifier > 0:
  64. self.__init_data__(mean, min_val, max_val, quantifier)
  65. else:
  66. self.__init_data__(mean, mean, mean, 1)
  67. def __init_data__(self, mean, min_val, max_val, quantifier):
  68. self['quantifier'] = quantifier
  69. self['max_val'] = max_val
  70. self['min_val'] = min_val
  71. self['mean'] = mean
  72. def __add__(self, other):
  73. if self.quantifier == 0:
  74. return continues_statistic(**other)
  75. elif other.quantifier == 0:
  76. return continues_statistic(**self)
  77. else:
  78. return continues_statistic(
  79. mean=(self.mean * self.quantifier + other.mean * other.quantifier) / (self.quantifier + other.quantifier),
  80. min_val=min(self.min, other.min),
  81. max_val=max(self.max, other.max),
  82. quantifier=self.quantifier + other.quantifier,
  83. )
  84. @property
  85. def mean(self):
  86. """
  87. The current mean value.
  88. """
  89. return self.get('mean')
  90. @property
  91. def min(self):
  92. """
  93. The current min value.
  94. """
  95. return self.get('min_val')
  96. @property
  97. def max(self):
  98. """
  99. The current max value.
  100. """
  101. return self.get('max_val')
  102. @property
  103. def quantifier(self):
  104. """
  105. The current quantifier (number of values for continues statistic).
  106. """
  107. return self.get('quantifier')
  108. def pop(self):
  109. """
  110. This pops out the current statistic data and returns these as an instance of :class:`helpers.continues_statistic`.
  111. :returns: The current statistic or None, if no value is available.
  112. :rtype: :class:`helpers.continues_statistic` or None
  113. """
  114. if self.quantifier == 0:
  115. return None
  116. else:
  117. rv = continues_statistic(self.mean, self.min, self.max, self.quantifier)
  118. self.__init_data__(None, None, None, 0)
  119. return rv
  120. def __str__(self):
  121. return "mean=%(mean)s, min=%(min_val)s, max=%(max_val)s, quantifier=%(quantifier)s" % self
  122. class continues_statistic_multivalue(dict):
  123. """
  124. This class stores multiple statistics of stream values. The statistic of each value is stored as :class:`helpers.continues_statistic`.
  125. .. note:: You can use the mathematic operators "+, +=" to add instances.
  126. .. note:: You are able to initialise this class in the following ways:
  127. * Without arguments to get an empty instance
  128. * With a keword argument(s) which will be passed to :class:`continues_statistic`
  129. * With a dict equivalent of :class:`continues_statistic_multivalue` to initialise an existing statistic
  130. **Example:**
  131. .. literalinclude:: helpers/_examples_/continues_statistic_multivalue.py
  132. Will result to the following output:
  133. .. literalinclude:: helpers/_examples_/continues_statistic_multivalue.log
  134. """
  135. def __init__(self, *args, **kwargs):
  136. dict.__init__(self)
  137. if len(args) == 0 and len(kwargs) >= 0:
  138. for key in kwargs:
  139. if type(kwargs[key]) is continues_statistic:
  140. self[key] = kwargs[key]
  141. elif type(kwargs[key]) is dict:
  142. self[key] = continues_statistic(**kwargs[key])
  143. else:
  144. self[key] = continues_statistic(kwargs[key])
  145. elif len(args) == 1 and len(kwargs) == 0:
  146. for key in args[0]:
  147. self[key] = continues_statistic(**args[0][key])
  148. else:
  149. raise TypeError("No valid initialisation value(s)!")
  150. def __add__(self, other):
  151. rv_dict = {}
  152. for key in set(list(self.keys()) + list(other.keys())):
  153. rv_dict[key] = self.get(key, continues_statistic()) + other.get(key, continues_statistic())
  154. return continues_statistic_multivalue(**rv_dict)
  155. def pop(self, key=None):
  156. """
  157. This pops out the current statistic data for a given key and returns these as an instance of :class:`helpers.continues_statistic`.
  158. If no key is given it pops out the current statistic data and returns these as an instance of :class:`helpers.continues_statistic_multiple`.
  159. :param key: The key to get data for
  160. :type key: str or NoneType
  161. :returns: The current statistic or None, if no value is available.
  162. :rtype: :class:`helpers.continues_statistic` or None
  163. """
  164. if key is None:
  165. if len(self) == 0:
  166. return None
  167. else:
  168. rv = continues_statistic_multivalue(**self)
  169. self.clear()
  170. return rv
  171. else:
  172. return self[key].pop()
  173. def __str__(self):
  174. if len(self) == 0:
  175. return '-'
  176. else:
  177. return '\n'.join([key + ': ' + str(self[key]) for key in self])
  178. class direct_socket_base(object):
  179. """
  180. This is the base class for other classes in this module.
  181. """
  182. DEFAULT_CHANNEL_NAME = 'all_others'
  183. def __init__(self, max_len=None, virtual_rate_bps=None):
  184. self.__max_length__ = max_len
  185. self.__rate_bps__ = virtual_rate_bps
  186. #
  187. self.init_channel_name()
  188. self.__queue__ = task.threaded_queue()
  189. self.__queue__.run()
  190. #
  191. self.__remote_socket__ = None
  192. self.__last_remote_socket__ = None
  193. self.__data_callback__ = None
  194. self.__connect_callback__ = None
  195. self.__disconnect_callback__ = None
  196. #
  197. self.__clean_buffer__()
  198. def __chunks__(self, data):
  199. chunks = []
  200. if self.__max_length__ is None:
  201. chunks.append(data)
  202. else:
  203. for i in range(0, len(data), self.__max_length__):
  204. chunks.append(data[i:i + self.__max_length__])
  205. return chunks
  206. def __clean_buffer__(self):
  207. self.__rx_buffer__ = b''
  208. self.logger.info('%s Cleaning RX-Buffer...', self.__log_prefix__())
  209. def __connect__(self, remote_socket):
  210. if self.__remote_socket__ is None:
  211. self.__remote_socket__ = remote_socket
  212. self.logger.info('%s Connection established...', self.__log_prefix__())
  213. self.__clean_buffer__()
  214. if self.__connect_callback__ is not None:
  215. self.__connect_callback__()
  216. remote_socket.__connect__(self)
  217. def __log_prefix__(self):
  218. return 'Client:' if self.IS_CLIENT else 'Server:'
  219. def __rx__(self, data):
  220. self.__rx_buffer__ += data
  221. self.logger.debug('%s RX <- %s', self.__log_prefix__(), stringtools.hexlify(data))
  222. if self.__data_callback__ is not None:
  223. self.__data_callback__(self)
  224. def __tx__(self, q, data):
  225. self.logger.debug('%s TX -> %s', self.__log_prefix__(), stringtools.hexlify(data))
  226. if self.__rate_bps__ is not None:
  227. time.sleep(len(data) / self.__rate_bps__)
  228. self.__remote_socket__.__rx__(data)
  229. def disconnect(self):
  230. """
  231. Method to disconnect client and server.
  232. """
  233. if self.__remote_socket__ is not None:
  234. self.__last_remote_socket__ = self.__remote_socket__
  235. self.__remote_socket__ = None
  236. self.logger.info('%s Connection Lost...', self.__log_prefix__())
  237. if self.__disconnect_callback__ is not None:
  238. self.__disconnect_callback__()
  239. self.__last_remote_socket__.disconnect()
  240. def init_channel_name(self, channel_name=None):
  241. """
  242. With this Method, the channel name for logging can be changed.
  243. :param channel_name: The name for the logging channel
  244. :type channel_name: str
  245. """
  246. if channel_name is None:
  247. self.logger = logger.getChild(self.DEFAULT_CHANNEL_NAME)
  248. else:
  249. self.logger = logger.getChild(channel_name)
  250. def is_connected(self):
  251. """
  252. With this Method the connection status can be identified.
  253. :return: True, if a connection is established, otherwise False.
  254. :rtype: bool
  255. """
  256. return self.__remote_socket__ is not None
  257. def receive(self, timeout=1.0, num=None):
  258. """
  259. This method returns received data.
  260. :param timeout: The timeout for receiving data (at least after the timeout the method returns data or None).
  261. :type timeout: float
  262. :param num: the number of bytes to receive (use None to get all available data).
  263. :type num: int or None
  264. :return: The received data.
  265. :rtype: bytes
  266. """
  267. i = 0
  268. while len(self.__rx_buffer__) < (num or 1) and i < timeout * 10:
  269. i += 1
  270. time.sleep(.1)
  271. if len(self.__rx_buffer__) < (num or 1):
  272. return b''
  273. else:
  274. if num is None:
  275. rv = self.__rx_buffer__
  276. self.__rx_buffer__ = b''
  277. else:
  278. rv = self.__rx_buffer__[:num]
  279. self.__rx_buffer__ = self.__rx_buffer__[num:]
  280. return rv
  281. def register_callback(self, callback):
  282. """
  283. This method stores the callback which is executed, if data is available. You need to execute :func:`receive` of this instance
  284. given as first argument.
  285. :param callback: The callback which will be executed, when data is available.
  286. :type callback:
  287. """
  288. self.__data_callback__ = callback
  289. def register_connect_callback(self, callback):
  290. """
  291. This method stores the callback which is executed, if a connection is established.
  292. :param callback: The callback which will be executed, when a connection is established.
  293. :type callback:
  294. """
  295. self.__connect_callback__ = callback
  296. def register_disconnect_callback(self, callback):
  297. """
  298. This method stores the callback which is executed, after the connection is lost.
  299. :param callback: The callback which will be executed, after the connection is lost.
  300. :type callback:
  301. """
  302. self.__disconnect_callback__ = callback
  303. def send(self, data, timeout=1.0):
  304. """
  305. This method sends data via the initiated communication channel.
  306. :param data: The data to be send over the communication channel.
  307. :type data: bytes
  308. :param timeout: The timeout for sending data (e.g. time to establish new connection).
  309. :type timeout: float
  310. :param log_lvl: The log level to log outgoing TX-data
  311. :type log_lvl: int
  312. :return: True if data had been sent, otherwise False.
  313. :rtype: bool
  314. """
  315. i = 0
  316. while not self.is_connected() and i < timeout * 10:
  317. i += 1
  318. time.sleep(.1)
  319. if not self.is_connected():
  320. return False
  321. else:
  322. for tx_data in self.__chunks__(data):
  323. self.__queue__.enqueue(1, self.__tx__, tx_data)
  324. return True
  325. class direct_socket_client(direct_socket_base):
  326. """
  327. Class to create a direct client socket. See also parent :class:`helpers.direct_socket_base`.
  328. **Example:**
  329. .. literalinclude:: helpers/_examples_/direct_socket.py
  330. Will result to the following output:
  331. .. literalinclude:: helpers/_examples_/direct_socket.log
  332. """
  333. IS_CLIENT = True
  334. def connect(self, remote_socket):
  335. """
  336. Method to create a connection between this client and a :class:`helpers.direct_socket_server` instance.
  337. :param remote_socket: The remote socket to connect to.
  338. :type remote_socket: :class:`helpers.direct_socket_server`
  339. """
  340. self.__connect__(remote_socket)
  341. def reconnect(self):
  342. """
  343. Method to do a reconnect.
  344. .. note:: The :const:`remote_socket` of the prefious :func:`connect` call will be used.
  345. """
  346. if self.__last_remote_socket__ is not None and self.__remote_socket__ is None:
  347. self.connect(self.__last_remote_socket__)
  348. return True
  349. return False
  350. class direct_socket_server(direct_socket_base):
  351. """
  352. Class to create a direct server socket. See also parent :class:`helpers.direct_socket_base`.
  353. **Example:**
  354. .. literalinclude:: helpers/_examples_/direct_socket.py
  355. Will result to the following output:
  356. .. literalinclude:: helpers/_examples_/direct_socket.log
  357. """
  358. IS_CLIENT = False
  359. def __init__(self, *args, **kwargs):
  360. direct_socket_base.__init__(self, *args, **kwargs)
  361. self.logger.info('%s Waiting for incomming connection', self.__log_prefix__())
  362. class ringbuffer(list):
  363. """
  364. Class for a list with a limited number of elements.
  365. .. note:: On adding Objects, the list will be reduced to the maximum length by deleting entries starting from index 0.
  366. **Example:**
  367. .. literalinclude:: helpers/_examples_/ringbuffer.py
  368. Will result to the following output:
  369. .. literalinclude:: helpers/_examples_/ringbuffer.log
  370. """
  371. def __init__(self, *args, **kwargs):
  372. self.__max_length__ = kwargs.pop('length')
  373. list.__init__(self, *args, **kwargs)
  374. self.__reduce_list__()
  375. def __reduce_list__(self):
  376. while len(self) > self.__max_length__:
  377. self.pop(0)
  378. def append(self, obj):
  379. rv = list.append(self, obj)
  380. self.__reduce_list__()
  381. return rv
  382. def extend(self, iterable):
  383. rv = list.extend(self, iterable)
  384. self.__reduce_list__()
  385. return rv