repeats via cronline expression
This commit is contained in:
+253
-40
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user