Python Library Task
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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. """
  4. task (Task Module)
  5. ==================
  6. **Author:**
  7. * Dirk Alders <sudo-dirk@mount-mockery.de>
  8. **Description:**
  9. This Module supports helpfull classes for queues, tasks, ...
  10. **Submodules:**
  11. * :class:`task.crontab`
  12. * :class:`task.delayed`
  13. * :class:`task.periodic`
  14. * :class:`task.queue`
  15. * :class:`task.threaded_queue`
  16. **Unittest:**
  17. See also the :download:`unittest <task/_testresults_/unittest.pdf>` documentation.
  18. **Module Documentation:**
  19. """
  20. __DEPENDENCIES__ = []
  21. import logging
  22. import sys
  23. import threading
  24. import time
  25. if sys.version_info >= (3, 0):
  26. from queue import PriorityQueue
  27. from queue import Empty
  28. else:
  29. from Queue import PriorityQueue
  30. from Queue import Empty
  31. try:
  32. from config import APP_NAME as ROOT_LOGGER_NAME
  33. except ImportError:
  34. ROOT_LOGGER_NAME = 'root'
  35. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  36. __DESCRIPTION__ = """The Module {\\tt %s} is designed to help with task issues like periodic tasks, delayed tasks, queues, threaded queues and crontabs.
  37. For more Information read the documentation.""" % __name__.replace('_', '\_')
  38. """The Module Description"""
  39. __INTERPRETER__ = (2, 3)
  40. """The Tested Interpreter-Versions"""
  41. class queue(object):
  42. """
  43. Class to execute queued callbacks.
  44. :param bool expire: The default value for expire. See also :py:func:`expire`.
  45. **Example:**
  46. .. literalinclude:: task/_examples_/tqueue.py
  47. Will result to the following output:
  48. .. literalinclude:: task/_examples_/tqueue.log
  49. """
  50. class job(object):
  51. def __init__(self, priority, callback, *args, **kwargs):
  52. self.time = time.time()
  53. self.priority = priority
  54. self.callback = callback
  55. self.args = args
  56. self.kwargs = kwargs
  57. def run(self, queue):
  58. self.callback(queue, *self.args, **self.kwargs)
  59. def __lt__(self, other):
  60. if self.priority != other.priority:
  61. return self.priority < other.priority
  62. else:
  63. return self.time < other.time
  64. def __init__(self, expire=True):
  65. self.__expire = expire
  66. self.__stop = False
  67. self.queue = PriorityQueue()
  68. def clean_queue(self):
  69. """
  70. This Methods removes all jobs from the queue.
  71. .. note:: Be aware that already running jobs will not be terminated.
  72. """
  73. while not self.queue.empty():
  74. try:
  75. self.queue.get(False)
  76. except Empty: # This block is hard to reach for a testcase, but is
  77. continue # needed, if the thread runs dry while cleaning the queue.
  78. self.queue.task_done()
  79. def enqueue(self, priority, callback, *args, **kwargs):
  80. """
  81. This enqueues a given callback.
  82. :param number priority: The priority indication number of this task. The lowest value will be queued first.
  83. :param callback callback: Callback to be executed
  84. :param args args: Arguments to be given to callback
  85. :param kwargs kwargs: Keword Arguments to be given to callback
  86. .. note:: Callback will get this instance as first argument, followed by :py:data:`args` und :py:data:`kwargs`.
  87. """
  88. self.queue.put(self.job(priority, callback, *args, **kwargs))
  89. def qsize(self):
  90. return self.queue.qsize()
  91. def run(self):
  92. """
  93. This starts the execution of the queued callbacks.
  94. """
  95. self.__stop = False
  96. while not self.__stop:
  97. try:
  98. self.queue.get(timeout=0.1).run(self)
  99. except Empty:
  100. if self.__expire:
  101. break
  102. if type(self) is threaded_queue:
  103. self.thread = None
  104. def expire(self):
  105. """
  106. This sets the expire flag. That means that the process will stop after queue gets empty.
  107. """
  108. self.__expire = True
  109. def stop(self):
  110. """
  111. This sets the stop flag. That means that the process will stop after finishing the active task.
  112. """
  113. self.__stop = True
  114. class threaded_queue(queue):
  115. """Class to execute queued callbacks in a background thread (See also parent :py:class:`queue`).
  116. :param bool expire: The default value for expire. See also :py:func:`queue.expire`.
  117. **Example:**
  118. .. literalinclude:: task/_examples_/threaded_queue.py
  119. Will result to the following output:
  120. .. literalinclude:: task/_examples_/threaded_queue.log
  121. """
  122. def __init__(self, expire=False):
  123. queue.__init__(self, expire=expire)
  124. self.thread = None
  125. def run(self):
  126. if self.thread is None:
  127. self.thread = threading.Thread(target=self._start, args=(), daemon=True)
  128. self.thread.daemon = True # Daemonize thread
  129. self.thread.start() # Start the execution
  130. def join(self):
  131. """
  132. This blocks till the queue is empty.
  133. .. note:: If the queue does not run dry, join will block till the end of the days.
  134. """
  135. self.expire()
  136. if self.thread is not None:
  137. self.thread.join()
  138. def stop(self):
  139. queue.stop(self)
  140. self.join()
  141. def _start(self):
  142. queue.run(self)
  143. class periodic(object):
  144. """
  145. Class to execute a callback cyclicly.
  146. :param float cycle_time: Cycle time in seconds -- callback will be executed every *cycle_time* seconds
  147. :param callback callback: Callback to be executed
  148. :param args args: Arguments to be given to the callback
  149. :param kwargs kwargs: Keword Arguments to be given to callback
  150. .. note:: The Callback will get this instance as first argument, followed by :py:data:`args` und :py:data:`kwargs`.
  151. **Example:**
  152. .. literalinclude:: task/_examples_/periodic.py
  153. Will result to the following output:
  154. .. literalinclude:: task/_examples_/periodic.log
  155. """
  156. def __init__(self, cycle_time, callback, *args, **kwargs):
  157. self._lock = threading.Lock()
  158. self._timer = None
  159. self.callback = callback
  160. self.cycle_time = cycle_time
  161. self.args = args
  162. self.kwargs = kwargs
  163. self._stopped = True
  164. self._last_tm = None
  165. self.dt = None
  166. def join(self):
  167. """
  168. This blocks till the cyclic task is terminated.
  169. .. note:: Using join means that somewhere has to be a condition calling :py:func:`stop` to terminate. Otherwise :func:`task.join` will never return.
  170. """
  171. while not self._stopped:
  172. time.sleep(.1)
  173. def run(self):
  174. """
  175. This starts the cyclic execution of the given callback.
  176. """
  177. if self._stopped:
  178. self._set_timer(force_now=True)
  179. def stop(self):
  180. """
  181. This stops the execution of any further task.
  182. """
  183. self._lock.acquire()
  184. self._stopped = True
  185. if self._timer is not None:
  186. self._timer.cancel()
  187. self._lock.release()
  188. def _set_timer(self, force_now=False):
  189. """
  190. This sets the timer for the execution of the next task.
  191. """
  192. self._lock.acquire()
  193. self._stopped = False
  194. if force_now:
  195. self._timer = threading.Timer(0, self._start)
  196. else:
  197. self._timer = threading.Timer(self.cycle_time, self._start)
  198. self._timer.daemon = True
  199. self._timer.start()
  200. self._lock.release()
  201. def _start(self):
  202. tm = time.time()
  203. if self._last_tm is not None:
  204. self.dt = tm - self._last_tm
  205. self._set_timer(force_now=False)
  206. self.callback(self, *self.args, **self.kwargs)
  207. self._last_tm = tm
  208. class delayed(periodic):
  209. """Class to execute a callback a given time in the future. See also parent :py:class:`periodic`.
  210. :param float time: Delay time for execution of the given callback
  211. :param callback callback: Callback to be executed
  212. :param args args: Arguments to be given to callback
  213. :param kwargs kwargs: Keword Arguments to be given to callback
  214. **Example:**
  215. .. literalinclude:: task/_examples_/delayed.py
  216. Will result to the following output:
  217. .. literalinclude:: task/_examples_/delayed.log
  218. """
  219. def run(self):
  220. """
  221. This starts the timer for the delayed execution.
  222. """
  223. self._set_timer(force_now=False)
  224. def _start(self):
  225. self.callback(*self.args, **self.kwargs)
  226. self.stop()
  227. class crontab(periodic):
  228. """Class to execute a callback at the specified time conditions. See also parent :py:class:`periodic`.
  229. :param accuracy: Repeat time in seconds for background task checking event triggering. This time is the maximum delay between specified time condition and the execution.
  230. :type accuracy: float
  231. **Example:**
  232. .. literalinclude:: task/_examples_/crontab.py
  233. Will result to the following output:
  234. .. literalinclude:: task/_examples_/crontab.log
  235. """
  236. ANY = '*'
  237. """Constant for matching every condition."""
  238. class cronjob(object):
  239. """Class to handle cronjob parameters and cronjob changes.
  240. :param minute: Minute for execution. Either 0...59, [0...59, 0...59, ...] or :py:const:`crontab.ANY` for every Minute.
  241. :type minute: int, list, str
  242. :param hour: Hour for execution. Either 0...23, [0...23, 0...23, ...] or :py:const:`crontab.ANY` for every Hour.
  243. :type hour: int, list, str
  244. :param day_of_month: Day of Month for execution. Either 0...31, [0...31, 0...31, ...] or :py:const:`crontab.ANY` for every Day of Month.
  245. :type day_of_month: int, list, str
  246. :param month: Month for execution. Either 0...12, [0...12, 0...12, ...] or :py:const:`crontab.ANY` for every Month.
  247. :type month: int, list, str
  248. :param day_of_week: Day of Week for execution. Either 0...6, [0...6, 0...6, ...] or :py:const:`crontab.ANY` for every Day of Week.
  249. :type day_of_week: int, list, str
  250. :param callback: The callback to be executed. The instance of :py:class:`cronjob` will be given as the first, args and kwargs as the following parameters.
  251. :type callback: func
  252. .. note:: This class should not be used stand alone. An instance will be created by adding a cronjob by using :py:func:`crontab.add_cronjob()`.
  253. """
  254. class all_match(set):
  255. """Universal set - match everything"""
  256. def __contains__(self, item):
  257. (item)
  258. return True
  259. def __init__(self, minute, hour, day_of_month, month, day_of_week, callback, *args, **kwargs):
  260. self.set_trigger_conditions(minute or crontab.ANY, hour or crontab.ANY,
  261. day_of_month or crontab.ANY, month or crontab.ANY, day_of_week or crontab.ANY)
  262. self.callback = callback
  263. self.args = args
  264. self.kwargs = kwargs
  265. self.__last_cron_check_time__ = None
  266. self.__last_execution__ = None
  267. def set_trigger_conditions(self, minute=None, hour=None, day_of_month=None, month=None, day_of_week=None):
  268. """This Method changes the execution parameters.
  269. :param minute: Minute for execution. Either 0...59, [0...59, 0...59, ...] or :py:const:`crontab.ANY` for every Minute.
  270. :type minute: int, list, str
  271. :param hour: Hour for execution. Either 0...23, [0...23, 0...23, ...] or :py:const:`crontab.ANY` for every Hour.
  272. :type hour: int, list, str
  273. :param day_of_month: Day of Month for execution. Either 0...31, [0...31, 0...31, ...] or :py:const:`crontab.ANY` for every Day of Month.
  274. :type day_of_month: int, list, str
  275. :param month: Month for execution. Either 0...12, [0...12, 0...12, ...] or :py:const:`crontab.ANY` for every Month.
  276. :type month: int, list, str
  277. :param day_of_week: Day of Week for execution. Either 0...6, [0...6, 0...6, ...] or :py:const:`crontab.ANY` for every Day of Week.
  278. :type day_of_week: int, list, str
  279. """
  280. if minute is not None:
  281. self.minute = self.__conv_to_set__(minute)
  282. if hour is not None:
  283. self.hour = self.__conv_to_set__(hour)
  284. if day_of_month is not None:
  285. self.day_of_month = self.__conv_to_set__(day_of_month)
  286. if month is not None:
  287. self.month = self.__conv_to_set__(month)
  288. if day_of_week is not None:
  289. self.day_of_week = self.__conv_to_set__(day_of_week)
  290. def __conv_to_set__(self, obj):
  291. if obj is crontab.ANY:
  292. return self.all_match()
  293. elif isinstance(obj, (int, long) if sys.version_info < (3, 0) else (int)):
  294. return set([obj])
  295. else:
  296. return set(obj)
  297. def __execution_needed_for__(self, minute, hour, day_of_month, month, day_of_week):
  298. if self.__last_execution__ != [minute, hour, day_of_month, month, day_of_week]:
  299. if minute in self.minute and hour in self.hour and day_of_month in self.day_of_month and month in self.month and day_of_week in self.day_of_week:
  300. return True
  301. return False
  302. def __store_execution_reminder__(self, minute, hour, day_of_month, month, day_of_week):
  303. self.__last_execution__ = [minute, hour, day_of_month, month, day_of_week]
  304. def cron_execution(self, tm):
  305. """This Methods executes the Cron-Callback, if a execution is needed for the given time (depending on the parameters on initialisation)
  306. :param tm: (Current) Time Value to be checked. The time needs to be given in seconds since 1970 (e.g. generated by int(time.time())).
  307. :type tm: int
  308. """
  309. if self.__last_cron_check_time__ is None:
  310. self.__last_cron_check_time__ = tm - 1
  311. #
  312. for t in range(self.__last_cron_check_time__ + 1, tm + 1):
  313. lt = time.localtime(t)
  314. if self.__execution_needed_for__(lt[4], lt[3], lt[2], lt[1], lt[6]):
  315. self.callback(self, *self.args, **self.kwargs)
  316. self.__store_execution_reminder__(lt[4], lt[3], lt[2], lt[1], lt[6])
  317. break
  318. self.__last_cron_check_time__ = tm
  319. def __init__(self, accuracy=30):
  320. periodic.__init__(self, accuracy, self.__periodic__)
  321. self.__crontab__ = []
  322. def __periodic__(self, rt):
  323. (rt)
  324. tm = int(time.time())
  325. for cronjob in self.__crontab__:
  326. cronjob.cron_execution(tm)
  327. def add_cronjob(self, minute, hour, day_of_month, month, day_of_week, callback, *args, **kwargs):
  328. """This Method adds a cronjob to be executed.
  329. :param minute: Minute for execution. Either 0...59, [0...59, 0...59, ...] or :py:const:`crontab.ANY` for every Minute.
  330. :type minute: int, list, str
  331. :param hour: Hour for execution. Either 0...23, [0...23, 0...23, ...] or :py:const:`crontab.ANY` for every Hour.
  332. :type hour: int, list, str
  333. :param day_of_month: Day of Month for execution. Either 0...31, [0...31, 0...31, ...] or :py:const:`crontab.ANY` for every Day of Month.
  334. :type day_of_month: int, list, str
  335. :param month: Month for execution. Either 0...12, [0...12, 0...12, ...] or :py:const:`crontab.ANY` for every Month.
  336. :type month: int, list, str
  337. :param day_of_week: Day of Week for execution. Either 0...6, [0...6, 0...6, ...] or :py:const:`crontab.ANY` for every Day of Week.
  338. :type day_of_week: int, list, str
  339. :param callback: The callback to be executed. The instance of :py:class:`cronjob` will be given as the first, args and kwargs as the following parameters.
  340. :type callback: func
  341. .. note:: The ``callback`` will be executed with it's instance of :py:class:`cronjob` as the first parameter.
  342. The given Arguments (:data:`args`) and keyword Arguments (:data:`kwargs`) will be stored in that object.
  343. """
  344. self.__crontab__.append(self.cronjob(minute, hour, day_of_month, month, day_of_week, callback, *args, **kwargs))