Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Ticket #2474: crontab.patch

File crontab.patch, 12.8 KB (added by bochecha, 2 years ago)
  • setup.py

     
    2020    license=license, 
    2121    platforms=["any"], 
    2222    zip_safe=False, 
     23    install_requires=[ 
     24        "python-dateutil" 
     25    ], 
    2326    packages=packages, 
    2427    #package_data=package_data, 
    2528    keywords=[ 
  • tgscheduler/__init__.py

     
    1111 
    1212from scheduler import start_scheduler, stop_scheduler, \ 
    1313                add_interval_task, add_monthly_task, add_single_task, \ 
    14                 add_weekday_task, cancel 
     14                add_weekday_task, add_cron_like_task, cancel 
  • tgscheduler/kronos.py

     
    5353    "ForkedSingleTask", 
    5454    "ForkedTaskMixin", 
    5555    "ForkedWeekdayTask", 
     56    "ForkedCronLikeTask", 
    5657    "IntervalTask", 
    5758    "MonthdayTask", 
     59    "CronLikeTask", 
    5860    "Scheduler", 
    5961    "SingleTask", 
    6062    "Task", 
     
    6466    "ThreadedSingleTask", 
    6567    "ThreadedTaskMixin", 
    6668    "ThreadedWeekdayTask", 
     69    "ThreadedCronLikeTask", 
    6770    "WeekdayTask", 
    6871    "add_interval_task", 
    6972    "add_monthday_task", 
    7073    "add_single_task", 
    7174    "add_weekday_task", 
     75    "add_cron_like_task", 
    7276    "cancel", 
    7377    "method", 
    7478] 
     
    8387import logging 
    8488log = logging.getLogger(__name__) 
    8589 
     90import datetime 
     91from dateutil.rrule import rrule, SECONDLY 
     92 
     93# bounds for each field of the cron-like syntax 
     94MINUTE_BOUNDS = (0, 59) 
     95HOUR_BOUNDS = (0, 23) 
     96DOM_BOUNDS = (1, 31) 
     97MONTH_BOUNDS = (1, 12) 
     98DOW_BOUNDS = (0, 7) 
     99 
     100# some fields of the cron-like syntax can be specified as names 
     101MONTH_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} 
     103DOW_MAPPING = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 
     104        'sat': 6} 
     105 
     106 
    86107class method: 
    87108    sequential = "sequential" 
    88109    forked = "forked" 
     
    259280        self.schedule_task_abs(task, firsttime) 
    260281        return task 
    261282 
     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 
    262313    def schedule_task(self, task, delay): 
    263314        """ 
    264315        Add a new task to the scheduler with the given delay (seconds). 
     
    497548            self.action(*self.args, **self.kw) 
    498549 
    499550 
     551class 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 
    500672try: 
    501673    import threading 
    502674 
     
    566738        """Monthday Task that executes in its own thread.""" 
    567739        pass 
    568740 
     741    class ThreadedCronLikeTask(ThreadedTaskMixin, CronLikeTask): 
     742        """Cron-like Task that executes in its own thread.""" 
     743        pass 
     744 
    569745except ImportError: 
    570746    # threading is not available 
    571747    pass 
     
    638814        """Monthday Task that executes in its own process.""" 
    639815        pass 
    640816 
     817    class ForkedCronLikeTask(ForkedTaskMixin, CronLikeTask): 
     818        """Cron-like Task that executes in its own process.""" 
     819        pass 
    641820 
    642821if __name__ == "__main__": 
    643822    log.setLevel(logging.DEBUG) 
  • tgscheduler/scheduler.py

     
    147147            weekdays=None, monthdays=monthdays, timeonday=timeonday, 
    148148            processmethod=processmethod, args=args, kw=kw) 
    149149 
     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 
    150171    def get_task(self, taskname): 
    151172        """ 
    152173        Retrieve a task by task name 
     
    181202add_weekday_task = scheduler.add_weekday_task 
    182203add_single_task = scheduler.add_single_task 
    183204add_interval_task = scheduler.add_interval_task 
     205add_cron_like_task = scheduler.add_cron_like_task 
    184206get_task = scheduler.get_task 
    185207get_tasks = scheduler.get_tasks 
    186208cancel = scheduler.cancel 
  • tgscheduler/tests/test_scheduler.py

     
    11from 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 
    34 
    45def functest(*args, **kws): 
    56    pass 
     
    2526    assert task.action == functest 
    2627    assert task.name == "intervaltest" 
    2728     
     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 
    2863    stop_scheduler() 
    2964 
    3065