~yerinalexey/pcrond

0465534bccb5b2251b68a2e953071b74e3f4c8f4 — Luca Vercelli 2 years ago f5ff9ac
clean code
5 files changed, 83 insertions(+), 53 deletions(-)

M pcrond/__init__.py
M pcrond/job.py
M pcrond/sched.py
M setup.py
M test_scheduler.py
M pcrond/__init__.py => pcrond/__init__.py +4 -1
@@ 1,6 1,9 @@

# Here, flake8 gives error F401 '.job.Job' imported but unused
# However I have to import that, don't I ?
# pylint: disable-msg= F401 
from .job import Job
from .sched import Scheduler

#default instance
# default instance
scheduler = Scheduler()

M pcrond/job.py => pcrond/job.py +44 -26
@@ 1,8 1,8 @@

MONTH_OFFSET = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
        'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov':11, 'dec':12}
                'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
WEEK_OFFSET = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5,
        'sat': 6}
               'sat': 6}
ALIASES = {
    '@yearly':  '0 0 1 1 *',
    '@annually':  '0 0 1 1 *',


@@ 12,6 12,10 @@ ALIASES = {
    '@hourly':  '0 * * * *',
}

import logging
logger = logging.getLogger('schedule')


class Job(object):
    """
    A periodic job as used by :class:`Scheduler`.


@@ 21,7 25,8 @@ class Job(object):
        Constructor
        :param crontab:
            string containing crontab pattern
            Its tokens may be either: 1 (if alias), 5 (without year token), 6 (with year token)
            Its tokens may be either: 1 (if alias), 5 (without year token),
            6 (with year token)
        :param job_func:
            the job 0-ary function to run
            if None, you should set it later


@@ 44,18 49,26 @@ class Job(object):
        if len(crontab_lst) == 5:
            crontab_lst.append("*")
        if len(crontab_lst) != 6:
            raise ValueError("Each crontab pattern *must* contain either 5 or 6 items")
        [self.allowed_every_min, self.allowed_min] = self._parse_min(crontab_lst[0])
        [self.allowed_every_hour, self.allowed_hours] = self._parse_hour(crontab_lst[1])
        [self.allowed_every_dom, self.allowed_dom] = self._parse_day_in_month(crontab_lst[2])
        [self.allowed_every_month, self.allowed_months] = self._parse_month(crontab_lst[3])
        [self.allowed_every_dow, self.allowed_dow] = self._parse_day_in_week(crontab_lst[4])
        [self.allowed_every_year, self.allowed_years] = self._parse_year(crontab_lst[5])
            raise ValueError(
                "Each crontab pattern *must* contain either 5 or 6 items")
        [self.allowed_every_min, self.allowed_min] = \
                self._parse_min(crontab_lst[0])
        [self.allowed_every_hour, self.allowed_hours] = \
                self._parse_hour(crontab_lst[1])
        [self.allowed_every_dom, self.allowed_dom] = \
                self._parse_day_in_month(crontab_lst[2])
        [self.allowed_every_month, self.allowed_months] = \
                self._parse_month(crontab_lst[3])
        [self.allowed_every_dow, self.allowed_dow] = \
                self._parse_day_in_week(crontab_lst[4])
        [self.allowed_every_year, self.allowed_years] = \
                self._parse_year(crontab_lst[5])
        
        self.must_calculate_last_dom = True if -1 in self.allowed_dom else False
        self.must_calculate_last_dom = (-1 in self.allowed_dom)
        
        if -1 in self.allowed_years:
            raise ValueError("Wrong format '%s' : 'L' is meaningless talking about Years" % crontab_lst[5])
            raise ValueError(("Wrong format '%s' : 'L' is meaningless " +
                                      "talking about Years") % crontab_lst[5])
        
        self.crontab_pattern = crontab_lst



@@ 68,21 81,26 @@ class Job(object):
            try:
                return int(newtoken)
            except ValueError:
                raise ValueError("token %s maps to %s, however the latter is not an integer" % (token, newtoken))
                raise ValueError("token %s maps to %s, however the latter " +
                    "is not an integer" % (token, newtoken))
        try:
            return int(token)
        except ValueError:
            raise ValueError("token %s is not an integer, nor it is a known constant" % token)
            raise ValueError(("token %s is not an integer, " +
                                        "nor it is a known constant") % token)
            
    
    def _parse_common(self, s, maxval, offsets={}):
        """
        generate a set of integers, corresponding to "allowed values".
        Work for minute, hours, weeks, month, ad days of week, because they are all "similar".
        Generate a set of integers, corresponding to "allowed values".
        Work for minute, hours, weeks, month, ad days of week, because they
        are all "similar".
        Does not work very well for years and days of month
        Supported formats: "*", "*/3", "1,2,3", "L", "1,2-5,jul,10-L", "50-10"
        :param maxval: es. 60 for minutes, 12 for month, ...
        :param offsets: a dict mapping names (es. "mar") to their offsets (es. 2).
        :param maxval:
            es. 60 for minutes, 12 for month, ...
        :param offsets:
            a dict mapping names (es. "mar") to their offsets (es. 2).
        """
        if "L" not in offsets:
            offsets["L"] = maxval-1


@@ 92,7 110,8 @@ class Job(object):
            try:
                step = int(s[2:])
            except ValueError:
                raise ValueError("Wrong format '%s' - expecting an integer after '*/'" % s)
                raise ValueError(
                    "Wrong format '%s' - expecting an integer after '*/'" % s)
            return [False, set(range(0, maxval, step))]
        else:                           #at given minutes
            ranges = s.split(",")                       #   ["1","2-5","jul","10-L"]


@@ 123,15 142,16 @@ class Job(object):
        return self._parse_common(s, 7, WEEK_OFFSET)

    def _parse_year(self, s):
        """ to put things simple, I assume a range of years between 0 and 3000 ... This is mostly useless. """
        """ to put things simple, I assume a range of years between 0 and 3000 ...
        This is mostly useless. """
        return self._parse_common(s, 3000, {"L" : -1})

    def _parse_day_in_month(self, s):
        return self._parse_common(s, 31, {"L" : -1})    #this works by chance
        return self._parse_common(s, 31, {"L" : -1})    # this works by chance

    def _check_day_in_month(self, now):
        if self.must_calculate_last_dom:
            #this is a hack for avoiding to calculate "L" when not needed
            # this is a hack for avoiding to calculate "L" when not needed
            import calendar
            last_day_of_month = calendar.monthrange(now.year, now.month)[1]
            if now.day == last_day_of_month:


@@ 151,7 171,6 @@ class Job(object):
        """
        import datetime
        now = datetime.datetime.now()
        #FIXME was: return datetime.datetime.now() >= self.next_run
        return not self.running \
            and (self.allowed_every_year or now.year in self.allowed_years) \
            and (self.allowed_every_month or now.month in self.allowed_months) \


@@ 165,17 184,16 @@ class Job(object):
        Run the job.
        :return: The return value returned by the `job_func`
        """
        logger.info('Running job %s', self)
        self.running = True
        ret = self.job_func()
        self.running = false
        return ret
        

    def run_if_should(self):
        """
        Run the job if needed.
        :return: The return value returned by the `job_func`
        """
        if self.should_run():
            logger.info('Running job %s', self)
            return self.run()


M pcrond/sched.py => pcrond/sched.py +26 -16
@@ 1,6 1,8 @@
#most of the code here comes from https://github.com/dbader/schedule
# most of the code here comes from https://github.com/dbader/schedule

from .job import ALIASES, Job
import logging
import time

def std_launch_func(cmd_splitted):
    """


@@ 9,9 11,10 @@ def std_launch_func(cmd_splitted):
    def f():
        import subprocess
        subprocess.run(cmd_splitted)
        #not returning anything here
        # not returning anything here
    return f


class Scheduler(object):
    """
    Objects instantiated by the :class:`Scheduler <Scheduler>` are


@@ 19,7 22,7 @@ class Scheduler(object):
    handle their execution.
    """
    def __init__(self):
        self.delay = 60         #in seconds
        self.delay = 60         # in seconds
        self.jobs = []

    def run_pending(self):


@@ 78,13 81,15 @@ class Scheduler(object):

    def _run_job(self, job):
        ret = job.job_func()
        return ret

    def add_job(self, crontab, job_func):
        """
        Create a job and add it to this Scheduler
        :param crontab:
            string containing crontab pattern
            Its tokens may be either: 1 (if alias), 5 (without year token), 6 (with year token)
            Its tokens may be either: 1 (if alias), 5 (without year token),
            6 (with year token)
        :param job_func:
            the job 0-ary function to run
        :return: a Job


@@ 92,8 97,9 @@ class Scheduler(object):
        job = Job(crontab, job_func, self)
        self.jobs.append(job)
        return job
        
    def _load_crontab_line(self, rownum, crontab_line, job_func_func=std_launch_func):

    def _load_crontab_line(self, rownum, crontab_line, job_func_func=
                                                        std_launch_func):
        """
        create a Job from a single crontab entry, and add it to this Scheduler
        :param crontab_line:


@@ 105,40 111,45 @@ class Scheduler(object):
        """
        pieces = crontab_line.split()
        
        #is pattern using aliases?
        # is pattern using aliases?
        if pieces[0] in ALIASES.keys():
            try:
                #pattern using alias
                # pattern using alias
                job = self.add_job(pieces[0:1], job_func_func(pieces[1:]))
                return job
            except ValueError:
                print("Error at line %d, cannot parse pattern" % rownum)    #shouldn't happen
                # shouldn't happen
                print("Error at line %d, cannot parse pattern" % rownum)
                return None
        elif len(pieces) < 6:
            print("Error at line %d, expected at least 6 tokens" % rownum)
            return None
            if len(pieces) >= 7:
                try:
                    #pattern including year
                    job = self.add_job(" ".join(pieces[0:6]), job_func_func(pieces[6:]))
                    # pattern including year
                    job = self.add_job(" ".join(pieces[0:6]), job_func_func(
                                                                pieces[6:]))
                    return job
                except ValueError:
                    pass
            try:
                #pattern not including  year
                job = self.add_job(" ".join(pieces[0:5]), job_func_func(pieces[5:]))
                # pattern not including  year
                job = self.add_job(" ".join(pieces[0:5]), job_func_func(
                                                                pieces[5:]))
                return job
            except ValueError:
                print("Error at line %d, cannot parse pattern" % rownum)
                return None

    def load_crontab_file(self, crontab_file, clear=True, job_func_func=std_launch_func):
    def load_crontab_file(self, crontab_file, clear=True, job_func_func=
                                                             std_launch_func):
        """
        Read crontab file, create corresponding jobs in this scheduler
        :param crontab_file:
            crontab file path
        :param job_func_func:
            a function that takes a list of tokens (from crontab file) and returns a 0-args function
            a function that takes a list of tokens (from crontab file) and
            returns a 0-args function
        :param clear:
            should the new schedule override the previous ones?
        """


@@ 160,4 171,3 @@ class Scheduler(object):
        while True:
            self.run_pending()
            time.sleep(self.delay)


M setup.py => setup.py +6 -7
@@ 1,9 1,9 @@
#!/usr/bin/env python
#this is the standard way of installing a Python module, using distutils:
#sudo ./setup.py install
#or
#./setup.py install --prefix=~/.local
#uninstall is not provided, see https://stackoverflow.com/questions/1550226
# this is the standard way of installing a Python module, using distutils:
# sudo ./setup.py install
# or
# ./setup.py install --prefix=~/.local
# uninstall is not provided, see https://stackoverflow.com/questions/1550226

from distutils.core import setup



@@ 15,5 15,4 @@ setup(name='pcrond',
      url='https://github.com/luca-vercelli/pcrond',
      packages=['pcrond'],
      scripts=['scripts/pcrond'],
     )

      )

M test_scheduler.py => test_scheduler.py +3 -3
@@ 5,9 5,9 @@ import unittest

# Silence "missing docstring", "method could be a function",
# "class already defined", and "too many public methods" messages:
# pylint: disable-msg=R0201,C0111,E0102,R0904,R0901
# p y l i n t: disable-msg=R0201,C0111,E0102,R0904,R0901

from pcrond import scheduler, Job, Scheduler
from pcrond import scheduler, Job


def do_nothing():


@@ 65,7 65,7 @@ class SchedulerTests(unittest.TestCase):

    def test_job_constructor_reverse_order(self):
        job = Job("* 23-4 * * *")
        assert job.allowed_hours == set([23,0,1,2,3,4])
        assert job.allowed_hours == set([23, 0, 1, 2, 3, 4])

    def test_job_constructor_wrong(self):
        with self.assertRaises(ValueError):