"""A scheduler for periodic jobs that uses the builder pattern to
construct easily readable definitions for periodic tasts.

Examples:
    every(10).minutes.do(job) -- Run job() every 10 minutes
    every().hour.do(job) -- Run job() every hour
    every().day.at('10:30').do(job) -- Run job() every day at 10:30

Additional reading:
    http://adam.heroku.com/past/2010/6/30/replace_cron_with_clockwork/
    https://devcenter.heroku.com/articles/scheduled-jobs-custom-clock-processes
    https://devcenter.heroku.com/articles/clock-processes-python
"""
import datetime
import logging
import time

logger = logging.getLogger('schedule')


class Scheduler(object):
    def __init__(self):
        self.jobs = set()

    def run_pending(self):
        """Run all jobs that are scheduled to run.

        Please note that it is *intended behavior that tick() does not
        run missed jobs*. For example, if you've registered a job that
        should run every minute and you only call tick() in one hour
        increments then your job won't be run 60 times in between but
        only once.
        """
        runnable_jobs = [job for job in self.jobs if job.should_run]
        for job in runnable_jobs:
            job.run()

    def run_all(self, delay_seconds=60):
        """Run all jobs regardless if they are scheduled to run or not.

        A delay of `delay` seconds is added between each job. This helps
        distribute system load generated by the jobs more evenly
        over time."""
        logger.info('Running *all* %i jobs with %is delay inbetween',
                    len(self.jobs), delay_seconds)
        for job in self.jobs:
            job.run()
            time.sleep(delay_seconds)

    def clear(self):
        """Deletes all scheduled jobs."""
        self.jobs.clear()

    def every(self, interval=1):
        job = PeriodicJob(interval)
        self.jobs.add(job)
        return job


class PeriodicJob(object):
    def __init__(self, interval):
        self.interval = interval  # pause interval * unit between runs
        self.job_func = None  # the job job_func to run
        self.unit = None  # time units, e.g. 'minutes', 'hours', ...
        self.time = None  # optional time at which this job runs
        self.last_run = None  # datetime of the last run
        self.next_run = None  # datetime of the next run
        self.period = None  # timedelta between runs

    def __repr__(self):
        def format_time(t):
            return t.strftime("%Y-%m-%d %H:%M:%S") if t else '[never]'

        timestats = '(last run: %s, next run: %s)' % (
                    format_time(self.last_run), format_time(self.next_run))

        if self.time is not None:
            return 'Every %s %s at %s do %s %s' % (
                   self.interval,
                   self.unit[:-1] if self.interval == 1 else self.unit,
                   self.time, self.job_func.__name__, timestats)
        else:
            return 'Every %s %s do %s %s' % (
                   self.interval,
                   self.unit[:-1] if self.interval == 1 else self.unit,
                   self.job_func.__name__, timestats)

    @property
    def second(self):
        assert self.interval == 1
        return self.seconds

    @property
    def seconds(self):
        self.unit = 'seconds'
        return self

    @property
    def minute(self):
        assert self.interval == 1
        return self.minutes

    @property
    def minutes(self):
        self.unit = 'minutes'
        return self

    @property
    def hour(self):
        assert self.interval == 1
        return self.hours

    @property
    def hours(self):
        self.unit = 'hours'
        return self

    @property
    def day(self):
        assert self.interval == 1
        return self.days

    @property
    def days(self):
        self.unit = 'days'
        return self

    @property
    def week(self):
        assert self.interval == 1
        return self.weeks

    @property
    def weeks(self):
        self.unit = 'weeks'
        return self

    def at(self, time_str):
        """Schedule the job every day at a specific time.

        Calling this is only valid for jobs scheduled to run every
        N day(s).
        """
        assert self.unit == 'days'
        hour, minute = [int(t) for t in time_str.split(':')]
        assert 0 <= hour <= 23
        assert 0 <= minute <= 59
        self.time = datetime.time(hour, minute)
        return self

    def do(self, job_func):
        """Specifies the `job_func` that should be called every time the
        job runs.
        """
        self.job_func = job_func
        self._schedule_next_run()
        return self

    @property
    def should_run(self):
        """True if the job should be run now."""
        return datetime.datetime.now() >= self.next_run

    def run(self):
        """Run the job."""
        logger.info('Running job %s', self)
        self.job_func()
        self.last_run = datetime.datetime.now()
        self._schedule_next_run()

    def _schedule_next_run(self):
        """Compute the datetime when this job should run next."""
        def schedule_at_specific_time():
            next_run = (datetime.datetime.today() +
                        datetime.timedelta(days=self.interval))
            self.next_run = next_run.replace(hour=self.time.hour,
                                             minute=self.time.minute,
                                             second=self.time.second,
                                             microsecond=0)

        def schedule_periodic():
            self.period = datetime.timedelta(**{self.unit: self.interval})
            self.next_run = datetime.datetime.now() + self.period

        if not self.unit in ('seconds', 'minutes', 'hours', 'days', 'weeks'):
            raise ValueError('Invalid time unit: %s' % self.unit)

        if self.time is None:
            schedule_periodic()
        else:
            schedule_at_specific_time()


default_scheduler = Scheduler()
jobs = default_scheduler.jobs


def every(interval=1):
    return default_scheduler.every(interval)


def run_pending():
    """Run all jobs that are scheduled to run.

    Please note that it is *intended behavior that run_pending()
    does not run missed jobs*. For example, if you've registered a job
    that should run every minute and you only call run_pending()
    in one hour increments then your job won't be run 60 times in
    between but only once.
    """
    default_scheduler.run_pending()


def run_all(delay=60):
    """Run all jobs regardless if they are scheduled to run or not.

    A delay of `delay` seconds is added between each job. This can help
    to distribute the system load generated by the jobs more evenly over
    time."""
    default_scheduler.run_all(delay_seconds=delay)


def clear():
    """Deletes all scheduled jobs."""
    default_scheduler.clear()

@@ -113,17 +113,17 @@
 odic tas
-t
+k
 s.%0A%0AExam
@@ -304,16 +304,93 @@
 at 10:30
+%0A    every(3).days.at('11:00').do(job) -- Run job() every three days at 11:00
 %0A%0AAdditi
@@ -1337,17 +1337,16 @@
 seconds=
-6
 0):%0A    
@@ -1905,24 +1905,67 @@
 nterval=1):%0A
+        %22%22%22Schedule a new periodic job.%22%22%22%0A
         job 
@@ -4982,24 +4982,54 @@
 %22Run the job
+ and immediately reschedule it
 .%22%22%22%0A       

Set default delay for run_all() to 0 seconds.