Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ Thanks to all the wonderful folks who have contributed to schedule over the year
- schnepp <https://github.com/schnepp> <https://bitbucket.org/saschaschnepp>
- grampajoe <https://github.com/grampajoe>
- gilbsgilbs <https://github.com/gilbsgilbs>
- ljanyst <https://github.com/ljanyst>
- antwal <https://github.com/antwal> <https://bitbucket.org/antwal>

50 changes: 49 additions & 1 deletion FAQ.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,52 @@ How can I pass arguments to the job function?
print('Hello', name)

schedule.every(2).seconds.do(greet, name='Alice')
schedule.every(4).seconds.do(greet, name='Bob')
schedule.every(4).seconds.do(greet, name='Bob')

How to run a job with decorator?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

from schedule import every, repeat
import schedule
import time

@repeat(every(1).minutes)
def job_a():
print("Called function job_a()")

@repeat(every(15).seconds)
def job_b():
print("Called function job_b()")

while True:
schedule.run_pending()
time.sleep(1)

How to run a job with string interval definitions?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

import threading
from schedule import when
import schedule
import time

def run_threaded(job_func):
job_thread = threading.Thread(target=job_func)
job_thread.start()

def job_a():
print("Called function job_a()")

def job_b():
print("Called function job_b()")

schedule.when('every 15 seconds').do(run_threaded, job_a)
schedule.when('every 1 minute').do(run_threaded, job_b)

while True:
schedule.run_pending()
time.sleep(1)
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ Usage

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every().day.at("10:30:00").do(job)
schedule.every(5).to(10).minutes.do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().wednesday.at("13:15:20").do(job)

while True:
schedule.run_pending()
Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Main Interface
.. autodata:: jobs

.. autofunction:: every
.. autofunction:: when
.. autofunction:: run_pending
.. autofunction:: run_all
.. autofunction:: clear
Expand Down
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ Usage

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every().day.at("10:30:00").do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().wednesday.at("13:15:20").do(job)

while True:
schedule.run_pending()
Expand Down
164 changes: 154 additions & 10 deletions schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
import random
import time

try:
from datetime import timezone
utc = timezone.utc
except ImportError:
from schedule.timezone import UTC
utc = UTC()


logger = logging.getLogger('schedule')


Expand Down Expand Up @@ -127,6 +135,21 @@ def every(self, interval=1):
job = Job(interval, self)
return job

def when(self, interval_definition):
"""
Schedule a new periodic job.

:param interval_definition: See :meth:`Job.when <Job.when>`.
:return: A partially configured :class:`Job <Job>`
"""
job = Job(1, self)
try:
job.when(interval_definition)
except Exception:
self.cancel_job(job)
raise
return job

def _run_job(self, job):
ret = job.run()
if isinstance(ret, CancelJob) or ret is CancelJob:
Expand All @@ -149,7 +172,7 @@ def idle_seconds(self):
:return: Number of seconds until
:meth:`next_run <Scheduler.next_run>`.
"""
return (self.next_run - datetime.datetime.now()).total_seconds()
return (self.next_run - datetime.datetime.now(utc)).total_seconds()


class Job(object):
Expand Down Expand Up @@ -181,6 +204,7 @@ def __init__(self, interval, scheduler=None):
self.start_day = None # Specific day of the week to start on
self.tags = set() # unique set of tags for the job
self.scheduler = scheduler # scheduler to register with
self.directive_map = None # map of directive names to function calls

def __lt__(self, other):
"""
Expand All @@ -191,7 +215,7 @@ def __lt__(self, other):

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

timestats = '(last run: %s, next run: %s)' % (
format_time(self.last_run), format_time(self.next_run))
Expand Down Expand Up @@ -338,19 +362,25 @@ def at(self, time_str):
Calling this is only valid for jobs scheduled to run
every N day(s).

:param time_str: A string in `XX:YY` format.
:param time_str: A string in `HH:MM` or `HH:MM:SS` format.
:return: The invoked job instance
"""
assert self.unit in ('days', 'hours') or self.start_day
hour, minute = time_str.split(':')
parts = time_str.split(':')
assert 1 < len(parts) < 4, '`XX:YY` or `XX:YY:ZZ` format expected'
if len(parts) == 2:
parts.append(0)
hour, minute, second = parts
minute = int(minute)
second = int(second)
if self.unit == 'days' or self.start_day:
hour = int(hour)
assert 0 <= hour <= 23
elif self.unit == 'hours':
hour = 0
assert 0 <= minute <= 59
self.at_time = datetime.time(hour, minute)
assert 0 <= second <= 59
self.at_time = datetime.time(hour, minute, second)
return self

def to(self, latest):
Expand Down Expand Up @@ -391,12 +421,100 @@ def do(self, job_func, *args, **kwargs):
self.scheduler.jobs.append(self)
return self

def _build_directive_map(self):
"""
Build a map of directive names to function calls for the parser
"""
if self.directive_map is not None:
return

# a list of valid directives
directive_names = ['second', 'seconds', 'minute', 'minutes', 'hour',
'hours', 'day', 'days', 'week', 'weeks', 'monday',
'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'sunday', 'at', 'to']

# get an appropriate setter reference
def get_attr(obj, attr):
for obj in [obj] + obj.__class__.mro():
if attr in obj.__dict__:
ret = obj.__dict__[attr]
if isinstance(ret, property):
return lambda x: ret.__get__(x, type(x))
return ret

# build the dictionary of properties
self.directive_map = {}
for d in directive_names:
self.directive_map[d] = get_attr(self, d)

def when(self, interval_definition):
"""
Schedule a job according to an interval definition. The definition
is a string that is the same as a sequence of method calls, except
that dots and parentheses are replaced with spaces. For example:
`when('every monday at 17:51')`.

