Index: setup.py
===================================================================
--- setup.py	(revision 7033)
+++ setup.py	(working copy)
@@ -20,6 +20,9 @@
     license=license,
     platforms=["any"],
     zip_safe=False,
+    install_requires=[
+        "python-dateutil"
+    ],
     packages=packages,
     #package_data=package_data,
     keywords=[
Index: tgscheduler/__init__.py
===================================================================
--- tgscheduler/__init__.py	(revision 7033)
+++ tgscheduler/__init__.py	(working copy)
@@ -11,4 +11,4 @@
 
 from scheduler import start_scheduler, stop_scheduler, \
                 add_interval_task, add_monthly_task, add_single_task, \
-                add_weekday_task, cancel
+                add_weekday_task, add_cron_like_task, cancel
Index: tgscheduler/kronos.py
===================================================================
--- tgscheduler/kronos.py	(revision 7033)
+++ tgscheduler/kronos.py	(working copy)
@@ -53,8 +53,10 @@
     "ForkedSingleTask",
     "ForkedTaskMixin",
     "ForkedWeekdayTask",
+    "ForkedCronLikeTask",
     "IntervalTask",
     "MonthdayTask",
+    "CronLikeTask",
     "Scheduler",
     "SingleTask",
     "Task",
@@ -64,11 +66,13 @@
     "ThreadedSingleTask",
     "ThreadedTaskMixin",
     "ThreadedWeekdayTask",
+    "ThreadedCronLikeTask",
     "WeekdayTask",
     "add_interval_task",
     "add_monthday_task",
     "add_single_task",
     "add_weekday_task",
+    "add_cron_like_task",
     "cancel",
     "method",
 ]
@@ -83,6 +87,23 @@
 import logging
 log = logging.getLogger(__name__)
 
+import datetime
+from dateutil.rrule import rrule, SECONDLY
+
+# bounds for each field of the cron-like syntax
+MINUTE_BOUNDS = (0, 59)
+HOUR_BOUNDS = (0, 23)
+DOM_BOUNDS = (1, 31)
+MONTH_BOUNDS = (1, 12)
+DOW_BOUNDS = (0, 7)
+
+# some fields of the cron-like syntax can be specified as names
+MONTH_MAPPING = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
+        'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
+DOW_MAPPING = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5,
+        'sat': 6}
+
+
 class method:
     sequential = "sequential"
     forked = "forked"
@@ -259,6 +280,36 @@
         self.schedule_task_abs(task, firsttime)
         return task
 
