Commits

Tetsuya Morimoto committed 4f1672d

moved "src/traccron" to "traccron" so that trac-admin panel can find module files in EGG-INFO/SOURCES.txt

  • Participants
  • Parent commits 32d55aa

Comments (0)

Files changed (35)

     distribute_setup.use_setuptools()
 
 import pkg_resources
-from setuptools import find_packages, setup
+from setuptools import setup
 
 REQUIRES = [
     'Trac >= 0.10',
     version='0.3',
     description='Task scheduler plugin for Trac',
     long_description=LONG_DESCRIPTION,
-    package_dir={'': 'src'},
-    packages=find_packages('src'),
+    packages=['traccron'],
     package_data={'traccron': ['templates/*.*']},
     author='Thierry Bressure',
     author_email='thierry@bressure.net',

src/traccron/__init__.py

-__import__('pkg_resources').declare_namespace(__name__)

src/traccron/api.py

-# -*- encoding: UTF-8 -*-
-'''
-Created on 28 oct. 2010
-
-@author: thierry
-'''
-
-###############################################################################
-##
-##                     I N T E R F A C E    A P I
-##
-###############################################################################
-
-from trac.core import Interface
-
-class ICronTask(Interface):
-    """
-    Interface for component task
-    """
-
-    def wake_up(self, *args):
-        """
-        Call by the scheduler when the task need to be executed
-        """
-        raise NotImplementedError
-
-    def getId(self):
-        """
-        Return the key to use in trac.ini to configure this task
-        """
-        raise NotImplementedError
-
-    def getDescription(self):
-        """
-        Return the description of this task to be used in the admin panel.
-        """
-        raise NotImplementedError
-
-
-class ISchedulerType(Interface):
-    """
-    Interface for scheduler type. A Scheduler type is a sort of scheduler that
-    can trigger a task based on a specific scheduling.
-    """
-
-    def getId(self):
-        """
-        Return the id to use in trac.ini for this schedule type
-        """
-        raise NotImplementedError
-
-    def getHint(self):
-        """
-        Return a description of what it is and the format used to defined
-        the schedule
-        """
-        raise NotImplementedError
-
-    def isTriggerTime(self, task, currentTime):
-        """
-        Test is accordingly to this scheduler and given currentTime,
-        is time to fire the task
-        """
-        raise NotImplementedError
-
-
-class ITaskEventListener(Interface):
-    """
-    Interface that listen to event occuring on task
-    """
-
-    def onStartTask(self, task):
-        """
-        called by the core system when the task is triggered,
-        just before the waek_up method is called
-        """
-        raise NotImplementedError
-
-    def onEndTask(self, task, success):
-        """
-        called by the core system when the task execution is finished,
-        just after the task wake_up method exit
-        """
-        raise NotImplementedError
-
-    def getId(self):
-        """
-        return the id of the listener. It is used in trac.ini
-        """
-        raise NotImplementedError
-
-    def getDescription(self):
-        """
-        Return a description used in admin panel
-        """
-        raise NotImplementedError
-
-class IHistoryTaskExecutionStore(Interface):
-    """
-    Interface that store an history of task execution.
-    """
-
-    def addExecution(self, task, start, end, success):
-        """
-        Add a new execution of a task into this history.
-        Task is the task object.
-        start is the start time in second from EPOC
-        end is the end time in seconf from EPOC
-        """
-        raise NotImplementedError
-
-    def getExecution(self, task=None, fromTime=None, toTime=None, sucess=None):
-        """
-        Return a iterator on all execution stored. Each element is a tuple
-        of (task, start time, end time, success status) where
-        task is the task object
-        start time and end time are second from EPOC
-        success status is a boolean value
-
-        Optional paramater can be used to filter the result.
-        """
-        raise NotImplementedError
-
-    def clear(self):
-        """
-        Clear all event in this history store
-        """
-
-        raise NotImplementedError

src/traccron/core.py

-# -*- encoding: UTF-8 -*-
-"""
-Created on 28 oct. 2010
-
-@author: thierry
-"""
-
-###############################################################################
-##
-##        C O R E    C L A S S E S     O F     T H E    P L U G I N
-##
-###############################################################################
-
-from datetime import datetime, timedelta
-from time import time, localtime
-from threading import Timer
-
-from trac.core import Component, ExtensionPoint, implements
-from trac.admin import IAdminPanelProvider
-from trac.web.chrome import ITemplateProvider
-from trac.web.chrome import IRequestHandler
-from trac.web.chrome import add_notice, add_warning, add_link
-from trac.util.translation import _
-from trac.util.text import exception_to_unicode
-from trac.util.datefmt import utc, http_date
-
-from traccron.api import ICronTask
-from traccron.api import IHistoryTaskExecutionStore
-from traccron.api import ISchedulerType
-from traccron.api import ITaskEventListener
-
-
-class Core(Component):
-    """
-    Main class of the Trac Cron Plugin. This is the entry point
-    for Trac plugin architecture
-    """
-    implements(IAdminPanelProvider, ITemplateProvider, IRequestHandler)
-
-    cron_task_list = ExtensionPoint(ICronTask)
-
-    supported_schedule_type = ExtensionPoint(ISchedulerType)
-
-    task_event_list = ExtensionPoint(ITaskEventListener)
-
-    history_store_list = ExtensionPoint(IHistoryTaskExecutionStore)
-
-    current_ticker = None
-
-    def __init__(self, *args, **kwargs):
-        """
-        Intercept the instanciation to start the ticker
-        """
-        Component.__init__(self, *args, **kwargs)
-        self.cronconf = CronConfig(self.env)
-        self.webUi = WebUi(self)
-        self.apply_config()
-
-    def apply_config(self, wait=False):
-        """
-        Read configuration and apply it
-        """
-        # stop existing ticker if any
-        self.env.log.debug('applying config')
-        if Core.current_ticker is not None:
-            self.env.log.debug('stop existing ticker')
-            Core.current_ticker.cancel(wait=wait)
-
-        if self.getCronConf().get_ticker_enabled():
-            self.env.log.debug('ticker is enabled')
-            # try to execute task a first time
-            # because we don't want to wait for interval to elapse
-            self.check_task()
-            interval = self.getCronConf().get_ticker_interval()
-            Core.current_ticker = Ticker(self.env, interval, self.check_task)
-        else:
-            self.env.log.debug('ticker is disabled')
-
-    def getCronConf(self):
-        """
-        Return the configuration for TracCronPlugin
-        """
-        return self.cronconf
-
-    def getTaskList(self):
-        """
-        Return the list of existing task
-        """
-        return self.cron_task_list
-
-    def getSupportedScheduleType(self):
-        """
-        Return the list of supported schedule type
-        """
-        return self.supported_schedule_type
-
-    def getHistoryList(self):
-        """
-        Return the list of history store
-        """
-        return self.history_store_list
-
-    def getTaskListnerList(self):
-        """
-        Return the list of task event listener
-        """
-        return self.task_event_list
-
-    def clearHistory(self):
-        """
-        Clear history store
-        """
-        for history in self.history_store_list:
-            history.clear()
-
-    def check_task(self):
-        """
-        Check if any task need to be executed.
-        This method is called by the Ticker.
-        """
-        # store current time
-        currentTime = localtime(time())
-        self.env.log.debug("check existing task")
-        for task in self.cron_task_list:
-            task_id = task.getId()
-            if self.cronconf.is_task_enabled(task):
-                # test current time with task planing
-                self.env.log.debug("check task: %s" % task_id)
-                for schedule in self.supported_schedule_type:
-                    sch_id = schedule.getId()
-                    if self.cronconf.is_schedule_enabled(task, schedule):
-                        if schedule.isTriggerTime(task, currentTime):
-                            self._runTask(task, schedule)
-                        else:
-                            self.env.log.debug("nothing to do: %s" % task_id)
-                    else:
-                        self.env.log.debug("schedule is disabled: %s" % sch_id)
-            else:
-                self.env.log.debug("task is disabled: %s" % task_id)
-
-    def runTask(self, task, parameters=None):
-        """
-        run a given task with specified argument string
-        parameters maybe comma-separated values
-        """
-        self._runTask(task, parameters=parameters)
-
-    # IAdminPanel interface
-
-    def get_admin_panels(self, req):
-        return self.webUi.get_admin_panels(req)
-
-    def render_admin_panel(self, req, category, page, path_info):
-        return self.webUi.render_admin_panel(req, category, page, path_info)
-
-    # ITemplateProvider interface
-
-    def get_htdocs_dirs(self):
-        return self.webUi.get_htdocs_dirs()
-
-    def get_templates_dirs(self):
-        return self.webUi.get_templates_dirs()
-
-    # IRequestHandler interface
-
-    def match_request(self, req):
-        return self.webUi.match_request(req)
-
-    def process_request(self, req):
-        return self.webUi.process_request(req)
-
-    # internal method
-
-    def _notify_start_task(self, task):
-        for listener in self.task_event_list:
-            # notify only if listener is enabled
-            if self.cronconf.is_task_listener_enabled(listener):
-                try:
-                    listener.onStartTask(task)
-                except Exception, e:
-                    msg = 'listener %s failed  onStartTask event : %s' % (
-                          str(listener), exception_to_unicode(e))
-                    self.env.log.warn(msg)
-
-    def _notify_end_task(self, task, success=True):
-        for listener in self.task_event_list:
-            if self.cronconf.is_task_listener_enabled(listener):
-                try:
-                    listener.onEndTask(task, success)
-                except Exception, e:
-                    msg = 'listener %s failed  onEndTask event : %s' % (
-                          str(listener), exception_to_unicode(e))
-                    self.env.log.warn(msg)
-
-    def _runTask(self, task, schedule=None, parameters=None):
-        task_id = task.getId()
-        self.env.log.info('executing task: %s' % task_id)
-        self._notify_start_task(task)
-        try:
-            args = []
-            if schedule:
-                args = self.cronconf.get_schedule_arg_list(task, schedule)
-            elif parameters:
-                args = parameters
-            task.wake_up(*args)
-        except Exception, e:
-            self.env.log.error('task execution result is FAILURE, %s',
-                               exception_to_unicode(e))
-            self._notify_end_task(task, success=False)
-        else:
-            self.env.log.info('task execution result is SUCCESS')
-            self._notify_end_task(task)
-            self.env.log.info('task is finished: %s' % task_id)
-
-
-class Ticker():
-    """
-    A Ticker is simply a simply timer that will repeatly wake up.
-    """
-    def __init__(self, env, interval, callback):
-        """
-        Create a new Ticker.
-        env : the trac environnement
-        interval: interval in minute
-        callback: the function callback to call o every wake-up
-        """
-        self.env = env
-        self.interval = interval
-        self.callback = callback
-        self.timer = None
-        self.create_new_timer()
-
-    def create_new_timer(self, wait=False, delay=None):
-        """
-        Create a new timer before killing existing one if required.
-        wait : if True the current thread wait until running task finished.
-        Default is False
-        """
-        self.env.log.debug('create new ticker')
-        if self.timer is not None:
-            self.timer.cancel()
-            if (wait):
-                self.timer.join()
-
-        if delay:
-            # use specified delay to wait
-            _delay = delay
-        else:
-            # use default delay
-            _delay = self.interval * 60
-        self.timer = Timer(_delay, self.wake_up)
-        self.timer.start()
-        self.env.log.debug('new ticker started')
-
-    def wake_up(self):
-        """
-        Wake up this ticker. This ticker will call the callback function then
-        create a new timer to wake it up again
-        """
-        self.env.log.debug('ticker wake up')
-        in_hurry = True
-        while (in_hurry):
-            wake_up_time = datetime(*datetime.now().timetuple()[:6])
-
-            self.callback()
-
-            now = datetime(*datetime.now().timetuple()[:6])
-            next_wake_up_time = wake_up_time + timedelta(minutes=self.interval)
-            self.env.log.debug('last wake up time %s' % wake_up_time)
-            self.env.log.debug('next wake up time %s' % next_wake_up_time)
-            self.env.log.debug('current time %s' % now)
-
-            if now < next_wake_up_time:
-                # calculate amount of second to wait in second
-                seconds_before_next_wake_up = (next_wake_up_time - now).seconds
-                # need to adjust it to ensure to skip this last wake up
-                # minute window
-                if next_wake_up_time.second == 0:
-                    # slide up 1 second
-                    seconds_before_next_wake_up += 1
-                # need to adjust it to ensure to not skip next minute window
-                elif next_wake_up_time.second == 59:
-                    # slid down 1 second
-                    seconds_before_next_wake_up -= 1
-
-                msg = 'adjusted wait %s secondes'
-                self.env.log.debug(msg % seconds_before_next_wake_up)
-                self.create_new_timer(delay=seconds_before_next_wake_up)
-                in_hurry = False
-            else:
-                # the next wake up is over,
-                seconds_before_next_wake_up = (now - next_wake_up_time).seconds
-                msg = 'task processing duration overtake ticker interval, '\
-                      'next wake up is over since %d seconds'
-                self.env.log.warn(msg % seconds_before_next_wake_up)
-
-    def cancel(self, wait=False):
-        self.timer.cancel()
-        if (wait):
-            self.timer.join()
-
-
-class CronConfig():
-    """
-    This class read and write configuration for TracCronPlugin
-    """
-    TRACCRON_SECTION = 'traccron'
-
-    TICKER_ENABLED_KEY = 'ticker_enabled'
-    TICKER_ENABLED_DEFAULT = 'False'
-
-    TICKER_INTERVAL_KEY = 'ticker_interval'
-    TICKER_INTERVAL_DEFAULT = 1  # minutes
-
-    TASK_ENABLED_KEY = 'enabled'
-    TASK_ENABLED_DEFAULT = 'True'
-
-    SCHEDULE_ENABLED_KEY = 'enabled'
-    SCHEDULE_ENABLED_DEFAULT = 'True'
-
-    SCHEDULE_ARGUMENT_KEY = 'arg'
-    SCHEDULE_ARGUMENT_DEFAULT = ''
-
-    TASK_LISTENER_ENABLED_KEY = 'enabled'
-    TASK_LISTENER_ENABLED_DEFAULT = 'True'
-
-    EMAIL_NOTIFIER_TASK_BASEKEY = 'email_task_event'
-
-    EMAIL_NOTIFIER_TASK_LIMIT_KEY = 'limit'
-    EMAIL_NOTIFIER_TASK_LIMIT_DEFAULT = 1
-
-    EMAIL_NOTIFIER_TASK_RECIPIENT_KEY = 'recipient'
-    EMAIL_NOTIFIER_TASK_RECIPIENT_DEFAULT = ''
-
-    EMAIL_NOTIFIER_TASK_ONLY_ERROR_KEY = 'only_error'
-    EMAIL_NOTIFIER_TASK_ONLY_ERROR_DEFAULT = 'False'
-
-    UNREACHABLE_MILESTONE_TASK_BASEKEY = 'unreachable_milestone'
-    UNREACHABLE_MILESTONE_TASK_RECIPIENT_KEY = 'recipient'
-    UNREACHABLE_MILESTONE_TASK_RECIPIENT_DEFAULT = ''
-
-    def __init__(self, env):
-        self.env = env
-
-    def _make_task_and_schedule_key(self, task, schedule_type):
-        return '%s.%s' % (task.getId(), schedule_type.getId())
-
-    def _make_task_enabled_key(self, task):
-        return '%s.%s' % (task.getId(), self.TASK_ENABLED_KEY)
-
-    def _make_schedule_enabled_key(self, task, schedule_type):
-        return '%s.%s.%s' % (task.getId(), schedule_type.getId(),
-                             self.SCHEDULE_ENABLED_KEY)
-
-    def _make_schedule_argument_key(self, task, schedule_type):
-        return '%s.%s.%s' % (task.getId(), schedule_type.getId(),
-                             self.SCHEDULE_ARGUMENT_KEY)
-
-    def _make_listener_enabled_key(self, listener):
-        return '%s.%s' % (listener.getId(), self.TASK_LISTENER_ENABLED_KEY)
-
-    @property
-    def email_notifier_task_limit_key(self):
-        return '%s.%s' % (self.EMAIL_NOTIFIER_TASK_BASEKEY,
-                          self.EMAIL_NOTIFIER_TASK_LIMIT_KEY)
-
-    @property
-    def email_notifier_task_recipient_key(self):
-        return '%s.%s' % (self.EMAIL_NOTIFIER_TASK_BASEKEY,
-                          self.EMAIL_NOTIFIER_TASK_RECIPIENT_KEY)
-
-    @property
-    def email_notifier_task_only_error_key(self):
-        return '%s.%s' % (self.EMAIL_NOTIFIER_TASK_BASEKEY,
-                          self.EMAIL_NOTIFIER_TASK_ONLY_ERROR_KEY)
-
-    @property
-    def unreachable_milestone_task_recipient_key(self):
-        return '%s.%s' % (self.UNREACHABLE_MILESTONE_TASK_BASEKEY,
-                          self.UNREACHABLE_MILESTONE_TASK_RECIPIENT_KEY)
-
-    def get_ticker_enabled(self):
-        return self.env.config.getbool(self.TRACCRON_SECTION,
-                                       self.TICKER_ENABLED_KEY,
-                                       self.TICKER_ENABLED_DEFAULT)
-
-    def set_ticker_enabled(self, value):
-        self.env.config.set(self.TRACCRON_SECTION,
-                            self.TICKER_ENABLED_KEY,
-                            value)
-
-    def get_ticker_interval(self):
-        return self.env.config.getint(self.TRACCRON_SECTION,
-                                      self.TICKER_INTERVAL_KEY,
-                                      self.TICKER_INTERVAL_DEFAULT)
-
-    def set_ticker_interval(self, value):
-        self.env.config.set(self.TRACCRON_SECTION,
-                            self.TICKER_INTERVAL_KEY,
-                            value)
-
-    def get_schedule_value(self, task, schedule_type):
-        """
-        Return the raw value of the schedule
-        """
-        key = self._make_task_and_schedule_key(task, schedule_type)
-        return self.env.config.get(self.TRACCRON_SECTION, key, None)
-
-    def get_schedule_value_list(self, task, schedule_type):
-        """
-        Return the list of value for the schedule type and task
-        """
-        key = self._make_task_and_schedule_key(task, schedule_type)
-        return self.env.config.getlist(self.TRACCRON_SECTION, key)
-
-    def set_schedule_value(self, task, schedule_type, value):
-        key = self._make_task_and_schedule_key(task, schedule_type)
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def is_task_enabled(self, task):
-        """
-        Return the value that indicate if the task is enabled
-        """
-        return self.env.config.getbool(self.TRACCRON_SECTION,
-                                       self._make_task_enabled_key(task),
-                                       self.TASK_ENABLED_DEFAULT)
-
-    def set_task_enabled(self, task, value):
-        key = self._make_task_enabled_key(task)
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def is_schedule_enabled(self, task, schedule):
-        """
-        Return the value that indicate if the schedule for a given task
-        is enabled
-        """
-        key = self._make_schedule_enabled_key(task, schedule)
-        return self.env.config.getbool(self.TRACCRON_SECTION,
-                                       key,
-                                       self.SCHEDULE_ENABLED_DEFAULT)
-
-    def set_schedule_enabled(self, task, schedule, value):
-        key = self._make_schedule_enabled_key(task, schedule)
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def get_schedule_arg(self, task, schedule):
-        """
-        Return the raw value of argument for a given schedule of a task
-        """
-        key = self._make_schedule_argument_key(task, schedule)
-        return self.env.config.get(self.TRACCRON_SECTION, key, None)
-
-    def get_schedule_arg_list(self, task, schedule):
-        """
-        Return the list of argument for a given schedule of a task
-        """
-        key = self._make_schedule_argument_key(task, schedule)
-        return self.env.config.getlist(self.TRACCRON_SECTION, key)
-
-    def set_schedule_arg(self, task, schedule, value):
-        key = self._make_schedule_argument_key(task, schedule)
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def get_email_notifier_task_limit(self):
-        """
-        Return the number of task event to notify.
-        """
-        return self.env.config.getint(self.TRACCRON_SECTION,
-                                      self.email_notifier_task_limit_key,
-                                      self.EMAIL_NOTIFIER_TASK_LIMIT_DEFAULT)
-
-    def get_email_notifier_task_recipient(self):
-        """
-        Return the recipients for task listener as raw value
-        """
-        return self.env.config.get(self.TRACCRON_SECTION,
-                                   self.email_notifier_task_recipient_key,
-                                   self.EMAIL_NOTIFIER_TASK_RECIPIENT_DEFAULT)
-
-    def get_email_notifier_task_recipient_list(self):
-        """
-        Return the recipients for task listener
-        """
-        key = self.email_notifier_task_recipient_key
-        return self.env.config.getlist(self.TRACCRON_SECTION, key)
-
-    def is_email_notifier_only_error(self):
-        """
-        Return the value that indicate of the notification must be sent only
-        for task on error
-        """
-        return self.env.config.getbool(self.TRACCRON_SECTION,
-                                self.email_notifier_task_only_error_key,
-                                self.EMAIL_NOTIFIER_TASK_ONLY_ERROR_DEFAULT)
-
-    def set_email_notifier_only_error(self, value):
-        key = self.email_notifier_task_only_error_key
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def is_task_listener_enabled(self, listener):
-        return self.env.config.getbool(self.TRACCRON_SECTION,
-                                self._make_listener_enabled_key(listener),
-                                self.TASK_LISTENER_ENABLED_DEFAULT)
-
-    def set_task_listener_enabled(self, listener, value):
-        key = self._make_listener_enabled_key(listener)
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def get_unreachable_milestone_task_recipient_list(self):
-        """
-        Return recipient list for unreachable milestone
-        """
-        key = self.unreachable_milestone_task_recipient_key
-        return self.env.config.getlist(self.TRACCRON_SECTION, key)
-
-    def get_unreachable_milestone_task_recipient(self):
-        """
-        Return raw value of unreachable milestone recipient
-        """
-        return self.env.config.get(self.TRACCRON_SECTION,
-                            self.unreachable_milestone_task_recipient_key,
-                            self.UNREACHABLE_MILESTONE_TASK_RECIPIENT_DEFAULT)
-
-    def set_value(self, key, value):
-        self.env.config.set(self.TRACCRON_SECTION, key, value)
-
-    def remove_value(self, key):
-        self.env.config.remove(self.TRACCRON_SECTION, key)
-
-    def save(self):
-        self.env.config.save()
-
-
-class WebUi(IAdminPanelProvider, ITemplateProvider, IRequestHandler):
-    """
-    Class that deal with Web stuff.
-    It is the both the controller and the page builder.
-    """
-    def __init__(self, core):
-        self.env = core.env
-        self.cron_task_list = core.getTaskList()
-        self.cronconf = core.getCronConf()
-        self.history_store_list = core.getHistoryList()
-        self.all_schedule_type = core.getSupportedScheduleType()
-        self.cron_listener_list = core.getTaskListnerList()
-        self.core = core
-
-    # IAdminPanelProvider
-
-    def get_admin_panels(self, req):
-        if ('TRAC_ADMIN' in req.perm):
-            yield ('traccron', 'Trac Cron', 'cron_admin', u'Settings')
-            yield ('traccron', 'Trac Cron', 'cron_history', u'History')
-            yield ('traccron', 'Trac Cron', 'cron_listener', u'Listener')
-
-    def render_admin_panel(self, req, category, page, path_info):
-        req.perm.assert_permission('TRAC_ADMIN')
-
-        if req.method == 'POST':
-            if 'save' in req.args:
-                if page == 'cron_admin':
-                    self._saveSettings(req, category, page)
-                elif page == 'cron_listener':
-                    self._saveListenerSettings(req, category, page)
-            elif 'clear' in req.args:
-                if page == 'cron_history':
-                    self._clearHistory(req, category, page)
-        else:
-            # which view to display ?
-            if page == 'cron_admin':
-                return self._displaySettingView()
-            elif page == 'cron_history':
-                return self._displayHistoryView(req)
-            elif page == 'cron_listener':
-                return self._displayListenerView()
-
-    # ITemplateProvider
-
-    def get_htdocs_dirs(self):
-        return []
-
-    def get_templates_dirs(self):
-        from pkg_resources import resource_filename
-        return [resource_filename(__name__, 'templates')]
-
-    # IRequestHandler interface
-
-    def match_request(self, req):
-        return req.path_info.startswith('/traccron/')
-
-    def process_request(self, req):
-        if req.path_info == '/traccron/runtask':
-            self._runtask(req)
-        elif req.path_info == '/traccron/cron_history':
-            return self._displayHistoryView(req)
-        else:
-            msg = 'Trac Cron Plugin was unable to handle %s' % req.path_info
-            self.env.log.warn(msg)
-            add_warning(req, 'The request was not handled by trac cron plugin')
-            req.redirect(req.href.admin('traccron', 'cron_admin'))
-
-    # internal method
-
-    def _create_history_list(self):
-        """
-        Create list of task execution history
-        """
-        _history = []
-
-        for store in self.history_store_list:
-            for task, start, end, success in store.getExecution():
-                startTime = localtime(start)
-                endTime = localtime(end)
-                date = datetime.fromtimestamp(start, utc)
-                execution = {
-                    'timestamp': start,
-                    'datetime': date,
-                    'dateuid': self._to_utimestamp(date),
-                    'date': '%d-%d-%d' % (startTime.tm_mon, startTime.tm_mday,
-                                          startTime.tm_year),
-                    'task': task.getId(),
-                    'start': '%02d h %02d' % (startTime.tm_hour,
-                                              startTime.tm_min),
-                    'end': '%02d h %02d' % (endTime.tm_hour, endTime.tm_min),
-                    'success': success
-                }
-                _history.append(execution)
-
-        #apply sorting
-        _history.sort(None, lambda(x): x["timestamp"])
-        return _history
-
-    _epoc = datetime(1970, 1, 1, tzinfo=utc)
-
-    def _to_utimestamp(self, dt):
-        """Return a microsecond POSIX timestamp for the given `datetime`."""
-        if not dt:
-            return 0
-        diff = dt - self._epoc
-        return (diff.days * 86400000000L + diff.seconds * 1000000
-                + diff.microseconds)
-
-    def _save_config(self, req, notices=None):
-        """Try to save the config, and display either a success notice or a
-        failure warning.
-        """
-        try:
-            self.cronconf.save()
-            if notices is None:
-                notices = [_('Your changes have been saved.')]
-            for notice in notices:
-                add_notice(req, notice)
-        except Exception, e:
-            msg = 'Error writing to trac.ini: %s', exception_to_unicode(e)
-            self.env.log.error(msg)
-            add_warning(req, _('Error writing to trac.ini, make sure it is '
-                               'writable by the web server. Your changes have '
-                               'not been saved.'))
-
-    def _displaySettingView(self, data={}):
-        conf = self.cronconf  # just an alias
-        data.update({
-            conf.TICKER_ENABLED_KEY: conf.get_ticker_enabled(),
-            conf.TICKER_INTERVAL_KEY: conf.get_ticker_interval()
-        })
-        task_list = []
-        for task in self.cron_task_list:
-            task_data = {}
-            task_data['enabled'] = conf.is_task_enabled(task)
-            task_data['id'] = task.getId()
-            task_data['description'] = task.getDescription()
-            all_schedule_value = {}
-            for schedule in self.all_schedule_type:
-                value = conf.get_schedule_value(task, schedule)
-                if value is None:
-                    value = ''
-                task_enabled = conf.is_schedule_enabled(task, schedule)
-                task_arg = conf.get_schedule_arg(task, schedule)
-                if task_arg is None:
-                    task_arg = ''
-                all_schedule_value[schedule.getId()] = {
-                    'value': value,
-                    'hint': schedule.getHint(),
-                    'enabled': task_enabled,
-                    'arg': task_arg
-                }
-
-            task_data['schedule_list'] = all_schedule_value
-            task_list.append(task_data)
-
-        data['task_list'] = task_list
-        return 'cron_admin.html', data
-
-    def _displayHistoryView(self, req, data={}):
-        # create history list
-        data['history_list'] = self._create_history_list()
-
-        format = req.args.get('format')
-        if (format == 'rss'):
-            return 'cron_history.rss', data, 'application/rss+xml'
-        else:
-            rss_href = req.href.traccron('cron_history', format='rss')
-            add_link(req, 'alternate', rss_href, _('RSS Feed'),
-                     'application/rss+xml', 'rss')
-            return 'cron_history.html', data
-
-    def _displayListenerView(self, data={}):
-        listener_list = []
-        for listener in self.cron_listener_list:
-            listener_data = {
-                'id': listener.getId(),
-                'enabled': self.cronconf.is_task_listener_enabled(listener),
-                'description': listener.getDescription()
-            }
-            listener_list.append(listener_data)
-        data['listener_list'] = listener_list
-        return 'cron_listener.html', data
-
-    def _saveSettings(self, req, category, page):
-        arg_name_list = [self.cronconf.TICKER_ENABLED_KEY,
-                         self.cronconf.TICKER_INTERVAL_KEY]
-        for task in self.cron_task_list:
-            task_id = task.getId()
-            arg_name_list.append('%s.%s' % (task_id,
-                                            self.cronconf.TASK_ENABLED_KEY))
-            for schedule in self.all_schedule_type:
-                schedule_id = schedule.getId()
-                arg_name_list.append('%s.%s' % (task_id, schedule_id))
-                arg_name_list.append('%s.%s.%s' % (task_id, schedule_id,
-                                     self.cronconf.SCHEDULE_ENABLED_KEY))
-                arg_name_list.append('%s.%s.%s' % (task_id, schedule_id,
-                                     self.cronconf.SCHEDULE_ARGUMENT_KEY))
-
-        enabled_keys = ('.%s' % self.cronconf.TASK_ENABLED_KEY,
-                        '.%s' % self.cronconf.SCHEDULE_ENABLED_KEY)
-        for arg_name in arg_name_list:
-            arg_value = req.args.get(arg_name, '').strip()
-            self.env.log.debug('request args: %s=[%s]' % (arg_name, arg_value))
-            if (arg_value == ''):
-                # dont't remove the key because of default value may be True
-                if arg_name.endswith(enabled_keys):
-                    self.cronconf.set_value(arg_name, 'False')
-                else:
-                    # otherwise we can remove the key
-                    self.cronconf.remove_value(arg_name)
-            else:
-                self.cronconf.set_value(arg_name, arg_value)
-
-        self._save_config(req)
-        self.core.apply_config(wait=True)
-        req.redirect(req.abs_href.admin(category, page))
-
-    def _saveListenerSettings(self, req, category, page):
-        arg_name_list = []
-        for listener in self.cron_listener_list:
-            listener_id = listener.getId()
-            arg_name_list.append('%s.%s' % (listener_id,
-                                 self.cronconf.TASK_LISTENER_ENABLED_KEY))
-
-        for arg_name in arg_name_list:
-            arg_value = req.args.get(arg_name, '').strip()
-            self.env.log.debug('request args: %s=[%s]' % (arg_name, arg_value))
-            if (arg_value == ''):
-                # dont't remove the key because of default value may be True
-                if arg_name.endswith('.%s' % self.cronconf.TASK_ENABLED_KEY):
-                    self.cronconf.set_value(arg_name, 'False')
-                else:
-                    # otherwise we can remove the key
-                    self.cronconf.remove_value(arg_name)
-            else:
-                self.cronconf.set_value(arg_name, arg_value)
-
-        self._save_config(req)
-        self.core.apply_config(wait=True)
-        req.redirect(req.abs_href.admin(category, page))
-
-    def _clearHistory(self, req, category, page):
-        self.core.clearHistory()
-        req.redirect(req.abs_href.admin(category, page))
-
-    def _runtask(self, req):
-        taskId = req.args.get('task', '')
-
-        taskWithId = filter(lambda x: x.getId() == taskId, self.cron_task_list)
-
-        if len(taskWithId) == 0:
-            single_msg = 'The task with id %s was not found' % taskId
-            self.env.log.error(single_msg)
-            add_warning(req, single_msg)
-            req.redirect(req.href.admin('traccron', 'cron_admin'))
-        elif len(taskWithId) > 1:
-            multi_msg = 'Multiple task with id %s was not found' % taskId
-            self.env.log.error(multi_msg)
-            add_warning(req, multi_msg)
-            req.redirect(req.href.admin('traccron', 'cron_admin'))
-        else:
-            task = taskWithId[0]
-
-            #create parameters list if needed
-            value = req.args.get('parameters', None)
-            if value:
-                parameters = [item.strip() for item in value.split(',')]
-                self.core.runTask(task, parameters=parameters)
-            else:
-                self.core.runTask(task)
-            req.redirect(req.href.admin('traccron', 'cron_history'))

src/traccron/history.py

-# -*- encoding: UTF-8 -*-
-"""
-Created on 28 oct. 2010
-
-@author: thierry
-"""
-
-###############################################################################
-##
-##        O U T    O F    T H E    B O X    H I S T O R Y    S T O R E
-##
-###############################################################################
-
-from trac.core import Component, implements
-from traccron.api import IHistoryTaskExecutionStore
-
-class MemoryHistoryStore(Component, IHistoryTaskExecutionStore):
-
-    implements(IHistoryTaskExecutionStore)
-
-    history = []
-
-    def addExecution(self, task, start, end, success):
-        """
-        Add a new execution of a task into this history
-        """
-        self.history.append((task, start, end, success))
-
-    def getExecution(self, task=None, fromTime=None, toTime=None, sucess=None):
-        """
-        Return a iterator on all execution stored. Each element is a tuple
-        of (task, start time, end time, success status)
-        """
-        for h in self.history:
-            yield h
-
-    def clear(self):
-        self.history[:] = []

src/traccron/listener.py

-# -*- encoding: UTF-8 -*-
-"""
-Created on 28 oct. 2010
-
-@author: thierry
-"""
-###############################################################################
-##
-##        O U T    O F    T H E    B O X    T A S K    L I S T E N E R
-##
-###############################################################################
-
-from time import time, localtime
-
-from trac.core import Component, implements, ExtensionPoint
-from trac.notification import NotifyEmail
-from trac.web.chrome import ITemplateProvider
-from traccron.api import ITaskEventListener, IHistoryTaskExecutionStore
-from traccron.core import CronConfig
-
-
-class NotifyEmailTaskEvent(NotifyEmail):
-
-    template_name = 'notify_task_event_template.txt'
-
-    def __init__(self, env):
-        NotifyEmail.__init__(self, env)
-        self.cronconf = CronConfig(self.env)
-
-    def get_recipients(self, resid):
-        """
-        Return the recipients as defined in trac.ini.
-        """
-        reclist = self.cronconf.get_email_notifier_task_recipient_list()
-        return (reclist, [])
-
-    def notifyTaskEvent(self, task_event_list):
-        """
-        Send task event by mail if recipients is defined in trac.ini
-        """
-        self.env.log.debug('notifying task event...')
-        if self.cronconf.get_email_notifier_task_recipient():
-            # prepare the data for the email content generation
-            mess = ''
-            start = True
-            for event in task_event_list:
-                event_time = 'at %d h %d' % event.time[3:5]  # hour, min
-                if start:
-                    mess += 'task[%s]' % event.task.getId()
-                    mess += '\nstarted %s' % event_time
-                    mess += '\n'
-                else:
-                    mess += 'ended %s' % event_time
-                    if event.success:
-                        mess += '\nsuccess'
-                    else:
-                        mess += '\nFAILURE'
-                    mess += '\n\n'
-                start = not start
-
-            self.data.update(notify_body=mess)
-            NotifyEmail.notify(self, None, 'task event notification')
-        else:
-            self.env.log.debug('no recipient for task event, aborting')
-
-    def send(self, torcpts, ccrcpts):
-        return NotifyEmail.send(self, torcpts, ccrcpts)
-
-
-class StartTaskEvent():
-    """
-    Store the event of a task start
-    """
-    def __init__(self, task):
-        self.task = task
-        self.time = localtime(time())
-
-
-class EndTaskEvent():
-    """
-    Store the event of a task end
-    """
-    def __init__(self, task, success):
-        self.task = task
-        self.time = localtime(time())
-        self.success = success
-
-
-class NotificationEmailTaskEvent(Component, ITaskEventListener,
-                                 ITemplateProvider):
-    """
-    This task listener send notification mail about task event.
-    """
-    implements(ITaskEventListener, ITemplateProvider)
-
-    def __init__(self):
-        self.cronconf = CronConfig(self.env)
-        self.task_event_buffer = []
-        self.task_count = 0
-        self.notifier = None
-
-    def get_htdocs_dirs(self):
-        return []
-
-    def get_templates_dirs(self):
-        from pkg_resources import resource_filename
-        return [resource_filename(__name__, 'templates')]
-
-    def onStartTask(self, task):
-        """
-        called by the core system when the task is triggered,
-        just before the waek_up method is called
-        """
-        self.task_event_buffer.append(StartTaskEvent(task))
-        self.task_count = self.task_count + 1
-
-    def onEndTask(self, task, success):
-        """
-        called by the core system when the task execution is finished,
-        just after the task wake_up method exit
-        """
-        if (self.cronconf.is_email_notifier_only_error() and success):
-            self.task_event_buffer.pop()
-            self.task_count -= 1
-            return
-
-        self.task_event_buffer.append(EndTaskEvent(task, success))
-        # if the buffer reach the count then we notify
-        if (self.task_count >= self.cronconf.get_email_notifier_task_limit()):
-            # send the mail
-            if not self.notifier:
-                self.notifier = NotifyEmailTaskEvent(self.env)
-            self.notifier.notifyTaskEvent(self.task_event_buffer)
-
-            # reset task event buffer
-            self.task_event_buffer[:] = []
-            self.task_count = 0
-
-    def getId(self):
-        return self.cronconf.EMAIL_NOTIFIER_TASK_BASEKEY
-
-    def getDescription(self):
-        return self.__doc__
-
-
-class HistoryTaskEvent(Component, ITaskEventListener):
-    """
-    This task event listener catch task execution to fill all History store
-    in its environment
-    """
-    implements(ITaskEventListener)
-
-    history_store_list = ExtensionPoint(IHistoryTaskExecutionStore)
-
-    def onStartTask(self, task):
-        """
-        called by the core system when the task is triggered,
-        just before the wake_up method is called
-        """
-        self.task = task
-        self.start = time()
-
-    def onEndTask(self, task, success):
-        """
-        called by the core system when the task execution is finished,
-        just after the task wake_up method exit
-        """
-        # currently Core assume that task are not threaded so any end event
-        # match the previous start event
-        assert task.getId() == self.task.getId()
-        self.end = time()
-        self.success = success
-
-        # notify all history store
-        self._notify_history()
-
-    def getId(self):
-        """
-        return the id of the listener. It is used in trac.ini
-        """
-        return 'history_task_event'
-
-    def getDescription(self):
-        return self.__doc__
-
-    def _notify_history(self):
-        for hist in self.history_store_list:
-            hist.addExecution(self.task, self.start, self.end, self.success)

src/traccron/scheduler.py

-# -*- encoding: UTF-8 -*-
-"""
-Created on 28 oct. 2010
-
-@author: thierry
-"""
-###############################################################################
-##
-##          O U T    O F    T H E    B O X    S C H E D U L E R
-##
-###############################################################################
-
-import re
-
-from trac.core import Component, implements
-from traccron.api import ISchedulerType
-from traccron.core import CronConfig
-
-
-class SchedulerType(ISchedulerType):
-    """
-    Define a sort of scheduling. Base class for any scheduler type
-    implementation
-    """
-    implements(ISchedulerType)
-
-    def __init__(self):
-        self.cronconf = CronConfig(self.env)
-
-    def getId(self):
-        """
-        Return the id to use in trac.ini for this schedule type
-        """
-        raise NotImplementedError
-
-    def getHint(self):
-        """
-        Return a description of what it is and the format used to defined
-        the schedule
-        """
-        return ''
-
-    def isTriggerTime(self, task, currentTime):
-        """
-        Test is accordingly to this scheduler and given currentTime,
-        is time to fire the task
-        """
-        # read the configuration value for the task
-        self.env.log.debug('looking for schedule of type: %s' % self.getId())
-        for schedule_value in self._get_task_schedule_value_list(task):
-            msg = 'task is scheduled: %s, %s' % (task.getId(), schedule_value)
-            self.env.log.debug(msg)
-            if self.compareTime(currentTime, schedule_value):
-                return True
-        self.env.log.debug('no matching schedule found')
-        return False
-
-    def compareTime(self, currentTime, schedule_value):
-        """
-        Test is accordingly to this scheduler, given currentTime and
-        schedule value, is time to fire the task.
-        currentTime is a structure computed by time.localtime(time())
-        scheduled_value is the value of the configuration in trac.ini
-        """
-        raise NotImplementedError
-
-    def _get_task_schedule_value_list(self, task):
-        return self.cronconf.get_schedule_value_list(task, self)
-
-    def _output_comp_debug_log(self, currentTime, schedule_value):
-        self.env.log.debug('compareTime: %s, %s, %s' % (
-                           self.getId(), currentTime, schedule_value))
-
-
-class DailyScheduler(Component, SchedulerType):
-    """
-    Scheduler that trigger a task once a day based uppon a defined time
-    """
-
-    def __init__(self):
-        SchedulerType.__init__(self)
-
-    def getId(self):
-        return 'daily'
-
-    def getHint(self):
-        return 'ex: 8h30 fire every day at 8h30'
-
-    def compareTime(self, currentTime, schedule_value):
-        self._output_comp_debug_log(currentTime, schedule_value)
-        return schedule_value == '%sh%s' % currentTime[3:5]
-
-
-class HourlyScheduler(Component, SchedulerType):
-    """
-    Scheduler that trigger a task once an hour at a defined time
-    """
-
-    def __init__(self):
-        SchedulerType.__init__(self)
-
-    def getId(self):
-        return 'hourly'
-
-    def getHint(self):
-        return 'ex: 45 fire every hour at 0h45 then 1h45 and so on'
-
-    def compareTime(self, currentTime, schedule_value):
-        self._output_comp_debug_log(currentTime, schedule_value)
-        return schedule_value == str(currentTime.tm_min)
-
-
-class WeeklyScheduler(Component, SchedulerType):
-    """
-    Scheduler that trigger a task once a week at a defined day and time
-    """
-
-    def __init__(self):
-        SchedulerType.__init__(self)
-
-    def getId(self):
-        return 'weekly'
-
-    def getHint(self):
-        return 'ex: 0@12h00 fire every monday at 12h00'
-
-    def compareTime(self, currentTime, schedule_value):
-        self._output_comp_debug_log(currentTime, schedule_value)
-        return schedule_value == '%s@%sh%s' % (currentTime.tm_wday,
-                                               currentTime.tm_hour,
-                                               currentTime.tm_min)
-
-
-class MonthlyScheduler(Component, SchedulerType):
-    """
-    Scheduler that trigger a task once a week at a defined day and time
-    """
-
-    def __init__(self):
-        SchedulerType.__init__(self)
-
-    def getId(self):
-        return 'monthly'
-
-    def getHint(self):
-        msg = 'ex: 15@12h00 fire every month on the 15th day of month at 12h00'
-        return msg
-
-    def compareTime(self, currentTime, schedule_value):
-        self._output_comp_debug_log(currentTime, schedule_value)
-        return schedule_value == '%s@%sh%s' % (currentTime.tm_mday,
-                                               currentTime.tm_hour,
-                                               currentTime.tm_min)
-
-
-class CronScheduler(Component, SchedulerType):
-    """
-    Scheduler that used a cron-like syntax to specified when task must
-    be triggered. Use cron like expression,
-    set either day-of-month or day-of-week and other one must be '?'
-
-    ex: *  *  *  ?  *  ?  *
-        ┬  ┬  ┬  ┬  ┬  ┬  ┬
-        │  │  │  │  │  │  └--── year (omissible)
-        │  │  │  │  │  └─────── day of week (1 - 7, 1 is Monday)
-        │  │  │  │  └────────── month (1 - 12)
-        │  │  │  └───────────── day of month (1 - 31)
-        │  │  └──────────────── hour (0 - 23)
-        │  └─────────────────── min (0 - 59)
-        └────────────────────── None (reserved?)
-    """
-    cron_syntax = re.compile(r"""
-        (?P<value>
-            \* |
-            (?P<pre>\d+)(?P<sep>[-|\/])(?P<post>\d+) |  # e.g.) 1-31, 0/2
-            \d+
-        )""", re.X)
-
-    class AllMatch(set):
-        """
-        Universal set - match everything
-        Stand for * in cron expression
-        """
-        def __contains__(self, item):
-            return True
-
-    class OmitMatch(AllMatch):
-        """
-        Stand for ? in cron expression
-        """
-        pass
-
-    class CronExpressionError(Exception):
-        pass
-
-    _allMatch = AllMatch()
-    _omitMatch = OmitMatch()
-
-    # The actual Event class
-    class Event(object):
-        def __init__(self, min, hour, day, month, dow, year):
-            self.mins = self.conv_to_set(min)
-            self.hours = self.conv_to_set(hour)
-            self.days = self.conv_to_set(day)
-            self.months = self.conv_to_set(month)
-            self.dow = self.conv_to_set(dow)
-            self.year = self.conv_to_set(year)
-
-        def conv_to_set(self, obj):  # Allow single integer to be provided
-            if isinstance(obj, (int, long)):
-                return set([obj])  # Single item
-            elif not isinstance(obj, set):
-                return set(obj)
-            else:
-                return obj
-
-        def matchtime(self, t):
-            """
-            Return True if this event should trigger at the specified localtime
-            """
-            return ((t.tm_min in self.mins) and
-                    (t.tm_hour in self.hours) and
-                    (t.tm_mday in self.days) and
-                    (t.tm_mon in self.months) and
-                    (t.tm_wday in self.dow) and
-                    (t.tm_year in self.year))
-
-    def __init__(self):
-        SchedulerType.__init__(self)
-
-    def getId(self):
-        return 'cron'
-
-    def getHint(self):
-        return 'ex: * 5 10 * * ? * fire every day at 10h5'
-
-    def compareTime(self, currentTime, schedule_value):
-        self._output_comp_debug_log(currentTime, schedule_value)
-        try:
-            cron_settings = self._parse_cron_expression(schedule_value)
-        except CronScheduler.CronExpressionError:
-            self.env.log.debug('Failed to parse cron expression')
-            return False
-        else:
-            return CronScheduler.Event(**cron_settings).matchtime(currentTime)
-
-    def _output_range_error(self, value, name, min_value, max_value):
-        msg = 'invalid cron expression: %s is out of range [%d-%d] for %s'
-        self.env.log.error(msg % (value, min_value, max_value, name))
-
-    def _get_cron_value_range(self, field, name, min_value, max_value, adjust):
-        _begin = int(field.get('pre'))
-        _end = int(field.get('post'))
-        if _begin < min_value:
-            self._output_range_error(_begin, name, min_value, max_value)
-            raise CronScheduler.CronExpressionError()
-        elif _end > max_value:
-            self._output_range_error(_end, name, min_value, max_value)
-            raise CronScheduler.CronExpressionError()
-        # cron range expression is inclusive
-        return range(_begin + adjust, _end + 1 + adjust)
-
-    def _get_cron_value_every(self, field, name, min_value, max_value, adjust):
-        _begin = int(field.get('pre'))
-        if ((_begin < min_value) or (_begin > max_value)):
-            self._output_range_error(_begin, name, min_value, max_value)
-            raise CronScheduler.CronExpressionError()
-        _step = int(field.get('post'))
-        # cron range expression is inclusive
-        return range(_begin + adjust, max_value + 1 + adjust, _step)
-
-    def _get_cron_value_int(self, value, name, min_value, max_value, adjust):
-        _value = int(value)
-        if (_value < min_value) or (_value > max_value):
-            self._output_range_error(value, name, min_value, max_value)
-            raise CronScheduler.CronExpressionError()
-        return _value + adjust
-
-    def _parse_cron_value(self, name, value, min_value, max_value, adjust=0):
-        """
-        utility method to parse value of a cron item.
-        Support of *, range expression (ex 1-10)
-        adjust is used to translate value
-        (ex: first day of week is 0 in python and 1 in Cron)
-        """
-        match = re.match(CronScheduler.cron_syntax, value)
-        if not match:
-            self.env.log.error('invalid cron expression: ')
-            raise CronScheduler.CronExpressionError()
-
-        cron_field = match.groupdict()
-        cron_value = cron_field.get('value')
-        cron_sep = cron_field.get('sep')
-        if cron_value == '*':
-            return CronScheduler._allMatch
-        elif cron_sep == '-':
-            return self._get_cron_value_range(cron_field, name,
-                                              min_value, max_value, adjust)
-        elif cron_sep == '/':
-            return self._get_cron_value_every(cron_field, name,
-                                              min_value, max_value, adjust)
-        else:
-            return self._get_cron_value_int(cron_value, name,
-                                            min_value, max_value, adjust)
-
-    def _parse_cron_day_fields(self, day_of_month, day_of_week):
-        if (day_of_month == '?' and day_of_week == '?') or \
-           (day_of_month != '?' and day_of_week != '?'):
-            self.env.log.error('invalid cron expression: set value '
-                               'either day-of-month or day-of-week')
-            raise CronScheduler.CronExpressionError()
-
-        if day_of_month != '?':
-            day_value = self._parse_cron_value('day', day_of_month, 1, 31)
-            dow_value = CronScheduler._omitMatch
-        elif day_of_week != '?':
-            day_value = CronScheduler._omitMatch
-            dow_value = self._parse_cron_value('day-of-week', day_of_week,
-                                               1, 7, adjust=-1)
-        return day_value, dow_value
-
-    def _parse_cron_expression(self, cron_text):
-        """
-        Parse cron expression and return dictionary of argument key/value
-        suitable for Event object
-        """
-        self.env.log.debug('parsing cron expression: "%s"' % cron_text)
-        if not cron_text:
-            raise CronScheduler.CronExpressionError()
-
-        cron_params = cron_text.split()[0:7]
-        cron_params_length = len(cron_params)
-
-        if cron_params_length < 6:
-            self.env.log.error('cron expression must have at least 6 items')
-            raise CronScheduler.CronExpressionError()
-        elif cron_params_length == 6:
-            cron_params.append('*')  # year field can be abbreviate
-
-        day_dow = cron_params[3], cron_params[5]
-        day_value, dow_value = self._parse_cron_day_fields(*day_dow)
-        cron_settings = {
-            'min': self._parse_cron_value('min', cron_params[1], 0, 59),
-            'hour': self._parse_cron_value('hour', cron_params[2], 0, 23),
-            'day': day_value,
-            'month': self._parse_cron_value('month', cron_params[4], 1, 12),
-            'dow': dow_value,
-            'year': self._parse_cron_value('year', cron_params[6], 1970, 2099)
-        }
-        self.env.log.debug('result of parsing is %s' % cron_settings)
-        return cron_settings

src/traccron/task.py

-# -*- encoding: UTF-8 -*-
-"""
-Created on 28 oct. 2010
-
-@author: thierry
-"""
-###############################################################################
-##
-##             O U T    O F    T H E    B O X    T A S K
-##
-###############################################################################
-from datetime import datetime, timedelta
-from time import time, localtime
-
-from trac.ticket.model import Ticket
-from trac.core import Component, implements
-from trac.notification import NotifyEmail
-from trac.util.datefmt import utc, from_utimestamp, to_utimestamp
-from trac.web.chrome import ITemplateProvider
-from traccron.api import ICronTask
-from traccron.core import CronConfig
-
-
-class HeartBeatTask(Component, ICronTask):
-    """
-    This is a simple task for testing purpose.
-    It only write a trace in log at debug level
-    """
-    implements(ICronTask)
-
-    def wake_up(self, *args):
-        if len(args) > 0:
-            for arg in args:
-                self.env.log.debug('Heart beat: %s' % arg)
-        else:
-            self.env.log.debug('Heart beat: boom boom !!!')
-
-    def getId(self):
-        return 'heart_beat'
-
-    def getDescription(self):
-        return self.__doc__
-
-
-class SleepingTicketNotification(NotifyEmail):
-
-    template_name = 'sleeping_ticket_template.txt'
-
-    def __init__(self, env):
-        NotifyEmail.__init__(self, env)
-
-    def get_recipients(self, owner):
-        return ([owner], [])
-
-    def remind(self, tiketsByOwner, delay):
-        """
-        Send a digest mail to ticket owner to remind him of those
-        sleeping tickets
-        """
-        for owner in tiketsByOwner:
-            # prepare the data for the email content generation
-            self.data.update({
-                'ticket_count': len(tiketsByOwner[owner]),
-                'delay': delay
-            })
-            NotifyEmail.notify(self, owner, 'Sleeping ticket notification')
-
-    def send(self, torcpts, ccrcpts):
-        return NotifyEmail.send(self, torcpts, ccrcpts)
-
-
-class OrphanedTicketNotification(NotifyEmail):
-
-    template_name = 'orphaned_ticket_template.txt'
-
-    def __init__(self, env):
-        NotifyEmail.__init__(self, env)
-
-    def get_recipients(self, reporter):
-        return ([reporter], [])
-
-    def remind(self, tiketsByReporter, delay):
-        """
-        Send a digest mail to the reporter to remind them
-        of those orphaned tickets
-        """
-        for reporter in tiketsByReporter:
-            # prepare the data for the email content generation
-            self.data.update({
-                'ticket_count': len(tiketsByReporter[reporter]),
-                'delay': delay
-            })
-            NotifyEmail.notify(self, reporter, 'orphaned ticket notification')
-
-    def send(self, torcpts, ccrcpts):
-        return NotifyEmail.send(self, torcpts, ccrcpts)
-
-
-class SleepingTicketReminderTask(Component, ICronTask, ITemplateProvider):
-    """
-    Remind user about sleeping ticket they are assigned to.
-    """
-    implements(ICronTask, ITemplateProvider)
-
-    delay_days = 3
-
-    select_assigned_ticket = """
-        SELECT t.id , t.owner
-        FROM ticket t, ticket_change tc
-        WHERE t.id = tc.ticket
-        AND t.status in ('new','assigned','accepted')
-        AND (SELECT MAX(tc2.time)
-             FROM ticket_change tc2
-             WHERE tc2.ticket=tc.ticket) < %s
-        GROUP BY t.id
-    """
-
-    select_orphaned_ticket = """
-       SELECT t.id, t.reporter
-       FROM ticket t
-       WHERE t.id not in (SELECT tc.ticket
-                          FROM ticket_change tc
-                          WHERE tc.ticket=t.id)
-       AND t.time < %s
-       AND t.status = 'new'
-    """
-
-    def get_htdocs_dirs(self):
-        return []
-
-    def get_templates_dirs(self):
-        from pkg_resources import resource_filename
-        return [resource_filename(__name__, 'templates')]
-
-    def _find_sleeping_ticket(self, sql, delay, dico, msg):
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        delay_time = datetime.now(utc) - timedelta(days=delay)
-        cursor.execute(sql, (to_utimestamp(delay_time),))
-        for ticket, recipient in cursor:
-            self.env.log.info(msg % (ticket, recipient, delay))
-            if recipient in dico:
-                dico[recipient].append(ticket)
-            else:
-                dico[recipient] = [ticket]
-
-    def remind_assigned_ticket(self, delay):
-        dico = {}
-        msg = 'warning sleeping ticket %d assigned to %s ' \
-              'but is inactive since more than %d day'
-        self._find_sleeping_ticket(self.select_assigned_ticket,
-                                   delay, dico, msg)
-        SleepingTicketNotification(self.env).remind(dico, delay)
-
-    def remind_orphaned_ticket(self, delay):
-        dico = {}
-        msg = 'warning ticket %d is new but orphaned: %s, %d'
-        self._find_sleeping_ticket(self.select_orphaned_ticket,
-                                   delay, dico, msg)
-        OrphanedTicketNotification(self.env).remind(dico, delay)
-
-    def wake_up(self, *args):
-        delay = SleepingTicketReminderTask.delay_days
-        if len(args) > 0:
-            delay = int(args[0])
-
-        # look for ticket assigned but not touched since more that the delay
-        self.remind_assigned_ticket(delay)
-        self.remind_orphaned_ticket(delay)
-
-    def getId(self):
-        return 'sleeping_ticket'
-
-    def getDescription(self):
-        return self.__doc__
-
-
-class BaseTicketNotification(NotifyEmail):
-
-    def __init__(self, env, milestone):
-        NotifyEmail.__init__(self, env)
-        self.milestone = milestone
-
-    def get_recipients(self, recipient):
-        return ([recipient], [])
-
-    def send(self, torcpts, ccrcpts):
-        return NotifyEmail.send(self, torcpts, ccrcpts)
-
-    def populate_unreachable_tickets_data(self, tickets):
-        # we are not called if there is no tickets
-        self.data['milestone'] = tickets[0]['milestone']
-        due_date = from_utimestamp(tickets[0]['due'])
-        self.data['due_date'] = due_date.strftime('%Y-%m-%d')
-
-        tickets_list = ''
-        for ticket in tickets:
-            tickets_list += ticket['summary'] + '\n'
-            tickets_list += self.env.abs_href.ticket(ticket['ticket']) + '\n'
-            tickets_list += '\n'
-
-        self.data['tickets_list'] = tickets_list
-
-    def notify_opened_ticket(self, recipient, tickets):
-        """
-        Send a digest mail to recipients (e.g. ticket owner and reporter)
-        about ticket still opened
-        """
-        self.populate_unreachable_tickets_data(tickets)
-        subject = 'Milestone %s with still opened ticket' % self.milestone
-        NotifyEmail.notify(self, recipient, subject)
-        self.env.log.debug('notify opened ticket: %s, %s, %s' % (
-                           recipient, self.milestone, self.__class__.__name__))
-
-
-class ReporterOpenedTicketNotification(BaseTicketNotification):
-    """
-    Notify reporter about an opened ticket in a near milestone
-    """
-    template_name = "opened_ticket_for_reporter_template.txt"
-
-    def __init__(self, env, milestone):
-        BaseTicketNotification.__init__(self, env, milestone)
-
-
-class OwnerOpenedTicketNotification(BaseTicketNotification):
-    """
-    Notify owner about an opened ticket in a near milestone
-    """
-    template_name = "opened_ticket_for_owner_template.txt"
-
-    def __init__(self, env, milestone):
-        BaseTicketNotification.__init__(self, env, milestone)
-
-
-class UnreachableMilestoneNotification(BaseTicketNotification):
-    """
-    Notify the specified person (ex: admin, release manager) that a milestone
-    is about to closed but there still are opened ticket
-    """
-    template_name = 'unreachable_milestone_template.txt'
-
-    def __init__(self, env, milestone):
-        BaseTicketNotification.__init__(self, env, milestone)
-        self.cronconf = CronConfig(self.env)
-
-    def get_recipients(self, milestone):
-        reclist = self.cronconf.get_unreachable_milestone_task_recipient_list()
-        return (reclist, [])
-
-    def notify_unreachable_milestone(self, tickets):
-        """
-        Send a digest mail listing all tickets still opened in the milestone
-        """
-        self.populate_unreachable_tickets_data(tickets)
-        subject = "Milestone %s still has opened ticket" % self.milestone
-        NotifyEmail.notify(self, self.milestone, subject)
-        self.env.log.debug('notify unreachable milestone: %s, %s' % (
-                           self.milestone, self.__class__.__name__))
-
-
-class UnreachableMilestoneTask(Component, ICronTask, ITemplateProvider):
-    """
-    Send notification about near milestone with opened ticked
-    """
-    implements(ICronTask, ITemplateProvider)
-
-    ahead_days = 3
-
-    select_near_milestone_ticket = """
-        SELECT t.id , t.owner, t.reporter, t.milestone, t.summary, m.due
-        FROM ticket t, milestone m
-        WHERE t.milestone = m.name
-        AND m.due < %s
-    """
-
-    def __init__(self):
-        self.cronconf = CronConfig(self.env)
-
-    def get_htdocs_dirs(self):
-        return []
-
-    def get_templates_dirs(self):
-        from pkg_resources import resource_filename
-        return [resource_filename(__name__, 'templates')]
-
-    def _set_recipient_data(self, dico, recipient, milestone, ticket_data):
-        dico.setdefault(recipient, {}).__setitem__(milestone, [ticket_data])
-        dico[recipient].setdefault(milestone, []).append(ticket_data)
-
-    def remind_unreachable_recipients(self, dico):
-        """
-        Send notification for each milestone
-        """
-        for milestone in dico:
-            notifier = UnreachableMilestoneNotification(self.env, milestone)
-            notifier.notify_unreachable_milestone(dico[milestone])
-
-    def remind_recipient(self, dico, klass):
-        """
-        Send notification for owner or reporter
-        """
-        for recipient in dico:
-            _dico = dico[recipient]
-            for milestone in _dico:
-                notifier = klass(self.env, milestone)
-                notifier.notify_opened_ticket(recipient, _dico[milestone])
-
-    def wake_up(self, *args):
-        ahead = UnreachableMilestoneTask.ahead_days
-        if len(args) > 0:
-            ahead = int(args[0])
-
-        # look opened ticket in near milestone
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        # select ticket whom milestone are due in less than some days ahead
-        ahead_time = datetime.now(utc) + timedelta(days=ahead)
-        cursor.execute(self.select_near_milestone_ticket,
-                       (to_utimestamp(ahead_time),))
-
-        dico, dico_owner, dico_reporter = {}, {}, {}
-        for ticket, owner, reporter, milestone, summary, due in cursor:
-            msg = 'warning ticket %d will probably miss its milestone %s' % (
-                  ticket, milestone)
-            self.env.log.info(msg)
-            ticket_data = {
-                'ticket': ticket,
-                'owner': owner,
-                'reporter': reporter,
-                'milestone': milestone,
-                'summary': summary,
-                'due': due
-            }
-            dico.setdefault(milestone, []).append(ticket_data)
-            self._set_recipient_data(dico_owner, owner, milestone, ticket_data)
-            self._set_recipient_data(dico_reporter, reporter, milestone,
-                                     ticket_data)
-
-        self.remind_unreachable_recipients(dico)
-        self.remind_recipient(dico_owner, OwnerOpenedTicketNotification)
-        self.remind_recipient(dico_reporter, ReporterOpenedTicketNotification)
-
-    def getId(self):
-        return self.cronconf.UNREACHABLE_MILESTONE_TASK_BASEKEY
-
-    def getDescription(self):
-        return self.__doc__
-
-
-class AutoPostponeTask(Component, ICronTask):
-    """
-    Scan closed milestone for still opened ticket then posptone those tickets
-    to the next milestone
-    """
-    implements(ICronTask)
-
-    select_next_milestone = """
-        SELECT m.name, m.completed, m.due
-        FROM milestone m
-        WHERE m.completed is NULL or m.completed = 0
-        AND m.due not NULL and m.due > 0
-        ORDER BY m.due ASC LIMIT 1
-    """
-
-    select_closed_milestone_ticket = """
-        SELECT t.id , t.milestone
-        FROM ticket t, milestone m
-        WHERE t.status != 'closed'
-        AND t.milestone = m.name
-        AND m.completed not NULL and m.completed > 0
-    """
-
-    def wake_up(self, *args):
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        # find still opened more recent milestone
-        # select ticket whom milestone are due in less than specified delay
-        next_milestone = None
-        cursor.execute(self.select_next_milestone)
-        for name, completed, due in cursor:
-            next_milestone = name
-
-        if not next_milestone:
-            msg = 'No opened milestone found. Cannot postpone tickets'
-            self.env.log.debug(msg)
-            return
-
-        # select ticket whom milestone are due in less than specified delay
-        cursor.execute(self.select_closed_milestone_ticket)
-        for ticket_id, milestone in cursor:
-            msg = 'Ticket %s is opened in closed milestone %s. ' \
-                  'Should postpone this ticket to %s' % (
-                  ticket_id, milestone, next_milestone)
-            self.env.log.debug(msg)
-            ticket = Ticket(self.env, ticket_id)
-            ticket.populate({'milestone': next_milestone})
-            ticket.save_changes(self.getId(), msg)
-
-    def getId(self):
-        return 'auto_postpone'
-
-    def getDescription(self):
-        return self.__doc__

src/traccron/templates/cron_admin.html

-<!DOCTYPE html
-    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:i18n="http://genshi.edgewall.org/i18n">
-  <xi:include href="admin.html" />
-
-<head>
-
-
-<title>Trac Cron Scheduler</title>
-</head>
-
-<body>
-
-
-<form id="traccron_admin" action="" method="post">
-
-    <fieldset>
-    	<legend>ticker_enabled</legend>
-    	<input type="text" name="ticker_enabled" value="$ticker_enabled"/>    	
-    	<p class="hint">
-              Possible value are True, False, On, Off,1 or 0 
-        </p>    	
-    </fieldset>
-     <fieldset>
-    	<legend>ticker_interval</legend>
-    	<input type="text" name="ticker_interval" value="$ticker_interval"/>
-    	<p class="hint">
-              minute(s) between each ticker wake up 
-        </p>    	
-    </fieldset>
-    <fieldset>
-    	<legend>Task list</legend>
-    	<table class="listing" width="100%"  border="1">
-    		<thead>
-    			<tr>    			
-    				<th>id</th>
-    	 			<th>description</th>
-    	 			<th class="sel">active</th>
-    	 			<th>schedule</th>
-    	 		</tr>
-    	 	</thead>
-    	 	<tbody>
-    	 		<tr py:for="task in task_list">
-	    	   		<td>${task['id']}<a href="${href.traccron('runtask',[('task',task['id'])])}" title="Run now">&rarr;</a></td>
-    	   			<td>${task['description']}</td>
-    	   			<td>
-    	   				 <py:choose test="task['enabled']">    	   				 	
-		                 	<input py:when="True" type="checkbox" name="${task['id']}.enabled" value="True" checked=""/>
-		                 	<py:otherwise>
-		                 		<input type="checkbox" name="${task['id']}.enabled" value="True"/>
-		                 	</py:otherwise>
-		                 </py:choose>
-                	</td>
-    	   			<td>
-	    	   			<table class="listing" width="100%">
-	    	   				<thead>
-  		  						<tr>    			
-    								<th class="sel">Used</th>
-    								<th>type</th>
-    	 							<th>value</th>    	 							
-    	 							<th>arguments</th>
-    	 						</tr>
-    	 					</thead>
-    	   					<tbody>
-    	   						<tr py:for="type in task['schedule_list'].keys()">
-    	   						    <td>
-    	   						     	<py:choose test="task['schedule_list'][type]['enabled']">    	   						     		    	 
-		                 					<input py:when="True" type="checkbox" name="${task['id']}.${type}.enabled" value="True" checked=""/>
-		                 					<py:otherwise>
-		                 						<input type="checkbox" name="${task['id']}.${type}.enabled" value="True"/>
-		                 					</py:otherwise>
-		                 				</py:choose>
-                					</td>
-		    	   					<td align="right">$type</td>
-    	   							<td align="left">
-			    	   					<input type="text" id="${task['id']}.${type}" name="${task['id']}.${type}" value="${task['schedule_list'][type]['value']}"/>
-			    	   					<p class="hint">
-             								 ${task['schedule_list'][type]['hint']} 
-        								</p> 
-    	   							</td>
-    	   							<td>
-    	   								<input type="text" id="${task['id']}.${type}.arg" name="${task['id']}.${type}.arg" value="${task['schedule_list'][type]['arg']}"/>
-										<a href="${href.traccron('runtask',[('task',task['id']),('parameters',task['schedule_list'][type]['arg'])])}" title="Run now">&rarr;</a>    	   								    	   								    	   								
-    	   							</td>        	   								   						
-    	   						</tr>
-    	   					</tbody>
-    	   				</table>
-    	   			</td>
-    	 		</tr>
-    	 	</tbody>
-    	</table>
-    </fieldset>
-    <input type="submit" name="save" value="save" accesskey="s" />
-
-</form>
-
-</body>
-
-</html>

src/traccron/templates/cron_history.html

-<!DOCTYPE html
-    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:i18n="http://genshi.edgewall.org/i18n">
-  <xi:include href="admin.html" />
-
-<head>
-
-
-<title>Trac Cron Scheduler</title>
-</head>
-
-<body>     
-	<form id="traccron_admin" action="" method="post">
-  		<fieldset>
-    		<legend>Task execution history</legend>
-			<table class="listing" width="100%"  border="1">
-		    	<thead>
-    				<tr>    			
-		    			<th>Date</th>
-    	 				<th>task</th>