Merge pull request #1365 from niphlod/feature/scheduler_crontab

repeats via cronline expression
This commit is contained in:
mdipierro
2016-06-18 07:49:08 -05:00
committed by GitHub
2 changed files with 603 additions and 41 deletions
+253 -40
View File
@@ -9,9 +9,9 @@ Background processes made simple
--------------------------------- ---------------------------------
""" """
from __future__ import print_function from __future__ import print_function
from gluon._compat import Queue, long, iteritems
import os import os
import re
import time import time
import multiprocessing import multiprocessing
import sys import sys
@@ -24,13 +24,13 @@ import logging
import optparse import optparse
import tempfile import tempfile
import types import types
from functools import reduce
from json import loads, dumps from json import loads, dumps
from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_EMPTY_OR
from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid
from gluon._compat import Queue, long, iteritems
from gluon.storage import Storage from gluon.storage import Storage
from functools import reduce
USAGE = """ USAGE = """
## Example ## Example
@@ -71,23 +71,6 @@ http://127.0.0.1:8000/myapp/appadmin/select/db?query=db.scheduler_run.id>0
## view workers ## view workers
http://127.0.0.1:8000/myapp/appadmin/select/db?query=db.scheduler_worker.id>0 http://127.0.0.1:8000/myapp/appadmin/select/db?query=db.scheduler_worker.id>0
## To install the scheduler as a permanent daemon on Linux (w/ Upstart), put
## the following into /etc/init/web2py-scheduler.conf:
## (This assumes your web2py instance is installed in <user>'s home directory,
## running as <user>, with app <myapp>, on network interface eth0.)
description "web2py task scheduler"
start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown
respawn limit 8 60 # Give up if restart occurs 8 times in 60 seconds.
exec sudo -u <user> python /home/<user>/web2py/web2py.py -K <myapp>
respawn
## You can then start/stop/restart/check status of the daemon with:
sudo start web2py-scheduler
sudo stop web2py-scheduler
sudo restart web2py-scheduler
sudo status web2py-scheduler
""" """
path = os.getcwd() path = os.getcwd()
@@ -160,7 +143,7 @@ class TaskReport(object):
class JobGraph(object): class JobGraph(object):
"""Experimental: dependencies amongs tasks""" """Experimental: dependencies amongs tasks."""
def __init__(self, db, job_name): def __init__(self, db, job_name):
self.job_name = job_name or 'job_0' self.job_name = job_name or 'job_0'
@@ -216,9 +199,216 @@ class JobGraph(object):
db.rollback() db.rollback()
return None return None
class CronParser(object):
def __init__(self, cronline, base=None):
self.cronline = cronline
self.sched = base or datetime.datetime.now()
self.task = None
@staticmethod
def _rangetolist(s, period='min'):
retval = []
if s.startswith('*'):
if period == 'min':
s = s.replace('*', '0-59', 1)
elif period == 'hr':
s = s.replace('*', '0-23', 1)
elif period == 'dom':
s = s.replace('*', '1-31', 1)
elif period == 'mon':
s = s.replace('*', '1-12', 1)
elif period == 'dow':
s = s.replace('*', '0-6', 1)
m = re.compile(r'(\d+)-(\d+)/(\d+)')
match = m.match(s)
if match:
min_, max_ = int(match.group(1)), int(match.group(2)) + 1
step_ = int(match.group(3))
else:
m = re.compile(r'(\d+)/(\d+)')
ranges_max = {'min': 59, 'hr': 23, 'mon': 12, 'dom': 31, 'dow': 7}
match = m.match(s)
if match:
min_, max_ = int(match.group(1)), ranges_max[period] + 1
step_ = int(match.group(2))
if match:
for i in range(min_, max_, step_):
retval.append(i)
return retval
@staticmethod
def _sanitycheck(values, period):
if period == 'min':
check = all(0 <= i <= 59 for i in values)
elif period == 'hr':
check = all(0 <= i <= 23 for i in values)
elif period == 'dom':
check = all(1 <= i <= 31 or i == 'l' for i in values)
elif period == 'mon':
check = all(1 <= i <= 12 for i in values)
elif period == 'dow':
check = all(0 <= i <= 7 for i in values)
return check
def _parse(self):
line = self.cronline.lower()
task = {}
if line.startswith('@yearly'):
line = line.replace('@yearly', '0 0 1 1 *')
elif line.startswith('@annually'):
line = line.replace('@annually', '0 0 1 1 *')
elif line.startswith('@monthly'):
line = line.replace('@monthly', '0 0 1 * *')
elif line.startswith('@weekly'):
line = line.replace('@weekly', '0 0 * * 0')
elif line.startswith('@daily'):
line = line.replace('@daily', '0 0 * * *')
elif line.startswith('@midnight'):
line = line.replace('@midnight', '0 0 * * *')
elif line.startswith('@hourly'):
line = line.replace('@hourly', '0 * * * *')
params = line.strip().split()
if len(params) < 5:
raise ValueError('Invalid cron line (too short)')
elif len(params) > 5:
raise ValueError('Invalid cron line (too long)')
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4,
'fri': 5, 'sat': 6}
monthsofyear = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5,
'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10,
'nov': 11, 'dec': 12, 'l': 'l'}
for (s, i) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
if s not in [None, '*']:
task[i] = []
vals = s.split(',')
for val in vals:
if i == 'dow':
refdict = daysofweek
elif i == 'mon':
refdict = monthsofyear
if i in ('dow', 'mon') and '-' in val and '/' not in val:
isnum = val.split('-')[0].isdigit()
if isnum:
val = '%s/1' % val
else:
val = '-'.join([str(refdict[v])
for v in val.split('-')])
if val != '-1' and '-' in val and '/' not in val:
val = '%s/1' % val
if '/' in val:
task[i] += self._rangetolist(val, i)
elif val.isdigit() or val == '-1':
task[i].append(int(val))
elif i in ('dow', 'mon'):
if val in refdict:
task[i].append(refdict[val])
elif i == 'dom' and val == 'l':
task[i].append(val)
if not task[i]:
raise ValueError('Invalid cron value (%s)' % s)
if not self._sanitycheck(task[i], i):
raise ValueError('Invalid cron value (%s)' % s)
task[i] = sorted(task[i])
self.task = task
@staticmethod
def _get_next_dow(sched, task):
task_dow = [a % 7 for a in task['dow']]
while sched.isoweekday() % 7 not in task_dow:
sched += datetime.timedelta(days=1)
return sched
@staticmethod
def _get_next_dom(sched, task):
if task['dom'] == ['l']:
last_feb = 29 if sched.year % 4 == 0 else 28
lastdayofmonth = [
31, last_feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
]
task_dom = [lastdayofmonth[sched.month - 1]]
else:
task_dom = task['dom']
while sched.day not in task_dom:
sched += datetime.timedelta(days=1)
return sched
@staticmethod
def _get_next_mon(sched, task):
while sched.month not in task['mon']:
if sched.month < 12:
sched = sched.replace(month=sched.month + 1)
else:
sched = sched.replace(month=1, year=sched.year + 1)
return sched
@staticmethod
def _getnext_hhmm(sched, task, add_to=True):
if add_to:
sched += datetime.timedelta(minutes=1)
if 'min' in task:
while sched.minute not in task['min']:
sched += datetime.timedelta(minutes=1)
if 'hr' in task and sched.hour not in task['hr']:
while sched.hour not in task['hr']:
sched += datetime.timedelta(hours=1)
return sched
def _getnext_date(self, sched, task):
if 'dow' in task and 'dom' in task:
dow = self._get_next_dow(sched, task)
dom = self._get_next_dom(sched, task)
sched = min(dow, dom)
elif 'dow' in task:
sched = self._get_next_dow(sched, task)
elif 'dom' in task:
sched = self._get_next_dom(sched, task)
if 'mon' in task:
sched = self._get_next_mon(sched, task)
return sched.replace(hour=0, minute=0)
def get_next(self):
"""Get next date according to specs."""
if not self.task:
self._parse()
task = self.task
sched = self.sched
x = 0
while x < 1000: # avoid potential max recursions
x += 1
try:
next_date = self._getnext_date(sched, task)
except (ValueError, OverflowError) as e:
raise ValueError('Invalid cron expression (%s)' % e)
if next_date.date() > self.sched.date():
# we rolled date, check for valid hhmm
sched = self._getnext_hhmm(next_date, task, False)
break
else:
# same date, get next hhmm
sched_time = self._getnext_hhmm(sched, task, True)
if sched_time.date() > sched.date():
# we rolled date again :(
sched = sched_time
else:
sched = sched_time
break
else:
raise ValueError('Potential bug found, please submit your '
'cron expression to the authors')
self.sched = sched
return sched
def __iter__(self):
"""Support iteration."""
return self
__next__ = next = get_next
# the two functions below deal with simplejson decoding as unicode, esp for the dict decode # the two functions below deal with simplejson decoding as unicode, esp for the dict decode
# and subsequent usage as function Keyword arguments unicode variable names won't work! # and subsequent usage as function Keyword arguments unicode variable names won't work!
# borrowed from http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python # borrowed from http://stackoverflow.com/questions/956867/
def _decode_list(lst): def _decode_list(lst):
@@ -321,7 +511,7 @@ def executor(queue, task, out):
class MetaScheduler(threading.Thread): class MetaScheduler(threading.Thread):
"""Base class documenting scheduler's base methods""" """Base class documenting scheduler's base methods."""
def __init__(self): def __init__(self):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@@ -377,7 +567,7 @@ class MetaScheduler(threading.Thread):
while not out.empty(): while not out.empty():
tout += out.get() tout += out.get()
if tout: if tout:
logger.debug(' partial output: "%s"' % str(tout)) logger.debug(' partial output: "%s"', str(tout))
if CLEAROUT in tout: if CLEAROUT in tout:
task_output = tout[ task_output = tout[
tout.rfind(CLEAROUT) + len(CLEAROUT):] tout.rfind(CLEAROUT) + len(CLEAROUT):]
@@ -428,7 +618,7 @@ class MetaScheduler(threading.Thread):
self.have_heartbeat = False self.have_heartbeat = False
def terminate_process(self): def terminate_process(self):
"""Terminates any running tasks (internal use only)""" """Terminate any running tasks (internal use only)"""
try: try:
self.process.terminate() self.process.terminate()
except: except:
@@ -468,6 +658,22 @@ TASK_STATUS = (QUEUED, RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED, EXPIRED)
RUN_STATUS = (RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED) RUN_STATUS = (RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED)
WORKER_STATUS = (ACTIVE, PICK, DISABLED, TERMINATE, KILL, STOP_TASK) WORKER_STATUS = (ACTIVE, PICK, DISABLED, TERMINATE, KILL, STOP_TASK)
class IS_CRONLINE(object):
"""
Validates cronline
"""
def __init__(self, error_message=None):
self.error_message = error_message
def __call__(self, value):
recur = CronParser(value, datetime.datetime.now())
try:
recur.get_next()
return (value, None)
except (KeyError, ValueError) as e:
if not self.error_message:
return (value, e)
return (value, self.error_message)
class TYPE(object): class TYPE(object):
""" """
@@ -612,7 +818,10 @@ class Scheduler(MetaScheduler):
Field('period', 'integer', default=60, comment='seconds', Field('period', 'integer', default=60, comment='seconds',
requires=IS_INT_IN_RANGE(0, None)), requires=IS_INT_IN_RANGE(0, None)),
Field('prevent_drift', 'boolean', default=False, Field('prevent_drift', 'boolean', default=False,
comment='Cron-like start_times between runs'), comment='Exact start_times between runs'),
Field('cronline', default=None,
comment='Discard "period", use this cron expr instead',
requires=IS_EMPTY_OR(IS_CRONLINE())),
Field('timeout', 'integer', default=60, comment='seconds', Field('timeout', 'integer', default=60, comment='seconds',
requires=IS_INT_IN_RANGE(1, None)), requires=IS_INT_IN_RANGE(1, None)),
Field('sync_output', 'integer', default=0, Field('sync_output', 'integer', default=0,
@@ -665,14 +874,6 @@ class Scheduler(MetaScheduler):
if migrate is not False: if migrate is not False:
db.commit() db.commit()
@staticmethod
def total_seconds(td):
"""Backport for py2.6."""
if hasattr(td, 'total_seconds'):
return td.total_seconds()
else:
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10.0 ** 6
def loop(self, worker_name=None): def loop(self, worker_name=None):
"""Main loop. """Main loop.
@@ -694,7 +895,7 @@ class Scheduler(MetaScheduler):
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
try: try:
self.start_heartbeats() self.start_heartbeats()
while True and self.have_heartbeat: while self.have_heartbeat:
if self.w_stats.status == DISABLED: if self.w_stats.status == DISABLED:
logger.debug('Someone stopped me, sleeping until better' logger.debug('Someone stopped me, sleeping until better'
' times come (%s)', self.w_stats.sleep) ' times come (%s)', self.w_stats.sleep)
@@ -798,7 +999,10 @@ class Scheduler(MetaScheduler):
logger.info('nothing to do') logger.info('nothing to do')
return None return None
times_run = task.times_run + 1 times_run = task.times_run + 1
if not task.prevent_drift: if task.cronline:
cron_recur = CronParser(task.cronline, now.replace(second=0))
next_run_time = cron_recur.get_next()
elif not task.prevent_drift:
next_run_time = task.last_run_time + datetime.timedelta( next_run_time = task.last_run_time + datetime.timedelta(
seconds=task.period seconds=task.period
) )
@@ -806,7 +1010,7 @@ class Scheduler(MetaScheduler):
# calc next_run_time based on available slots # calc next_run_time based on available slots
# see #1191 # see #1191
next_run_time = task.start_time next_run_time = task.start_time
secondspassed = self.total_seconds(now - next_run_time) secondspassed = (now - next_run_time).total_seconds()
steps = secondspassed // task.period + 1 steps = secondspassed // task.period + 1
next_run_time += datetime.timedelta(seconds=task.period * steps) next_run_time += datetime.timedelta(seconds=task.period * steps)
@@ -1275,13 +1479,22 @@ class Scheduler(MetaScheduler):
tuuid = 'uuid' in kwargs and kwargs.pop('uuid') or web2py_uuid() tuuid = 'uuid' in kwargs and kwargs.pop('uuid') or web2py_uuid()
tname = 'task_name' in kwargs and kwargs.pop('task_name') or function tname = 'task_name' in kwargs and kwargs.pop('task_name') or function
immediate = 'immediate' in kwargs and kwargs.pop('immediate') or None immediate = 'immediate' in kwargs and kwargs.pop('immediate') or None
rtn = self.db.scheduler_task.validate_and_insert( cronline = kwargs.get('cronline')
kwargs.update(
function_name=function, function_name=function,
task_name=tname, task_name=tname,
args=targs, args=targs,
vars=tvars, vars=tvars,
uuid=tuuid, uuid=tuuid,
**kwargs) )
if cronline:
try:
start_time = kwargs.get('start_time', self.now)
next_run_time = CronParser(cronline, start_time).get_next()
kwargs.update(start_time=start_time, next_run_time=next_run_time)
except:
pass
rtn = self.db.scheduler_task.validate_and_insert(**kwargs)
if not rtn.errors: if not rtn.errors:
rtn.uuid = tuuid rtn.uuid = tuuid
if immediate: if immediate:
+350 -1
View File
@@ -15,7 +15,7 @@ fix_sys_path(__file__)
from gluon.storage import Storage from gluon.storage import Storage
from gluon.languages import translator from gluon.languages import translator
from gluon.scheduler import JobGraph, Scheduler from gluon.scheduler import JobGraph, Scheduler, CronParser
from gluon.dal import DAL from gluon.dal import DAL
@@ -51,6 +51,355 @@ class BaseTestScheduler(unittest.TestCase):
pass pass
class CronParserTest(unittest.TestCase):
def testMinute(self):
# minute asterisk
base = datetime.datetime(2010, 1, 23, 12, 18)
itr = CronParser('*/1 * * * *', base)
n1 = itr.get_next() # 19
self.assertEqual(base.year, n1.year)
self.assertEqual(base.month, n1.month)
self.assertEqual(base.day, n1.day)
self.assertEqual(base.hour, n1.hour)
self.assertEqual(base.minute, n1.minute - 1)
for i in range(39): # ~ 58
itr.get_next()
n2 = itr.get_next()
self.assertEqual(n2.minute, 59)
n3 = itr.get_next()
self.assertEqual(n3.minute, 0)
self.assertEqual(n3.hour, 13)
itr = CronParser('*/5 * * * *', base)
n4 = itr.get_next()
self.assertEqual(n4.minute, 20)
for i in range(6):
itr.get_next()
n5 = itr.get_next()
self.assertEqual(n5.minute, 55)
n6 = itr.get_next()
self.assertEqual(n6.minute, 0)
self.assertEqual(n6.hour, 13)
base = datetime.datetime(2010, 1, 23, 12, 18)
itr = CronParser('4/34 * * * *', base)
n7 = itr.get_next()
self.assertEqual(n7.minute, 38)
self.assertEqual(n7.hour, 12)
n8 = itr.get_next()
self.assertEqual(n8.minute, 4)
self.assertEqual(n8.hour, 13)
def testHour(self):
base = datetime.datetime(2010, 1, 24, 12, 2)
itr = CronParser('0 */3 * * *', base)
n1 = itr.get_next()
self.assertEqual(n1.hour, 15)
self.assertEqual(n1.minute, 0)
for i in range(2):
itr.get_next()
n2 = itr.get_next()
self.assertEqual(n2.hour, 0)
self.assertEqual(n2.day, 25)
def testDay(self):
base = datetime.datetime(2010, 2, 24, 12, 9)
itr = CronParser('0 0 */3 * *', base)
n1 = itr.get_next()
# 1 4 7 10 13 16 19 22 25 28
self.assertEqual(n1.day, 25)
n2 = itr.get_next()
self.assertEqual(n2.day, 28)
n3 = itr.get_next()
self.assertEqual(n3.day, 1)
self.assertEqual(n3.month, 3)
# test leap year
base = datetime.datetime(1996, 2, 27)
itr = CronParser('0 0 * * *', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 28)
self.assertEqual(n1.month, 2)
n2 = itr.get_next()
self.assertEqual(n2.day, 29)
self.assertEqual(n2.month, 2)
base2 = datetime.datetime(2000, 2, 27)
itr2 = CronParser('0 0 * * *', base2)
n3 = itr2.get_next()
self.assertEqual(n3.day, 28)
self.assertEqual(n3.month, 2)
n4 = itr2.get_next()
self.assertEqual(n4.day, 29)
self.assertEqual(n4.month, 2)
def testWeekDay(self):
base = datetime.datetime(2010, 2, 25)
itr = CronParser('0 0 * * sat', base)
n1 = itr.get_next()
self.assertEqual(n1.isoweekday(), 6)
self.assertEqual(n1.day, 27)
n2 = itr.get_next()
self.assertEqual(n2.isoweekday(), 6)
self.assertEqual(n2.day, 6)
self.assertEqual(n2.month, 3)
base = datetime.datetime(2010, 1, 25)
itr = CronParser('0 0 1 * wed', base)
n1 = itr.get_next()
self.assertEqual(n1.month, 1)
self.assertEqual(n1.day, 27)
self.assertEqual(n1.year, 2010)
n2 = itr.get_next()
self.assertEqual(n2.month, 2)
self.assertEqual(n2.day, 1)
self.assertEqual(n2.year, 2010)
n3 = itr.get_next()
self.assertEqual(n3.month, 2)
self.assertEqual(n3.day, 3)
self.assertEqual(n3.year, 2010)
def testMonth(self):
base = datetime.datetime(2010, 1, 25)
itr = CronParser('0 0 1 * *', base)
n1 = itr.get_next()
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 1)
n2 = itr.get_next()
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 1)
for i in range(8):
itr.get_next()
n3 = itr.get_next()
self.assertEqual(n3.month, 12)
self.assertEqual(n3.year, 2010)
n4 = itr.get_next()
self.assertEqual(n4.month, 1)
self.assertEqual(n4.year, 2011)
base = datetime.datetime(2010, 1, 25)
itr = CronParser('0 0 1 */4 *', base)
n1 = itr.get_next()
self.assertEqual(n1.month, 5)
self.assertEqual(n1.day, 1)
base = datetime.datetime(2010, 1, 25)
itr = CronParser('0 0 1 1-3 *', base)
n1 = itr.get_next()
self.assertEqual(n1.month, 2)
self.assertEqual(n1.day, 1)
n2 = itr.get_next()
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 1)
n3 = itr.get_next()
self.assertEqual(n3.month, 1)
self.assertEqual(n3.day, 1)
def testSundayToThursdayWithAlphaConversion(self):
base = datetime.datetime(2010, 8, 25, 15, 56)
itr = CronParser("30 22 * * sun-thu", base)
n1 = itr.get_next()
self.assertEqual(base.year, n1.year)
self.assertEqual(base.month, n1.month)
self.assertEqual(base.day, n1.day)
self.assertEqual(22, n1.hour)
self.assertEqual(30, n1.minute)
def testISOWeekday(self):
base = datetime.datetime(2010, 2, 25)
itr = CronParser('0 0 * * 7', base)
n1 = itr.get_next()
self.assertEqual(n1.isoweekday(), 7)
self.assertEqual(n1.day, 28)
n2 = itr.get_next()
self.assertEqual(n2.isoweekday(), 7)
self.assertEqual(n2.day, 7)
self.assertEqual(n2.month, 3)
base = datetime.datetime(2010, 2, 22)
itr = CronParser('0 0 * * */2', base)
n1 = itr.get_next()
self.assertEqual(n1.isoweekday(), 2)
self.assertEqual(n1.day, 23)
n2 = itr.get_next()
self.assertEqual(n2.isoweekday(), 4)
self.assertEqual(n2.day, 25)
def testBug2(self):
base = datetime.datetime(2012, 1, 1, 0, 0)
itr = CronParser('0 * * 3 *', base)
n1 = itr.get_next()
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, base.day)
self.assertEqual(n1.hour, base.hour)
self.assertEqual(n1.minute, base.minute)
n2 = itr.get_next()
self.assertEqual(n2.year, base.year)
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, base.day)
self.assertEqual(n2.hour, base.hour + 1)
self.assertEqual(n2.minute, base.minute)
n3 = itr.get_next()
self.assertEqual(n3.year, base.year)
self.assertEqual(n3.month, 3)
self.assertEqual(n3.day, base.day)
self.assertEqual(n3.hour, base.hour + 2)
self.assertEqual(n3.minute, base.minute)
def testBug3(self):
base = datetime.datetime(2013, 3, 1, 12, 17, 34, 257877)
c = CronParser('00 03 16,30 * *', base)
n1 = c.get_next()
self.assertEqual(n1.month, 3)
self.assertEqual(n1.day, 16)
n2 = c.get_next()
self.assertEqual(n2.month, 3)
self.assertEqual(n2.day, 30)
n3 = c.get_next()
self.assertEqual(n3.month, 4)
self.assertEqual(n3.day, 16)
def test_rangeGenerator(self):
base = datetime.datetime(2013, 3, 4, 0, 0)
itr = CronParser('1-9/2 0 1 * *', base)
n1 = itr.get_next()
n2 = itr.get_next()
n3 = itr.get_next()
n4 = itr.get_next()
n5 = itr.get_next()
self.assertEqual(n1.minute, 1)
self.assertEqual(n2.minute, 3)
self.assertEqual(n3.minute, 5)
self.assertEqual(n4.minute, 7)
self.assertEqual(n5.minute, 9)
def test_iterGenerator(self):
base = datetime.datetime(2013, 3, 4, 0, 0)
itr = CronParser('1-9/2 0 1 * *', base)
x = 0
for n in itr:
x += 1
if x > 4:
break
self.assertEqual(n.minute, 9)
def test_invalidcron(self):
base = datetime.datetime(2013, 3, 4, 0, 0)
itr = CronParser('5 4 31 2 *', base)
self.assertRaises(ValueError, itr.get_next)
itr = CronParser('* * 5-1 * *', base)
self.assertRaises(ValueError, itr.get_next)
itr = CronParser('* * * janu-jun *', base)
self.assertRaises(KeyError, itr.get_next)
itr = CronParser('* * * * * *', base)
self.assertRaises(ValueError, itr.get_next)
itr = CronParser('* * * *', base)
self.assertRaises(ValueError, itr.get_next)
def testLastDayOfMonth(self):
base = datetime.datetime(2015, 9, 4)
itr = CronParser('0 0 L * *', base)
n1 = itr.get_next()
self.assertEqual(n1.month, 9)
self.assertEqual(n1.day, 30)
n2 = itr.get_next()
self.assertEqual(n2.month, 10)
self.assertEqual(n2.day, 31)
n3 = itr.get_next()
self.assertEqual(n3.month, 11)
self.assertEqual(n3.day, 30)
n4 = itr.get_next()
self.assertEqual(n4.month, 12)
self.assertEqual(n4.day, 31)
base = datetime.datetime(1996, 2, 27)
itr = CronParser('0 0 L * *', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 29)
self.assertEqual(n1.month, 2)
n2 = itr.get_next()
self.assertEqual(n2.day, 31)
self.assertEqual(n2.month, 3)
def testSpecialExpr(self):
base = datetime.datetime(2000, 1, 1)
itr = CronParser('@yearly', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 1)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.year, base.year + 1)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
itr = CronParser('@annually', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 1)
self.assertEqual(n1.month, 1)
self.assertEqual(n1.year, base.year + 1)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
itr = CronParser('@monthly', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 1)
self.assertEqual(n1.month, base.month + 1)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
itr = CronParser('@weekly', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 2)
self.assertEqual(n1.month, base.month)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
n2 = itr.get_next()
self.assertEqual(n2.day, 9)
self.assertEqual(n2.month, base.month)
self.assertEqual(n2.year, base.year)
self.assertEqual(n2.hour, 0)
self.assertEqual(n2.minute, 0)
n3 = itr.get_next()
self.assertEqual(n3.day, 16)
self.assertEqual(n3.month, base.month)
self.assertEqual(n3.year, base.year)
self.assertEqual(n3.hour, 0)
self.assertEqual(n3.minute, 0)
itr = CronParser('@daily', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 2)
self.assertEqual(n1.month, base.month)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
itr = CronParser('@midnight', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 2)
self.assertEqual(n1.month, base.month)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.hour, 0)
self.assertEqual(n1.minute, 0)
itr = CronParser('@hourly', base)
n1 = itr.get_next()
self.assertEqual(n1.day, 1)
self.assertEqual(n1.month, base.month)
self.assertEqual(n1.year, base.year)
self.assertEqual(n1.hour, 1)
self.assertEqual(n1.minute, 0)
class TestsForJobGraph(BaseTestScheduler): class TestsForJobGraph(BaseTestScheduler):
def testJobGraph(self): def testJobGraph(self):