+    def add_cron_like_task(self, action, taskname, cron_str,
+            processmethod, args, kw):
+        """ Add a new Cron-like Task to the schedule. """
+        if not args:
+            args = []
+        if not kw:
+            kw = {}
+        if processmethod == method.sequential:
+            TaskClass = CronLikeTask
+        elif processmethod == method.threaded:
+            TaskClass = ThreadedCronLikeTask
+        elif processmethod == method.forked:
+            TaskClass = ForkedCronLikeTask
+        else:
+            raise ValueError("Invalid processmethod")
+        if self.running:
+            self._acquire_lock()
+            try:
+                if self.tasks.has_key(taskname):
+                    raise ValueError("A task with the name %s already exists" % taskname)
+            finally:
+                self._release_lock()
+        else:
+            if self.tasks.has_key(taskname):
+                raise ValueError("A task with the name %s already exists" % taskname)
+        task = TaskClass(taskname, action, cron_str, args, kw)
+        firsttime = task.get_schedule_time()
+        self.schedule_task_abs(task, firsttime)
+        return task
+
     def schedule_task(self, task, delay):
         """
         Add a new task to the scheduler with the given delay (seconds).
@@ -497,6 +548,127 @@
             self.action(*self.args, **self.kw)
 
 
+class CronLikeTask(Task):
+    """A class that is scheduled with a cron-like syntax.
+    """
+    def __init__(self, name, action, cron_str, args=None, kw=None):
+        Task.__init__(self, name, action, args, kw)
+
+        self.cron_str = cron_str
+        try:
+            min_str, hour_str, dom_str, month_str, dow_str = cron_str.split()
+        except:
+            raise ValueError("Invalid value: %s" % cron_str)
+
+        self.minutes = self.__process_str(min_str, MINUTE_BOUNDS)
+        self.hours = self.__process_str(hour_str, HOUR_BOUNDS)
+        self.doms = self.__process_str(dom_str, DOM_BOUNDS)
+        self.months = self.__process_str(month_str, MONTH_BOUNDS, mapping=MONTH_MAPPING)
+
+        # dows are somewhat special:
+        #   * Cron accepts both 0 and 7 for Sunday
+        #     => we deal with that using the "% 7" operator and a temporary set
+        #   * Python starts with Monday = 0 while Cron starts with Sunday = 0
+        #     => we deal with that by substracting 1
+        #   * (SUN - 1) % 7 = (0 - 1) % 7 = 6
+        #     => we need to sort the list
+        self.dows = list(set([ (dow - 1) % 7 \
+                for dow in self.__process_str(dow_str,
+                        DOW_BOUNDS, mapping=DOW_MAPPING) ]))
+        self.dows.sort()
+
+    def __process_str(self, time_str, bounds, mapping=None):
+        """Transforms a field of the cron-like string into a list
+
+        Note: specifying a range as a mix of integers and names
+              (months and dows) is NOT supported
+
+        @param time_str: a field in the cron-like string
+        @type time_str: string
+
+        @param bounds: the acceptable limits for this field
+        @type bounds: 2-tuple of integers
+
+        @param mapping: the mapping between names and integer values
+        @type mapping: dict
+        """
+        freq = 1
+        if '/' in time_str:
+            try:
+                time_str, freq = time_str.split('/')
+                freq = int(freq)
+            except:
+                raise ValueError("Invalid value: '%s'" % time_str)
+
+        if time_str == '*':
+            result = range(bounds[0], bounds[1] + 1)
+            return result[::freq]
+
+        result = list()
+
+        for item in time_str.split(','):
+            if not '-' in item:
+                # ex: time_str = "1,4,23"
+                try:
+                    time = int(item)
+                except:
+                    if mapping and mapping.has_key(item.lower()):
+                        time = mapping[item.lower()]
+                    else:
+                        raise ValueError("Invalid value: '%s'" % time_str)
+
+                if time < bounds[0] or time > bounds [1]:
+                    raise ValueError("Invalid value: '%s'" % time_str)
+
+                result.append(time)
+            else:
+                # ex: time_str = "1-4,7-9"
+                try:
+                    interval_low, interval_high = item.split('-')
+                except:
+                    # an interval can only have one dash
+                    raise ValueError("Invalid value: '%s'" % time_str)
+
+                try:
+                    # intervals are specified as integers
+                    time_low = int(interval_low)
+                    time_high = int(interval_high)
+                except:
+                    if mapping and mapping.has_key(interval_low.lower()) \
+                            and mapping.has_key(interval_high.lower()):
+                        # in some cases names can be used (months or dows)
+                        time_low = mapping[interval_low.lower()]
+                        time_high = mapping[interval_high.lower()]
+                    else:
+                        raise ValueError("Invalid value: '%s'" % time_str)
+
+                if time_low < bounds[0] or time_high > bounds [1]:
+                    raise ValueError("Invalid value: '%s'" % time_str)
+
+                result.extend(range(time_low, time_high + 1))
+
+        # filter results by frequency
+        return result[::freq]
+
+    def get_schedule_time(self):
+        """Determine the next execution time of the task.
+        """
+        now = datetime.datetime.now()
+
+        next = list(rrule(SECONDLY, count=1, bysecond=0, byminute=self.minutes,
+                byhour=self.hours, bymonthday=self.doms, bymonth=self.months,
+                byweekday=self.dows, dtstart=now)[0].timetuple())
+
+        return time.mktime(next)
+
+    def reschedule(self, scheduler):
+        """Reschedule this task according to its cron-like string.
+        """
+        if scheduler.running:
+            abstime = self.get_schedule_time()
+            scheduler.schedule_task_abs(self, abstime)
+
+
 try:
     import threading
 
@@ -566,6 +738,10 @@
         """Monthday Task that executes in its own thread."""
         pass
 
+    class ThreadedCronLikeTask(ThreadedTaskMixin, CronLikeTask):
+        """Cron-like Task that executes in its own thread."""
+        pass
+
 except ImportError:
     # threading is not available
     pass
@@ -638,6 +814,9 @@
         """Monthday Task that executes in its own process."""
         pass
 
+    class ForkedCronLikeTask(ForkedTaskMixin, CronLikeTask):
+        """Cron-like Task that executes in its own process."""
+        pass
 
 if __name__ == "__main__":
     log.setLevel(logging.DEBUG)
Index: tgscheduler/scheduler.py
===================================================================
--- tgscheduler/scheduler.py	(revision 7033)
+++ tgscheduler/scheduler.py	(working copy)
@@ -147,6 +147,27 @@
             weekdays=None, monthdays=monthdays, timeonday=timeonday,
             processmethod=processmethod, args=args, kw=kw)
 
+    def add_cron_like_task(self, action, cron_str, args=None, kw=None,
+            processmethod=method.threaded, taskname=None):
+        """
+        Runs a task based on a cron-like syntax.
+
+        @param cron_str: The scheduling information, written in a cron-like syntax
+
+        @param action: The callable that will be called at the time you request
+        @param args: Tuple of positional parameters to pass to the action
+        @param kw: Keyword arguments to pass to the action
+        @param taskname:  Tasks can have a name (stored in task.name), which can
+        help if you're trying to keep track of many tasks.
+        @param processmethod: By default, each task will be run in a new thread.
+        You can also pass in turbogears.scheduler.method.sequential or
+        turbogears.scheduler.method.forked.
+        """
+        si = self._get_scheduler()
+        return si.add_cron_like_task(action=action, taskname=taskname,
+                cron_str=cron_str, processmethod=processmethod,
+                args=args, kw=kw)
+
     def get_task(self, taskname):
         """
         Retrieve a task by task name
