Ticket #2474: crontab.patch
| File crontab.patch, 12.8 KB (added by bochecha, 2 years ago) |
|---|
-
setup.py
20 20 license=license, 21 21 platforms=["any"], 22 22 zip_safe=False, 23 install_requires=[ 24 "python-dateutil" 25 ], 23 26 packages=packages, 24 27 #package_data=package_data, 25 28 keywords=[ -
tgscheduler/__init__.py
11 11 12 12 from scheduler import start_scheduler, stop_scheduler, \ 13 13 add_interval_task, add_monthly_task, add_single_task, \ 14 add_weekday_task, cancel14 add_weekday_task, add_cron_like_task, cancel -
tgscheduler/kronos.py
53 53 "ForkedSingleTask", 54 54 "ForkedTaskMixin", 55 55 "ForkedWeekdayTask", 56 "ForkedCronLikeTask", 56 57 "IntervalTask", 57 58 "MonthdayTask", 59 "CronLikeTask", 58 60 "Scheduler", 59 61 "SingleTask", 60 62 "Task", … … 64 66 "ThreadedSingleTask", 65 67 "ThreadedTaskMixin", 66 68 "ThreadedWeekdayTask", 69 "ThreadedCronLikeTask", 67 70 "WeekdayTask", 68 71 "add_interval_task", 69 72 "add_monthday_task", 70 73 "add_single_task", 71 74 "add_weekday_task", 75 "add_cron_like_task", 72 76 "cancel", 73 77 "method", 74 78 ] … … 83 87 import logging 84 88 log = logging.getLogger(__name__) 85 89 90 import datetime 91 from dateutil.rrule import rrule, SECONDLY 92 93 # bounds for each field of the cron-like syntax 94 MINUTE_BOUNDS = (0, 59) 95 HOUR_BOUNDS = (0, 23) 96 DOM_BOUNDS = (1, 31) 97 MONTH_BOUNDS = (1, 12) 98 DOW_BOUNDS = (0, 7) 99 100 # some fields of the cron-like syntax can be specified as names 101 MONTH_MAPPING = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 102 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} 103 DOW_MAPPING = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 104 'sat': 6} 105 106 86 107 class method: 87 108 sequential = "sequential" 88 109 forked = "forked" … … 259 280 self.schedule_task_abs(task, firsttime) 260 281 return task 261 282 283 def add_cron_like_task(self, action, taskname, cron_str, 284 processmethod, args, kw): 285 """ Add a new Cron-like Task to the schedule. """ 286 if not args: 287 args = [] 288 if not kw: 289 kw = {} 290 if processmethod == method.sequential: 291 TaskClass = CronLikeTask 292 elif processmethod == method.threaded: 293 TaskClass = ThreadedCronLikeTask 294 elif processmethod == method.forked: 295 TaskClass = ForkedCronLikeTask 296 else: 297 raise ValueError("Invalid processmethod") 298 if self.running: 299 self._acquire_lock() 300 try: 301 if self.tasks.has_key(taskname): 302 raise ValueError("A task with the name %s already exists" % taskname) 303 finally: 304 self._release_lock() 305 else: 306 if self.tasks.has_key(taskname): 307 raise ValueError("A task with the name %s already exists" % taskname) 308 task = TaskClass(taskname, action, cron_str, args, kw) 309 firsttime = task.get_schedule_time() 310 self.schedule_task_abs(task, firsttime) 311 return task 312 262 313 def schedule_task(self, task, delay): 263 314 """ 264 315 Add a new task to the scheduler with the given delay (seconds). … … 497 548 self.action(*self.args, **self.kw) 498 549 499 550 551 class CronLikeTask(Task): 552 """A class that is scheduled with a cron-like syntax. 553 """ 554 def __init__(self, name, action, cron_str, args=None, kw=None): 555 Task.__init__(self, name, action, args, kw) 556 557 self.cron_str = cron_str 558 try: 559 min_str, hour_str, dom_str, month_str, dow_str = cron_str.split() 560 except: 561 raise ValueError("Invalid value: %s" % cron_str) 562 563 self.minutes = self.__process_str(min_str, MINUTE_BOUNDS) 564 self.hours = self.__process_str(hour_str, HOUR_BOUNDS) 565 self.doms = self.__process_str(dom_str, DOM_BOUNDS) 566 self.months = self.__process_str(month_str, MONTH_BOUNDS, mapping=MONTH_MAPPING) 567 568 # dows are somewhat special: 569 # * Cron accepts both 0 and 7 for Sunday 570 # => we deal with that using the "% 7" operator and a temporary set 571 # * Python starts with Monday = 0 while Cron starts with Sunday = 0 572 # => we deal with that by substracting 1 573 # * (SUN - 1) % 7 = (0 - 1) % 7 = 6 574 # => we need to sort the list 575 self.dows = list(set([ (dow - 1) % 7 \ 576 for dow in self.__process_str(dow_str, 577 DOW_BOUNDS, mapping=DOW_MAPPING) ])) 578 self.dows.sort() 579 580 def __process_str(self, time_str, bounds, mapping=None): 581 """Transforms a field of the cron-like string into a list 582 583 Note: specifying a range as a mix of integers and names 584 (months and dows) is NOT supported 585 586 @param time_str: a field in the cron-like string 587 @type time_str: string 588 589 @param bounds: the acceptable limits for this field 590 @type bounds: 2-tuple of integers 591 592 @param mapping: the mapping between names and integer values 593 @type mapping: dict 594 """ 595 freq = 1 596 if '/' in time_str: 597 try: 598 time_str, freq = time_str.split('/') 599 freq = int(freq) 600 except: 601 raise ValueError("Invalid value: '%s'" % time_str) 602 603 if time_str == '*': 604 result = range(bounds[0], bounds[1] + 1) 605 return result[::freq] 606 607 result = list() 608 609 for item in time_str.split(','): 610 if not '-' in item: 611 # ex: time_str = "1,4,23" 612 try: 613 time = int(item) 614 except: 615 if mapping and mapping.has_key(item.lower()): 616 time = mapping[item.lower()] 617 else: 618 raise ValueError("Invalid value: '%s'" % time_str) 619 620 if time < bounds[0] or time > bounds [1]: 621 raise ValueError("Invalid value: '%s'" % time_str) 622 623 result.append(time) 624 else: 625 # ex: time_str = "1-4,7-9" 626 try: 627 interval_low, interval_high = item.split('-') 628 except: 629 # an interval can only have one dash 630 raise ValueError("Invalid value: '%s'" % time_str) 631 632 try: 633 # intervals are specified as integers 634 time_low = int(interval_low) 635 time_high = int(interval_high) 636 except: 637 if mapping and mapping.has_key(interval_low.lower()) \ 638 and mapping.has_key(interval_high.lower()): 639 # in some cases names can be used (months or dows) 640 time_low = mapping[interval_low.lower()] 641 time_high = mapping[interval_high.lower()] 642 else: 643 raise ValueError("Invalid value: '%s'" % time_str) 644 645 if time_low < bounds[0] or time_high > bounds [1]: 646 raise ValueError("Invalid value: '%s'" % time_str) 647 648 result.extend(range(time_low, time_high + 1)) 649 650 # filter results by frequency 651 return result[::freq] 652 653 def get_schedule_time(self): 654 """Determine the next execution time of the task. 655 """ 656 now = datetime.datetime.now() 657 658 next = list(rrule(SECONDLY, count=1, bysecond=0, byminute=self.minutes, 659 byhour=self.hours, bymonthday=self.doms, bymonth=self.months, 660 byweekday=self.dows, dtstart=now)[0].timetuple()) 661 662 return time.mktime(next) 663 664 def reschedule(self, scheduler): 665 """Reschedule this task according to its cron-like string. 666 """ 667 if scheduler.running: 668 abstime = self.get_schedule_time() 669 scheduler.schedule_task_abs(self, abstime) 670 671 500 672 try: 501 673 import threading 502 674 … … 566 738 """Monthday Task that executes in its own thread.""" 567 739 pass 568 740 741 class ThreadedCronLikeTask(ThreadedTaskMixin, CronLikeTask): 742 """Cron-like Task that executes in its own thread.""" 743 pass 744 569 745 except ImportError: 570 746 # threading is not available 571 747 pass … … 638 814 """Monthday Task that executes in its own process.""" 639 815 pass 640 816 817 class ForkedCronLikeTask(ForkedTaskMixin, CronLikeTask): 818 """Cron-like Task that executes in its own process.""" 819 pass 641 820 642 821 if __name__ == "__main__": 643 822 log.setLevel(logging.DEBUG) -
tgscheduler/scheduler.py
147 147 weekdays=None, monthdays=monthdays, timeonday=timeonday, 148 148 processmethod=processmethod, args=args, kw=kw) 149 149 150 def add_cron_like_task(self, action, cron_str, args=None, kw=None, 151 processmethod=method.threaded, taskname=None): 152 """ 153 Runs a task based on a cron-like syntax. 154 155 @param cron_str: The scheduling information, written in a cron-like syntax 156 157 @param action: The callable that will be called at the time you request 158 @param args: Tuple of positional parameters to pass to the action 159 @param kw: Keyword arguments to pass to the action 160 @param taskname: Tasks can have a name (stored in task.name), which can 161 help if you're trying to keep track of many tasks. 162 @param processmethod: By default, each task will be run in a new thread. 163 You can also pass in turbogears.scheduler.method.sequential or 164 turbogears.scheduler.method.forked. 165 """ 166 si = self._get_scheduler() 167 return si.add_cron_like_task(action=action, taskname=taskname, 168 cron_str=cron_str, processmethod=processmethod, 169 args=args, kw=kw) 170 150 171 def get_task(self, taskname): 151 172 """ 152 173 Retrieve a task by task name … … 181 202 add_weekday_task = scheduler.add_weekday_task 182 203 add_single_task = scheduler.add_single_task 183 204 add_interval_task = scheduler.add_interval_task 205 add_cron_like_task = scheduler.add_cron_like_task 184 206 get_task = scheduler.get_task 185 207 get_tasks = scheduler.get_tasks 186 208 cancel = scheduler.cancel -
tgscheduler/tests/test_scheduler.py
1 1 from tgscheduler import start_scheduler, stop_scheduler, add_single_task,\ 2 add_interval_task, add_monthly_task, add_weekday_task 2 add_interval_task, add_monthly_task, add_weekday_task, \ 3 add_cron_like_task 3 4 4 5 def functest(*args, **kws): 5 6 pass … … 25 26 assert task.action == functest 26 27 assert task.name == "intervaltest" 27 28 29 task = add_cron_like_task(functest, "*/30 8-12,14-18 * * MON-FRI", taskname="crontest1") 30 assert task.action == functest 31 assert task.name == "crontest1" 32 assert task.minutes == [0, 30], \ 33 "Invalid minutes: %s" % t.minutes 34 assert task.hours == [8, 9, 10, 11, 12, 14, 15, 16, 17, 18], \ 35 "Invalid hours: %s" % t.hours 36 assert task.doms == range(1, 32), \ 37 "Invalid doms: %s" % t.doms 38 assert task.months == range(1, 13), \ 39 "Invalid months: %s" % t.months 40 assert task.dows == range(0, 5), \ 41 "Invalid dows: %s" % t.dows 42 43 task = add_cron_like_task(functest, "5-10,25-30,57 8-12,14-18 2,25-28 Jan-mar,10-12 SuN-wEd", taskname="crontest2") 44 assert task.action == functest 45 assert task.name == "crontest2" 46 assert task.minutes == [5, 6, 7, 8, 9, 10, 25, 26, 27, 28, 29, 30, 57], \ 47 "Invalid minutes: %s" % t.minutes 48 assert task.hours == [8, 9, 10, 11, 12, 14, 15, 16, 17, 18], \ 49 "Invalid hours: %s" % t.hours 50 assert task.doms == [2, 25, 26, 27, 28], \ 51 "Invalid doms: %s" % t.doms 52 assert task.months == [1, 2, 3, 10, 11, 12], \ 53 "Invalid months: %s" % t.months 54 assert task.dows == [0, 1, 2, 6], \ 55 "Invalid dows: %s" % t.dows 56 57 try: 58 task = add_cron_like_task(functest, "* * * Jan-12 *", taskname="crontest3") 59 assert False, "This shouldn't have worked" 60 except: 61 assert True 62 28 63 stop_scheduler() 29 64 30 65