123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- # -*- coding: utf-8 -*-
- """
- plan.job
- ~~~~~~~~
- Job classes for Plan.
- :copyright: (c) 2014 by Shipeng Feng.
- :license: BSD, see LICENSE for more details.
- """
- import sys
- import re
- import collections
- from .output import Output
- from .exceptions import ParseError, ValidationError
- from ._compat import iteritems
- # Time types
- MINUTE = "minute"
- HOUR = "hour"
- DAY = "day of month"
- MONTH = "month"
- WEEK = "day of week"
- MONTH_MAP = {
- "jan": "1",
- "feb": "2",
- "mar": "3",
- "apr": "4",
- "may": "5",
- "jun": "6",
- "jul": "7",
- "aug": "8",
- "sep": "9",
- "oct": "10",
- "nov": "11",
- "dec": "12"
- }
- WEEK_MAP = {
- "sun": "0",
- "mon": "1",
- "tue": "2",
- "wed": "3",
- "thu": "4",
- "fri": "5",
- "sat": "6"
- }
- CRON_TIME_SYNTAX_RE = re.compile(r"^.+\s+.+\s+.+\s+.+\s+.+$")
- PREDEFINED_DEFINITIONS = set(["yearly", "annually", "monthly", "weekly",
- "daily", "hourly", "reboot"])
- def is_month(time):
- """Tell whether time is one of the month names.
- :param time: a string of time.
- """
- return time[:3].lower() in MONTH_MAP
- def is_week(time):
- """Tell whether time is one of the days of week.
- :param time: a string of time.
- """
- return time[:3].lower() in WEEK_MAP or \
- time.lower() in ('weekday', 'weekend')
- def get_frequency(every):
- """Get frequency value from one every type value:
- >>> get_frequency('3.day')
- 3
- :param every: One every type value.
- """
- return int(every[:every.find('.')])
- def get_moment(at):
- """Get moment value from one at type value:
- >>> get_moment('minute.1')
- 1
- :param at: One at type value.
- """
- return int(at[at.find('.') + 1:])
- class Job(object):
- """The plan job base class.
- :param task: this is what the job does.
- :param every: how often does the job run.
- :param at: when does the job run.
- :param path: the path you want to run the task on,
- default to be current working directory.
- :param environment: the environment you want to run the task under.
- :param output: the output redirection for the task.
- """
- def __init__(self, task, every, at=None, path=None,
- environment=None, output=None):
- self.task = task
- self.every = every
- self.at = at
- self.path = path
- self.environment = environment
- self.output = str(Output(output))
- @property
- def env(self):
- if not self.environment:
- return ''
- kv_pairs = []
- for k, v in iteritems(self.environment):
- kv_pairs.append('='.join((k, v)))
- return ' '.join(kv_pairs)
- @property
- def main_template(self):
- """The main job template.
- """
- return "{task}"
- def task_template(self):
- """The task template. You should implement this in your own job type.
- The default template is::
- 'cd {path} && {environment} {task} {output}'
- """
- return 'cd {path} && {environment} {task} {output}'
- def process_template(self, template):
- """Process template content. Drop multiple spaces in a row and strip
- it.
- """
- template = re.sub(r'\s+', r' ', template)
- template = template.strip()
- return template
- def produce_frequency_time(self, frequency, maximum, start=0):
- """Translate frequency into comma separated times.
- """
- # how many units one time type have
- length = maximum - start + 1
- # if every is the same with unit length
- if frequency == length:
- return str(start)
- # else if every is one unit, we use '*'
- elif frequency == 1:
- return '*'
- # otherwise, we make steps comma separated
- else:
- times = list(range(start, maximum + 1, frequency))
- if length % frequency:
- del times[0]
- times = map(str, times)
- return ','.join(times)
- def parse_month(self, month):
- """Parses month into month numbers. Month can only occur in
- every value.
- :param month: this parameter can be the following values:
- jan feb mar apr may jun jul aug sep oct nov dec
- and all of those full month names(case insenstive)
- or <int:n>.month
- """
- if '.' in month:
- frequency = get_frequency(month)
- return self.produce_frequency_time(frequency, 12, 1)
- else:
- month = month[:3].lower()
- return MONTH_MAP[month]
- def parse_week(self, week):
- """Parses day of week name into week numbers.
- :param week: this parameter can be the following values:
- sun mon tue wed thu fri sat
- sunday monday tuesday wednesday thursday friday
- saturday
- weekday weedend(case insenstive)
- """
- if week.lower() == "weekday":
- return "1,2,3,4,5"
- elif week.lower() == "weekend":
- return "6,0"
- else:
- week = week[:3].lower()
- return WEEK_MAP[week]
- def parse_every(self):
- """Parse every value.
- :return: every_type.
- """
- every = self.every
- if '.minute' in every:
- every_type, frequency = MINUTE, get_frequency(every)
- if frequency not in range(1, 61):
- raise ParseError("Your every value %s is invalid, out of"
- " minute range[1-60]" % every)
- elif '.hour' in every:
- every_type, frequency = HOUR, get_frequency(every)
- if frequency not in range(1, 25):
- raise ParseError("Your every value %s is invalid, out of"
- " hour range[1-24]" % every)
- elif '.day' in every:
- every_type, frequency = DAY, get_frequency(every)
- if frequency not in range(1, 32):
- raise ParseError("Your every value %s is invalid, out of"
- " month day range[1-31]" % every)
- elif '.month' in every or is_month(every):
- every_type = MONTH
- if '.' in every:
- frequency = get_frequency(every)
- if frequency not in range(1, 13):
- raise ParseError("Your every value %s is invalid, out of"
- " month range[1-12]" % every)
- elif '.year' in every:
- every_type, frequency = MONTH, get_frequency(every)
- if frequency not in range(1, 2):
- raise ParseError("Your every value %s is invalid, out of"
- " year range[1]" % every)
- # Just handle months internally
- self.every = "12.months"
- elif is_week(every):
- every_type = WEEK
- else:
- raise ParseError("Your every value %s is invalid" % every)
- return every_type
- def preprocess_at(self, at):
- """Do preprocess for at value, just modify "12:12" style moment into
- "hour.12 minute.12" style moment value.
- :param at: The at value you want to do preprocess.
- """
- ats = at.split(' ')
- processed_ats = []
- for at in ats:
- if ':' in at:
- hour, minute = at.split(':')[:2]
- if minute.startswith('0') and len(minute) >= 2:
- minute = minute[1]
- hour = 'hour.' + hour
- minute = 'minute.' + minute
- processed_ats.append(hour)
- processed_ats.append(minute)
- else:
- processed_ats.append(at)
- return ' '.join(processed_ats)
- def parse_at(self):
- """Parse at value into (at_type, moment) pairs.
- """
- pairs = dict()
- if not self.at:
- return pairs
- processed_at = self.preprocess_at(self.at)
- ats = processed_at.split(' ')
- at_map = collections.defaultdict(list)
- # Parse at value into (at_type, moments_list) pairs.
- # One same at_type can have multiple moments like:
- # at = "minute.5 minute.10 hour.2"
- for at in ats:
- if 'minute.' in at:
- at_type, moment = MINUTE, get_moment(at)
- if moment not in range(60):
- raise ParseError("Your at value %s is invalid"
- " out of minute range[0-59]" % self.at)
- elif 'hour.' in at:
- at_type, moment = HOUR, get_moment(at)
- if moment not in range(24):
- raise ParseError("Your at value %s is invalid"
- " out of hour range[0-23]" % self.at)
- elif 'day.' in at:
- at_type, moment = DAY, get_moment(at)
- if moment not in range(1, 32):
- raise ParseError("Your at value %s is invalid"
- " out of month day range[1-31]" % self.at)
- elif 'month.' in at or 'year.' in at:
- raise ParseError("Your at value %s is invalid"
- " can not set month or year" % self.at)
- elif is_week(at):
- at_type = WEEK
- moment = self.parse_week(at)
- else:
- raise ParseError("Your at value %s is invalid" % self.at)
- if moment not in at_map[at_type]:
- at_map[at_type].append(moment)
- # comma seperate same at_type moments
- for at_type, moments in iteritems(at_map):
- moments = map(str, moments)
- pairs[at_type] = ','.join(moments)
- return pairs
- def validate_time(self):
- """Validate every and at value.
- every can be::
- [1-60].minute [1-24].hour [1-31].day
- [1-12].month [1].year
- jan feb mar apr may jun jul aug sep oct nov dec
- sun mon tue wed thu fri sat weekday weekend
- or any fullname of month names and day of week names
- (case insensitive)
- at::
- when every is minute, can not be set
- when every is hour, can be minute.[0-59]
- when every is day of month, can be minute.[0-59], hour.[0-23]
- when every is month, can be day.[1-31], day of week,
- minute.[0-59], hour.[0-23]
- when every is day of week, can be minute.[0-59], hour.[0-23]
- at can also be multiple at values seperated by one space.
- """
- every_type, every = self.parse_every(), self.every
- ats = self.parse_at()
- if every_type == MINUTE:
- if ats:
- raise ValidationError("at can not be set when every is"
- " minute related")
- elif every_type == HOUR:
- for at_type in ats:
- if at_type not in (MINUTE):
- raise ValidationError("%s can not be set when every is"
- " hour related" % at_type)
- elif every_type == DAY:
- for at_type in ats:
- if at_type not in (MINUTE, HOUR):
- raise ValidationError("%s can not be set when every is"
- " month day related" % at_type)
- elif every_type == MONTH:
- for at_type in ats:
- if at_type not in (MINUTE, HOUR, DAY, WEEK):
- raise ValidationError("%s can not be set when every is"
- " month related" % at_type)
- elif every_type == WEEK:
- for at_type in ats:
- if at_type not in (MINUTE, HOUR):
- raise ValidationError("%s can not be set when every is"
- " week day related" % at_type)
- return every_type, every, ats
- def parse_time(self):
- """Parse every and at into cron time syntax::
- # * * * * * command to execute
- # ┬ ┬ ┬ ┬ ┬
- # │ │ │ │ │
- # │ │ │ │ │
- # │ │ │ │ └─── day of week (0 - 7) (0 to 6 are Sunday to Saturday)
- # │ │ │ └───── month (1 - 12)
- # │ │ └─────── day of month (1 - 31)
- # │ └───────── hour (0 - 23)
- # └─────────── minute (0 - 59)
- """
- every_type, every, ats = self.validate_time()
- time = ['*'] * 5
- if every_type == MINUTE:
- frequency = get_frequency(every)
- time[0] = self.produce_frequency_time(frequency, 59)
- elif every_type == HOUR:
- frequency = get_frequency(every)
- time[0] = ats.get(MINUTE, '0')
- time[1] = self.produce_frequency_time(frequency, 23)
- elif every_type == DAY:
- frequency = get_frequency(every)
- time[0] = ats.get(MINUTE, '0')
- time[1] = ats.get(HOUR, '0')
- time[2] = self.produce_frequency_time(frequency, 31, 1)
- elif every_type == MONTH:
- time[0] = ats.get(MINUTE, '0')
- time[1] = ats.get(HOUR, '0')
- time[2] = ats.get(DAY, '1')
- time[3] = self.parse_month(every)
- time[4] = ats.get(WEEK, '*')
- else:
- time[0] = ats.get(MINUTE, '0')
- time[1] = ats.get(HOUR, '0')
- time[4] = self.parse_week(every)
- return ' '.join(time)
- @property
- def task_in_cron_syntax(self):
- """Cron content task part.
- """
- kwargs = {
- "path": self.path,
- "environment": self.env,
- "task": self.task,
- "output": self.output
- }
- task = self.task_template().format(**kwargs)
- task = self.process_template(task)
- main = self.main_template.format(task=task)
- return self.process_template(main)
- @property
- def time_in_cron_syntax(self):
- """Cron content time part.
- """
- if CRON_TIME_SYNTAX_RE.match(self.every):
- return self.every
- elif self.every in PREDEFINED_DEFINITIONS:
- return "@%s" % self.every
- else:
- return self.parse_time()
- @property
- def cron(self):
- """Job in cron syntax."""
- return ' '.join([self.time_in_cron_syntax, self.task_in_cron_syntax])
- class CommandJob(Job):
- """The command job.
- """
- def task_template(self):
- """Template::
- '{task} {output}'
- """
- return '{task} {output}'
- class ScriptJob(Job):
- """The script job.
- """
- def task_template(self):
- """Template::
- 'cd {path} && {environment} %s {task} {output}' % sys.executable
- """
- return 'cd {path} && {environment} %s {task} {output}' % sys.executable
- class ModuleJob(Job):
- """The module job.
- """
- def task_template(self):
- """Template::
- '{environment} %s -m {task} {output}' % sys.executable
- """
- return '{environment} %s -m {task} {output}' % sys.executable
- class RawJob(Job):
- """The raw job.
- """
- def task_template(self):
- """Template::
- '{task}'
- """
- return '{task}'
|