@@ -181,6 +202,7 @@
 add_weekday_task = scheduler.add_weekday_task
 add_single_task = scheduler.add_single_task
 add_interval_task = scheduler.add_interval_task
+add_cron_like_task = scheduler.add_cron_like_task
 get_task = scheduler.get_task
 get_tasks = scheduler.get_tasks
 cancel = scheduler.cancel
Index: tgscheduler/tests/test_scheduler.py
===================================================================
--- tgscheduler/tests/test_scheduler.py	(revision 7033)
+++ tgscheduler/tests/test_scheduler.py	(working copy)
@@ -1,5 +1,6 @@
 from tgscheduler import start_scheduler, stop_scheduler, add_single_task,\
-                                    add_interval_task, add_monthly_task, add_weekday_task
+                                    add_interval_task, add_monthly_task, add_weekday_task, \
+                                    add_cron_like_task
 
 def functest(*args, **kws):
     pass
@@ -25,6 +26,40 @@
     assert task.action == functest
     assert task.name == "intervaltest"
     
+    task = add_cron_like_task(functest, "*/30 8-12,14-18 * * MON-FRI", taskname="crontest1")
+    assert task.action == functest
+    assert task.name == "crontest1"
+    assert task.minutes == [0, 30], \
+        "Invalid minutes: %s" % t.minutes
+    assert task.hours == [8, 9, 10, 11, 12, 14, 15, 16, 17, 18], \
+        "Invalid hours: %s" % t.hours
+    assert task.doms == range(1, 32), \
+        "Invalid doms: %s" % t.doms
+    assert task.months == range(1, 13), \
+        "Invalid months: %s" % t.months
+    assert task.dows == range(0, 5), \
+        "Invalid dows: %s" % t.dows
+
+    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")
+    assert task.action == functest
+    assert task.name == "crontest2"
+    assert task.minutes == [5, 6, 7, 8, 9, 10, 25, 26, 27, 28, 29, 30, 57], \
+        "Invalid minutes: %s" % t.minutes
+    assert task.hours == [8, 9, 10, 11, 12, 14, 15, 16, 17, 18], \
+        "Invalid hours: %s" % t.hours
+    assert task.doms == [2, 25, 26, 27, 28], \
+        "Invalid doms: %s" % t.doms
+    assert task.months == [1, 2, 3, 10, 11, 12], \
+        "Invalid months: %s" % t.months
+    assert task.dows == [0, 1, 2, 6], \
+        "Invalid dows: %s" % t.dows
+
+    try:
+        task = add_cron_like_task(functest, "* * * Jan-12 *", taskname="crontest3")
+        assert False, "This shouldn't have worked"
+    except:
+        assert True
+
     stop_scheduler()
 
 

