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