:param interval_definition: the interval definition
:return: The invoked job instance
"""
directives = interval_definition.lower().split()
assert len(directives) >= 2, 'definition too short'
assert directives[0] == 'every', \
'the definition must start with "every"'

# set up the interval if necessary
try:
interval = int(directives[1])
self.interval = interval
assert len(directives) >= 3, "definition to short"
directives = directives[2:]
except ValueError:
directives = directives[1:]

# parse the definition
self._build_directive_map()
directives.reverse()
while directives:
directive = directives.pop()
assert directive in self.directive_map, \
'unknown directive: '+directive

args = []

# check the argument to "to"
if directive == 'to':
arg = directives.pop()
try:
arg = int(arg)
except ValueError:
assert False, 'the "to" directive expects an integer'
args.append(arg)

# check the argument to "at"
elif directive == 'at':
arg = directives.pop()
arg_split = arg.split(':')
assert len(arg_split) == 2, \
'the "at" directive expects a string like "12:34"'
try:
int(arg_split[0])
int(arg_split[1])
except ValueError:
assert False, \
'the "at" directive expects a string like "12:34"'
args.append(arg)

# call the setter function
self.directive_map[directive](self, *args)
return self

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

def run(self):
"""
Expand All @@ -406,7 +524,7 @@ def run(self):
"""
logger.info('Running job %s', self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
self.last_run = datetime.datetime.now(utc)
self._schedule_next_run()
return ret

Expand All @@ -423,7 +541,7 @@ def _schedule_next_run(self):
interval = self.interval

self.period = datetime.timedelta(**{self.unit: interval})
self.next_run = datetime.datetime.now() + self.period
self.next_run = datetime.datetime.now(utc) + self.period
if self.start_day is not None:
assert self.unit == 'weeks'
weekdays = (
Expand Down Expand Up @@ -454,15 +572,15 @@ def _schedule_next_run(self):
# If we are running for the first time, make sure we run
# at the specified time *today* (or *this hour*) as well
if not self.last_run:
now = datetime.datetime.now()
now = datetime.datetime.now(utc)
if (self.unit == 'days' and self.at_time > now.time() and
self.interval == 1):
self.next_run = self.next_run - datetime.timedelta(days=1)
elif self.unit == 'hours' and self.at_time.minute > now.minute:
self.next_run = self.next_run - datetime.timedelta(hours=1)
if self.start_day is not None and self.at_time is not None:
# Let's see if we will still make that time we specified today
if (self.next_run - datetime.datetime.now()).days >= 7:
if (self.next_run - datetime.datetime.now(utc)).days >= 7:
self.next_run -= self.period


Expand All @@ -483,6 +601,13 @@ def every(interval=1):
return default_scheduler.every(interval)


def when(interval_definition):
"""Calls :meth:`when <Scheduler.when>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.when(interval_definition)


def run_pending():
"""Calls :meth:`run_pending <Scheduler.run_pending>` on the
:data:`default scheduler instance <default_scheduler>`.
Expand Down Expand Up @@ -523,3 +648,22 @@ def idle_seconds():
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.idle_seconds


def repeat(job):
"""Decorator for scheduled functions/methods. The decorated
functions/methods should not accept any arguments.
Usage:
>>> from schedule import every, repeat
>>> import schedule
>>> import time
>>> @repeat(every(10).minutes)
>>> def job():
>>> print("I am a scheduled job")
>>> while True:
>>> schedule.run_pending()
>>> time.sleep(1)"""
def _inner_decorator(decorated_function):
job.do(decorated_function)
return decorated_function
return _inner_decorator
19 changes: 19 additions & 0 deletions schedule/timezone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import datetime


class UTC(datetime.tzinfo):
"""tzinfo derived concrete class named "UTC" with offset of 0"""
# can be changed to another timezone name/offset
def __init__(self):
self.__offset = datetime.timedelta(seconds=0)
self.__dst = datetime.timedelta(0)
self.__name = "UTC"

def utcoffset(self, dt):
return self.__offset

def dst(self, dt):
return self.__dst

def tzname(self, dt):
return self.__name
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from setuptools import setup


SCHEDULE_VERSION = '0.5.0'
SCHEDULE_VERSION = '0.5.1'
SCHEDULE_DOWNLOAD_URL = (
'https://github.com/dbader/schedule/tarball/' + SCHEDULE_VERSION
)
Expand Down
Loading