python-crontab-1.9.3/0000775000175000017500000000000012504000222016156 5ustar doctormodoctormo00000000000000python-crontab-1.9.3/crontab.py0000664000175000017500000007576212503776613020230 0ustar doctormodoctormo00000000000000# # Copyright 2014, Martin Owens # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Ideas from gnome-schedule: Philip Van Hoof, Gaute Hope, Kristof Vansant # # REQUEST: Please do NOT simply copy and paste this code into your own # projects. Please package this module for your distribution and # use as a direct dependancy. # """ from crontab import CronTab import sys # Create a new non-installed crontab cron = CronTab(tab='') job = cron.new(command='/usr/bin/echo') job.minute.during(5,50).every(5) job.hour.every(4) job.dow.on('SUN') job.month.during('APR', 'JUN') job.month.also.during('OCT', 'DEC') job.every(2).days() job.setall(1, 12, None, None, None) job2 = cron.new(command='/foo/bar', comment='SomeID') job2.every_reboot() jobs = list(cron.find_command('bar')) job3 = jobs[0] job3.clear() job3.minute.every(1) sys.stdout.write(str(cron.render())) job3.enable(False) for job4 in cron.find_command('echo'): sys.stdout.write(job4) for job5 in cron.find_comment('SomeID'): sys.stdout.write(job5) for job6 in cron: sys.stdout.write(job6) for job7 in cron: job7.every(3).hours() sys.stdout.write(job7) job7.every().dow() cron.remove_all(command='/foo/bar') cron.remove_all(comment='This command') cron.remove_all(time='* * * * *') cron.remove_all() output = cron.render() cron.write() cron.write(filename='/tmp/output.txt') #cron.write_to_user(user=True) #cron.write_to_user(user='root') # Croniter Extentions allow you to ask for the scheduled job times, make # sure you have croniter installed, it's not a hard dependancy. job3.schedule().get_next() job3.schedule().get_prev() """ import os import re import sys import codecs import tempfile import subprocess as sp from datetime import date, datetime __pkgname__ = 'python-crontab' __version__ = '1.9.3' ITEMREX = re.compile(r'^\s*([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)' r'\s+([^@#\s]+)\s+([^#\n]*)(\s+#\s*([^\n]*)|$)') SPECREX = re.compile(r'^\s*@(\w+)\s([^#\n]*)(\s+#\s*([^\n]*)|$)') DEVNULL = ">/dev/null 2>&1" WEEK_ENUM = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] MONTH_ENUM = [None, 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] SPECIALS = {"reboot": '@reboot', "hourly": '0 * * * *', "daily": '0 0 * * *', "weekly": '0 0 * * 0', "monthly": '0 0 1 * *', "yearly": '0 0 1 1 *', "annually": '0 0 1 1 *', "midnight": '0 0 * * *'} SPECIAL_IGNORE = ['midnight', 'annually'] S_INFO = [ {'name': 'Minutes', 'max': 59, 'min': 0}, {'name': 'Hours', 'max': 23, 'min': 0}, {'name': 'Day of Month', 'max': 31, 'min': 1}, {'name': 'Month', 'max': 12, 'min': 1, 'enum': MONTH_ENUM}, {'name': 'Day of Week', 'max': 6, 'min': 0, 'enum': WEEK_ENUM}, ] # Detect Python3 and which OS for temperments. import platform PY3 = platform.python_version()[0] == '3' WINOS = platform.system() == 'Windows' SYSTEMV = not WINOS and os.uname()[0] in ["SunOS", "AIX", "HP-UX"] if not WINOS: import pwd CRONCMD = "/usr/bin/crontab" if PY3: # pylint: disable=W0622 unicode = str basestring = str def pipeOpen(cmd, *args, **flags): """Runs a program and orders the arguments for compatability. a. keyword args are flags and always appear /before/ arguments for bsd """ l = tuple(cmd.split(' ')) for (k,v) in flags.items(): if v is not None: l += len(k)==1 and ("-%s" % (k,), str(v)) or ("--%s=%s" % (k,v),) l += tuple(args) return sp.Popen(tuple(a for a in l if a), stdout=sp.PIPE, stderr=sp.PIPE) class CronTab(object): """ Crontab object which can access any time based cron using the standard. user - Set the user of the crontab (default: None) * 'user' = Load from $username's crontab (instead of tab or tabfile) * None = Don't load anything from any user crontab. * True = Load from current $USER's crontab (unix only) * False = This is a system crontab, each command has a username tab - Use a string variable as the crontab instead of installed crontab tabfile - Use a file for the crontab instead of installed crontab log - Filename for logfile instead of /var/log/syslog """ def __init__(self, user=None, tab=None, tabfile=None, log=None): self.lines = None self.crons = None self.filen = None # Protect windows users self.root = not WINOS and os.getuid() == 0 # Storing user flag / username self._user = user # Load string or filename as inital crontab self.intab = tab self.read(tabfile) self._log = log @property def log(self): """Returns the CronLog object for this tab (user or root tab only)""" from cronlog import CronLog if self._log is None or isinstance(self._log, basestring): self._log = CronLog(self._log, user=self.user or 'root') return self._log @property def user(self): if self._user is True and pwd: return pwd.getpwuid(os.getuid())[0] return self._user @property def user_opt(self): # Fedora and Mac require the current user to not specify # But Ubuntu/Debian doesn't care. Be careful here. if self._user and self._user is not True: return {'u': self._user} return {} def read(self, filename=None): """ Read in the crontab from the system into the object, called automatically when listing or using the object. use for refresh. """ self.crons = [] self.lines = [] lines = [] if self.intab is not None: lines = self.intab.split('\n') elif filename: self.filen = filename with codecs.open(filename, 'r', encoding='utf-8') as fhl: lines = fhl.readlines() elif self.user: (out, err) = pipeOpen(CRONCMD, l='', **self.user_opt).communicate() if err and 'no crontab for' in str(err): pass elif err: raise IOError("Read crontab %s: %s" % (self.user, err)) lines = out.decode('utf-8').split("\n") for line in lines: cron = CronItem(line, cron=self) if cron.is_valid(): self.crons.append(cron) self.lines.append(cron) else: self.lines.append(line.replace('\n', '')) def write(self, filename=None): """Write the crontab to it's source or a given filename.""" if filename: self.filen = filename # Add to either the crontab or the internal tab. if self.intab is not None: self.intab = self.render() # And that's it if we never saved to a file if not self.filen: return if self.filen: fileh = open(self.filen, 'wb') else: filed, path = tempfile.mkstemp() fileh = os.fdopen(filed, 'wb') fileh.write(self.render().encode('utf-8')) fileh.close() if not self.filen: # Add the entire crontab back to the user crontab pipeOpen(CRONCMD, path, **self.user_opt).wait() os.unlink(path) def write_to_user(self, user=None): """Write the crontab to a user (or root) instead of a file.""" self.filen = None self.intab = None if user is not None: self._user = user self.write() def render(self): """Render this crontab as it would be in the crontab.""" crons = [] for cron in self.lines: crons.append(unicode(cron)) result = u'\n'.join(crons) if result and result[-1] not in (u'\n', u'\r'): result += u'\n' return result def new(self, command='', comment='', user=None): """ Create a new cron with a command and comment. Returns the new CronItem object. """ if not user and self.user is False: raise ValueError("User is required for system crontabs.") item = CronItem(command=command, comment=comment, user=user, cron=self) self.crons.append(item) self.lines.append(item) return item def find_command(self, command): """Return an iter of jobs matching any part of the command.""" for job in self.crons: if command in job.command: yield job def find_comment(self, comment): """Return an iter of jobs that match the comment field exactly.""" for job in self.crons: if job.comment == comment: yield job def find_time(self, *args): """Return an iter of jobs that match this time pattern""" for job in self.crons: if job.slices == CronSlices(*args): yield job @property def commands(self): """Return a generator of all unqiue commands used in this crontab""" returned = [] for cron in self.crons: if cron.command not in returned: yield cron.command returned.append(cron.command) @property def comments(self): """Return a generator of all unique comments/Id used in this crontab""" returned = [] for cron in self.crons: if cron.comment and cron.comment not in returned: yield cron.comment returned.append(cron.comment) def remove_all(self, command=None, comment=None, time=None): """Removes all crons using the stated command OR that have the stated comment OR removes everything if no arguments specified.""" if command: return self.remove(*self.find_command(command)) elif comment: return self.remove(*self.find_comment(comment)) elif time: return self.remove(*self.find_time(time)) return self.remove(*self.crons[:]) def remove(self, *items): """Remove a selected cron from the crontab.""" result = 0 for item in items: result += self._remove(item) return result def _remove(self, item): """Internal removal of an item""" # The last item often has a trailing line feed if self.crons[-1] == item and self.lines[-1] == '': self.lines.remove(self.lines[-1]) self.crons.remove(item) self.lines.remove(item) return 1 def __iter__(self): return self.crons.__iter__() def __getitem__(self, i): return self.crons[i] def __unicode__(self): return self.render() def __len__(self): return len(self.crons) def __str__(self): return self.render() class CronItem(object): """ An item which objectifies a single line of a crontab and May be considered to be a cron job object. """ def __init__(self, line=None, command='', comment='', user=None, cron=None): self.cron = cron self.user = user self.valid = False self.enabled = True self.special = False self.comment = None self.command = None self._log = None # Initalise five cron slices using static info. self.slices = CronSlices() self.set_comment(comment) if line and line.strip(): self.parse(line.strip()) elif command: self.set_command(command) self.valid = True def delete(self): """Delete this item and remove it from it's parent""" if not self.cron: raise UnboundLocalError("Cron item is not associated with a crontab!") else: self.cron.remove(self) def set_command(self, cmd): """Set the command and filter as needed""" self.command = cmd.strip() def set_comment(self, cmt): """Set the comment and don't filter""" self.comment = cmt def parse(self, line): """Parse a cron line string and save the info as the objects.""" if type(line) is str and not PY3: line = unicode(line, 'utf-8') if not line or line[0] == '#': self.enabled = False line = line[1:].strip() self._set_parse(ITEMREX.findall(line)) self._set_parse(SPECREX.findall(line)) def _set_parse(self, result): if not result: return if self.cron.user == False: # Special flag to look for per-command user (self.user, cmd) = (result[0][-3] + ' ').split(' ', 1) self.set_command(cmd) else: self.set_command(result[0][-3]) self.valid = self.setall(*result[0][:-3]) self.comment = result[0][-1] self.enabled = self.enabled and self.valid def enable(self, enabled=True): """Set if this cron job is enabled or not""" if enabled in [True, False]: self.enabled = enabled return self.enabled def is_enabled(self): """Return true if this job is enabled (not commented out)""" return self.enabled def is_valid(self): """Return true if this job is valid""" return self.valid def render(self): """Render this set cron-job to a string""" if type(self.command) is str and not PY3: self.command = unicode(self.command, 'utf-8') user = '' if self.cron and self.cron.user is False: if not self.user: raise ValueError("Job to system-cron format, no user set!") user = self.user + ' ' result = u"%s %s%s" % (str(self.slices), user, self.command) if self.comment: if type(self.comment) is str and not PY3: self.comment = unicode(self.comment, 'utf-8') result += u" # " + self.comment if not self.enabled: result = u"# " + result return result def every_reboot(self): """Set to every reboot instead of a time pattern: @reboot""" self.clear() return self.slices.setall('@reboot') def every(self, unit=1): """ Replace existing time pattern with a single unit, setting all lower units to first value in valid range. For instance job.every(3).days() will be `0 0 */3 * *` while job.day().every(3) would be `* * */3 * *` Many of these patterns exist as special tokens on Linux, such as `@midnight` and `@hourly` """ return ItemEveryInterface(self.slices, unit) def setall(self, *args): """Replace existing time pattern with these five values given as args: job.setall("1 2 * * *") job.setall(1, 2) == '1 2 * * *' job.setall(0, 0, None, '>', 'SUN') == '0 0 * 12 SUN' """ return self.slices.setall(*args) def clear(self): """Clear the special and set values""" return self.slices.clear() def frequency(self, year=None): """Returns the number of times this item will execute in a given year (defaults to this year) """ return self.slices.frequency(year=year) def frequency_per_year(self, year=None): """Returns the number of /days/ this item will execute on in a year (defaults to this year) """ return self.slices.frequency_per_year(year=year) def frequency_per_day(self): """Returns the number of time this item will execute in any day""" return self.slices.frequency_per_day() def schedule(self, date_from=None): """Return a croniter schedule is available.""" if not date_from: date_from = datetime.now() try: # Croniter is an optional import from croniter.croniter import croniter except ImportError: raise ImportError("Croniter is not available. Please install croniter" " python module via pip or your package manager") class Croniter(croniter): """Same as normal croniter, but always return datetime objects""" def get_next(self, type_ref=datetime): return croniter.get_next(self, type_ref) def get_prev(self, type_ref=datetime): return croniter.get_prev(self, type_ref) def get_current(self, type_ref=datetime): return croniter.get_current(self, type_ref) return Croniter(self.slices.clean_render(), date_from) @property def log(self): """Return a cron log specific for this job only""" if not self._log and self.cron: self._log = self.cron.log.for_program(self.command) return self._log @property def minute(self): """Return the minute slice""" return self.slices[0] @property def minutes(self): """Same as minute""" return self.minute @property def hour(self): """Return the hour slice""" return self.slices[1] @property def hours(self): """Same as hour""" return self.hour @property def day(self): return self.dom @property def dom(self): """Return the day-of-the month slice""" return self.slices[2] @property def month(self): """Return the month slice""" return self.slices[3] @property def months(self): """Same as month""" return self.month @property def dow(self): """Return the day of the week slice""" return self.slices[4] def __repr__(self): return "" % str(self) def __len__(self): return len(str(self)) def __getitem__(self, x): return self.slices[x] def __lt__(self, value): return self.frequency() < CronSlices(value).frequency() def __gt__(self, value): return self.frequency() > CronSlices(value).frequency() def __str__(self): return self.__unicode__() def __unicode__(self): if not self.is_valid(): raise ValueError('Refusing to render invalid crontab. Disable to continue.') return self.render() class ItemEveryInterface(object): """Provide an interface to the job.every() method: Available Calls: minute, minutes, hour, hours, dom, doms, month, months, dow, dows Once run all units will be cleared (set to *) then proceeding units will be set to '0' and the target unit will be set as every x units. """ def __init__(self, item, units): self.slices = item self.unit = units for (x, i) in enumerate(['minute', 'hour', 'dom', 'month', 'dow', 'min', 'hour', 'day', 'moon', 'weekday']): setattr(self, i, self._set(x % 5)) setattr(self, i+'s', self._set(x % 5)) def _set(self, target): def innercall(): """Returned inner call for setting slice targets""" self.slices.clear() # Day-of-week is actually a level 2 set, not level 4. for p in range(target == 4 and 2 or target): self.slices[p].on('<') self.slices[target].every(self.unit) return innercall def year(self): """Special every year target""" if self.unit > 1: raise ValueError("Invalid value '%s', outside 1 year" % self.unit) self.slices.setall('@yearly') class CronSlices(list): """Controls a list of five time 'slices' which reprisent: minute frequency, hour frequency, day of month frequency, month requency and finally day of the week frequency. """ def __init__(self, *args): for info in S_INFO: self.append(CronSlice(info)) self.special = None if args and not self.setall(*args): raise ValueError("Can't set cron value to: %s" % str(args)) def setall(self, to, *slices): """Parses the various ways date/time frequency can be specified""" self.clear() if isinstance(to, CronItem): slices = to.slices elif isinstance(to, list): slices = to elif slices: slices = (to,) + slices elif isinstance(to, basestring) and to: if to.count(' ') == 4: slices = to.strip().split(' ') elif to.strip()[0] == '@': to = to[1:] else: slices = [to] if to == 'reboot': self.special = '@reboot' return True elif to in SPECIALS.keys(): return self.setall(SPECIALS[to]) if id(slices) == id(self): raise AssertionError("Can not set cron to itself!") for a, b in zip(self, slices): try: a.parse(b) except ValueError as error: sys.stderr.write("WARNING: %s\n" % str(error)) return False except Exception: return False return True def clean_render(self): """Return just numbered parts of this crontab""" return ' '.join([unicode(s) for s in self]) def render(self): "Return just the first part of a cron job (the numbers or special)" time = self.clean_render() if self.special: return self.special elif not SYSTEMV: for (name, value) in SPECIALS.items(): if value == time and name not in SPECIAL_IGNORE: return "@%s" % name return time def clear(self): """Clear the special and set values""" self.special = None for s in self: s.clear() def frequency(self, year=None): return self.frequency_per_year(year=year) * self.frequency_per_day() def frequency_per_year(self, year=None): result = 0 if not year: year = date.today().year weekdays = list(self[4]) for month in self[3]: for day in self[2]: try: if date(year, month, day).weekday() in weekdays: result += 1 except ValueError: continue return result def frequency_per_day(self): """Returns the number of time this item will execute in any day""" return len(self[0]) * len(self[1]) def __str__(self): return self.render() def __eq__(self, arg): return self.render() == CronSlices(arg).render() class SundayError(KeyError): pass class CronSlice(object): """Cron slice object which shows a time pattern""" def __init__(self, info, value=None): self.min = info.get('min', None) self.max = info.get('max', None) self.name = info.get('name', None) self.enum = info.get('enum', None) self.parts = [] if value: self.parse(value) def parse(self, value): """Set values into the slice.""" self.parts = [] if value is None: return self.clear() for part in str(value).split(','): if part.find("/") > 0 or part.find("-") > 0 or part == '*': self.parts.append(self.get_range(part)) else: try: self.parts.append(self._v(part)) except SundayError: self.parts.append(0) except ValueError as err: raise ValueError('%s:%s/%s' % (str(err), self.name, part)) def render(self, resolve=False): """Return the slice rendered as a crontab. resolve - return integer values instead of enums (default False) """ if len(self.parts) == 0: return '*' return _render_values(self.parts, ',', resolve) def __repr__(self): return "" % str(self) def __eq__(self, value): return str(self) == str(value) def __str__(self): return self.__unicode__() def __unicode__(self): return self.render() def every(self, n_value, also=False): """Set the every X units value""" if not also: self.clear() self.parts.append(self.get_range(int(n_value))) return self.parts[-1] def on(self, *n_value, **opts): """Set the time values to the specified placements.""" if not opts.get('also', False): self.clear() for av in n_value: try: self.parts += self._v(av), except SundayError: self.parts += 0, return self.parts def during(self, vfrom, vto, also=False): """Set the During value, which sets a range""" if not also: self.clear() self.parts.append(self.get_range(self._v(vfrom), self._v(vto))) return self.parts[-1] @property def also(self): """Appends rather than replaces the new values""" outself = self class Also(object): """Will append new values""" def every(self, *a): """Also every one of these""" return outself.every(*a, also=True) def on(self, *a): """Also on these""" return outself.on(*a, also=True) def during(self, *a): """Also during these""" return outself.during(*a, also=True) return Also() def clear(self): """clear the slice ready for new vaues""" self.parts = [] def get_range(self, *vrange): """Return a cron range for this slice""" return CronRange(self, *vrange) def __iter__(self): """Return the entire element as an iterable""" r = {} # An empty part means '*' which is every(1) if not self.parts: self.every(1) for part in self.parts: if isinstance(part, CronRange): for bit in part.range(): r[bit] = 1 else: r[int(part)] = 1 for x in r: yield x def __len__(self): """Returns the number of times this slice happens in it's range""" return len(list(self.__iter__())) def _v(self, v): if v == '>': v = self.max elif v == '<': v = self.min try: out = get_cronvalue(v, self.enum) except ValueError: raise ValueError("Unrecognised '%s'='%s'" % (self.name, v)) except KeyError: raise KeyError("No enumeration '%s' got '%s'" % (self.name, v)) if self.max == 6 and int(out) == 7: raise SundayError("Detected Sunday as 7 instead of 0!") if int(out) < self.min or int(out) > self.max: raise ValueError("Invalid value '%s', expected %d-%d for %s" % ( str(v), self.min, self.max, self.name)) return out def filter_v(self, v): """Support wrapper for enumerations and check for range""" return self._v(v) def get_cronvalue(value, enums): """Returns a value as int (pass-through) or a special enum value""" if isinstance(value, int): return value elif str(value).isdigit(): return int(str(value)) if not enums: raise KeyError("No enumeration allowed") return CronValue(str(value), enums) class CronValue(object): """Represent a special value in the cron line""" def __init__(self, value, enums): self.enum = value self.value = enums.index(value.lower()) def __lt__(self, value): return self.value < int(value) def __repr__(self): return str(self) def __str__(self): return self.enum def __int__(self): return self.value def _render_values(values, sep=',', resolve=False): """Returns a rendered list, sorted and optionally resolved""" if len(values) > 1: values.sort() return sep.join([_render(val, resolve) for val in values]) def _render(value, resolve=False): """Return a single value rendered""" if isinstance(value, CronRange): return value.render(resolve) if resolve: return str(int(value)) return str(value) class CronRange(object): """A range between one value and another for a time range.""" def __init__(self, vslice, *vrange): self.slice = vslice self.cron = None self.seq = 1 if not vrange: self.all() elif isinstance(vrange[0], basestring): self.parse(vrange[0]) elif isinstance(vrange[0], int) or isinstance(vrange[0], CronValue): if len(vrange) == 2: (self.vfrom, self.vto) = vrange else: self.seq = vrange[0] self.all() def parse(self, value): """Parse a ranged value in a cronjob""" if value.count('/') == 1: value, seq = value.split('/') self.seq = self.slice.filter_v(seq) if self.seq < 1 or self.seq > self.slice.max - 1: raise ValueError("Sequence can not be divided by zero or max") if value.count('-') == 1: vfrom, vto = value.split('-') self.vfrom = self.slice.filter_v(vfrom) try: self.vto = self.slice.filter_v(vto) except SundayError: self.vto = 6 elif value == '*': self.all() else: raise ValueError('Unknown cron range value "%s"' % value) def all(self): """Set this slice to all units between the miniumum and maximum""" self.vfrom = self.slice.min self.vto = self.slice.max def render(self, resolve=False): """Render the ranged value for a cronjob""" value = '*' if int(self.vfrom) > self.slice.min or int(self.vto) < self.slice.max: value = _render_values([self.vfrom, self.vto], '-', resolve) if self.seq != 1: value += "/%d" % self.seq if value != '*' and SYSTEMV: value = ','.join(map(str, self.range())) return value def range(self): """Returns the range of this cron slice as a iterable list""" return range(int(self.vfrom), int(self.vto)+1, self.seq) def every(self, value): """Set the sequence value for this range.""" self.seq = int(value) def __lt__(self, value): return int(self.vfrom) < int(value) def __gt__(self, value): return int(self.vto) > int(value) def __int__(self): return int(self.vfrom) def __str__(self): return self.__unicode__() def __unicode__(self): return self.render() python-crontab-1.9.3/PKG-INFO0000664000175000017500000002651212504000222017261 0ustar doctormodoctormo00000000000000Metadata-Version: 1.1 Name: python-crontab Version: 1.9.3 Summary: Python Crontab API Home-page: https://launchpad.net/python-crontab Author: Martin Owens Author-email: doctormo@gmail.com License: GPLv3 Description: .. image:: https://launchpadlibrarian.net/134092458/python-cron-192.png :class: floating-box :alt: Python Crontab Logo Bug Reports and Development =========================== Please report any problems to the `launchpad bug tracker `_. Please use Bazaar and push patches to the `launchpad project code hosting `_. **Note:** If you get the error ``TypeError: __init__() takes exactly 2 arguments`` when using CronTab, you have the wrong module installed. You need to install ``python-crontab`` and not ``crontab`` from pypi or your local package manager and try again. Description =========== Crontab module for read and writing crontab files and accessing the system cron automatically and simply using a direct API. Comparing the `below chart `_ you will note that W, L, # and ? symbols are not supported as they are not standard Linux or SystemV crontab format. ============= =========== ================= =================== ============= Field Name Mandatory Allowed Values Special Characters Extra Values ============= =========== ================= =================== ============= Minutes Yes 0-59 \* / , - < > Hours Yes 0-23 \* / , - < > Day of month Yes 1-31 \* / , - < > Month Yes 1-12 or JAN-DEC \* / , - < > Day of week Yes 0-6 or SUN-SAT \* / , - < > ============= =========== ================= =================== ============= Extra Values are '<' for minimum value, such as 0 for minutes or 1 for months. And '>' for maximum value, such as 23 for hours or 12 for months. Supported special cases allow crontab lines to not use fields. These are the supported aliases which are not available in SystemV mode: =========== =========== Case Meaning =========== =========== @reboot Every boot @hourly 0 * * * * @daily 0 0 * * * @weekly 0 0 * * 0 @monthly 0 0 1 * * @yearly 0 0 1 1 * @annually 0 0 1 1 * @midnight 0 0 * * * =========== =========== How to Use the Module ===================== Getting access to a crontab can happen in five ways, three system methods that will work only on Unix and require you to have the right permissions:: from crontab import CronTab empty_cron = CronTab() my_user_cron = CronTab(user=True) users_cron = CronTab(user='username') And two ways from non-system sources that will work on Windows too:: file_cron = CronTab(tabfile='filename.tab') mem_cron = CronTab(tab=""" * * * * * command """) Special per-command user flag for vixie cron format (new in 1.9):: system_cron = CronTab(tabfile='/etc/crontab', user=False) job = system_cron[0] job.user != None system_cron.new(command='new_command', user='root') Creating a new job is as simple as:: job = cron.new(command='/usr/bin/echo') And setting the job's time restrictions:: job.minute.during(5,50).every(5) job.hour.every(4) job.day.on(4, 5, 6) job.dow.on('SUN') job.month.during('APR', 'NOV') Each time restriction will clear the previous restriction:: job.hour.every(10) # Set to * */10 * * * job.hour.on(2) # Set to * 2 * * * Appending restrictions is explicit:: job.hour.every(10) # Set to * */10 * * * job.hour.also.on(2) # Set to * 2,*/10 * * * Setting all time slices at once:: job.setall(2, 10, '2-4', '*/2', None) job.setall('2 10 * * *') Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Get the comment or command for a job:: command = job.command comment = job.comment Modify the comment or command on a job:: job.set_command("new_script.sh") job.set_comment("New ID or comment here") Disabled or Enable Job:: job.enable() job.enable(False) False == job.is_enabled() Validity Check:: True == job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command:: iter = cron.find_command('bar') Find an existing job by comment:: iter = cron.find_comment('ID or some text') Find an existing job by schedule:: iter = cron.find_time(2, 10, '2-4', '*/2', None) iter = cron.find_time("*/2 * * * *") Clean a job of all rules:: job.clear() Iterate through all jobs:: for job in cron: print job Iterate through all lines:: for line in cron.lines: print line Remove Items:: cron.remove( job ) cron.remove_all('echo') cron.remove_all(comment='foo') cron.remove_all(time='*/2') Clear entire cron of all jobs:: cron.remove_all() Write CronTab back to system or filename:: cron.write() Write CronTab to new filename:: cron.write( 'output.tab' ) Write to this user's crontab (unix only):: cron.write_to_user( user=True ) Write to some other user's crontab:: cron.write_to_user( user='bob' ) Proceeding Unit Confusion ========================= It is sometimes logical to think that job.hour.every(2) will set all proceeding units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling only the hours units and the minute column is unaffected. The real result would be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs. There is a special 'every' method on a job to clear the job's existing schedule and replace it with a simple single unit:: job.every(4).hours() == '0 */4 * * *' job.every().dom() == '0 0 * * *' job.every().month() == '0 0 0 * *' job.every(2).dows() == '0 0 * * */2' This is a convenience method only, it does normal things with the existing api. Frequency Calculation ===================== Every job's schedule has a frequency. We can attempt to calculate the number of times a job would execute in a give amount of time. We have three simple methods:: job.setall("1,2 1,2 * * *") job.frequency_per_day() == 4 The per year frequency method will tell you how many days a year the job would execute:: job.setall("* * 1,2 1,2 *") job.frequency_per_year(year=2010) == 4 These are combined to give the number of times a job will execute in any year:: job.setall("1,2 1,2 1,2 1,2 *") job.frequency(year=2010) == 16 Frequency can be quickly checked using python built-in operators:: job < "*/2 * * * *" job > job2 job.slices == "*/5" Log Functionality ================= The log functionality will read a cron log backwards to find you the last run instances of your crontab and cron jobs. The crontab will limit the returned entries to the user the crontab is for:: cron = CronTab(user='root') for d in cron.log: print d['pid'] + " - " + d['date'] Each job can return a log iterator too, these are filtered so you can see when the last execution was:: for d in cron.find_command('echo')[0].log: print d['pid'] + " - " + d['date'] Schedule Functionality ====================== If you have the croniter python module installed, you will have access to a schedule on each job. For example if you want to know when a job will next run:: schedule = job.schedule(date_from=datetime.now()) This creates a schedule croniter based on the job from the time specified. The default date_from is the current date/time if not specified. Next we can get the datetime of the next job:: datetime = schedule.get_next() Or the previous:: datetime = schedule.get_prev() The get methods work in the same way as the default croniter, except that they will return datetime objects by default instead of floats. If you want the original functionality, pass float into the method when calling:: datetime = schedule.get_current(float) If you don't have the croniter module installed, you'll get an ImportError when you first try using the schedule function on your cron job object. Extra Support ============= - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - Python 3.4 and Python 2.7/2.6 tested. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) Platform: linux Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Provides: crontab Provides: cronlog python-crontab-1.9.3/ChangeLog0000664000175000017500000000634212452562340017755 0ustar doctormodoctormo00000000000000python-crontab (1.9.0) precise; urgency=low * Added vixie cron compatability -- Martin Owens (DoctorMO) Fri, 02 Jan 2015 07:37:12 -0500 python-crontab (1.4.4) precise; urgency=low * Added every method to jobs for convience -- Martin Owens (DoctorMO) Thu, 11 Oct 2013 13:46:52 -0500 python-crontab (1.4.1) precise; urgency=low * Lots of bug fixes -- Martin Owens (DoctorMO) Thu, 28 May 2013 13:07:02 -0500 python-crontab (1.3.0) precise; urgency=low * Add optional croniter support * Improvements in testing -- Martin Owens (DoctorMO) Thu, 05 Apr 2013 00:22:58 -0500 python-crontab (1.2.0) precise; urgency=low * Add test for enums (MON-SUN/JAN-DEC) * Fix problems with enums * Make sure enums are preserved * Sort output for ranges and parts * Allow compatability mode to be set when creating crontab object * Allow tabs and tabfiles to be specified for opening -- Martin Owens (DoctorMO) Thu, 19 Sep 2012 12:12:31 -0400 python-crontab (1.1.0) precise; urgency=low * Add Python3 support and make sure it all works for both 2.7 and 3.2 -- Martin Owens (DoctorMO) Thu, 19 Sep 2012 12:12:31 -0400 python-crontab (1.0.0) precise; urgency=low * Add many tests to complete the functionality * Improve compatibility mode and test it * Change API of slice calls to be properties instead of methods -- Martin Owens (DoctorMO) Thu, 16 Aug 2012 21:39:52 -0400 python-crontab (0.9.7) precise; urgency=low * Add unix/sunos compatibility mode -- Martin Owens (DoctorMO) Thu, 14 Aug 2012 21:39:17 -0400 python-crontab (0.9.3) jaunty; urgency=low * Fixed problem with crontab line endings -- Martin Owens (DoctorMO) Tue, 18 Sep 2009 06:36:24 -0500 python-crontab (0.9.2) jaunty; urgency=low * Fixed problem with comments and invalid line printing -- Martin Owens (DoctorMO) Tue, 06 Jul 2009 20:10:28 -0500 python-crontab (0.9.1) intrepid; urgency=low * Fixed an error with @reboot -- Martin Owens (DoctorMO) Tue, 07 Apr 2009 17:39:08 -0500 python-crontab (0.9) hardy; urgency=low * Fix lots of pylint errors and warnings. -- Martin Owens (DoctorMO) Sat, 13 Dec 2008 21:32:01 -0800 python-crontab (0.8) hardy; urgency=low * Fix a number of problems fixed as reported. -- Martin Owens (DoctorMO) Thu, 09 Oct 2008 20:50:01 -0500 python-crontab (0.7) hardy; urgency=low * Fix parsing of special entries * Fix looping and printing of crons * Updated documentation to reflect new features. -- Martin Owens (DoctorMO) Fri, 23 Aug 2008 00:57:20 -0400 python-crontab (0.6) hardy; urgency=low * Fix problems with clearing of cron tasks * Don't let speacials mask cleared tasks * Add some comments for crontab methods * Add remove_all method to crontab -- Martin Owens (DoctorMO) Thu, 20 Aug 2008 19:00:00 -0500 python-crontab (0.5) hardy; urgency=low * Package created as base module for other packages. -- Martin Owens (DoctorMO) Thu, 12 Aug 2008 00:15:00 -0500 python-crontab-1.9.3/cronlog.py0000664000175000017500000000655712477464202020235 0ustar doctormodoctormo00000000000000# # Copyright 2013, Martin Owens # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import re import sys import string import codecs import platform py3 = platform.python_version()[0] == '3' if py3: unicode = str from dateutil import parser as dateparse MATCHER = r'(?P\w+ +\d+ +\d\d:\d\d:\d\d) (?P\w+) ' + \ r'CRON\[(?P\d+)\]: \((?P\w+)\) CMD \((?P.*)\)' def size(filename): return os.stat(filename)[6] class LogReader(object): """Opens a Log file, reading backwards and watching for changes""" def __init__(self, filename, mass=4096): self.filename = filename self.pipe = codecs.open(filename, 'r', encoding='utf-8') self.mass = mass self.size = -1 self.read = -1 def readlines(self, until=0): """Iterator for reading lines from a file backwards""" if not self.pipe or self.pipe.closed: raise IOError("Can't readline, no opened file.") # Always seek to the end of the file, this accounts for file updates # that happen during our running process. self.size = size(self.filename) block_num = 0 location = self.size halfline = '' while location > until: location -= self.mass mass = self.mass if location < 0: mass = self.mass + location location = 0 self.pipe.seek(location) line = self.pipe.read(mass) + halfline data = line.split('\n') if location != 0: halfline = data.pop(0) loc = location + mass data.reverse() for line in data: if line.strip() == '': continue yield (loc, line) loc -= len(line) def __iter__(self): for (offset, line) in self.readlines(): yield line class CronLog(LogReader): def __init__(self, filename='/var/log/syslog', user=None): LogReader.__init__(self, filename) self.user = user def for_program(self, command): return ProgramLog(self, command) def __iter__(self): for (offset, line) in self.readlines(): c = re.match(MATCHER, unicode(line)) datum = c and c.groupdict() if datum and (not self.user or datum['user'] == self.user): datum['date'] = dateparse.parse(datum['date']) yield datum class ProgramLog(object): """Specific log control for a single command/program""" def __init__(self, log, command): self.log = log self.command = command def __iter__(self): for entry in self.log: if entry['cmd'] == unicode(self.command): yield entry python-crontab-1.9.3/README0000664000175000017500000002025112452562340017056 0ustar doctormodoctormo00000000000000.. image:: https://launchpadlibrarian.net/134092458/python-cron-192.png :class: floating-box :alt: Python Crontab Logo Bug Reports and Development =========================== Please report any problems to the `launchpad bug tracker `_. Please use Bazaar and push patches to the `launchpad project code hosting `_. **Note:** If you get the error ``TypeError: __init__() takes exactly 2 arguments`` when using CronTab, you have the wrong module installed. You need to install ``python-crontab`` and not ``crontab`` from pypi or your local package manager and try again. Description =========== Crontab module for read and writing crontab files and accessing the system cron automatically and simply using a direct API. Comparing the `below chart `_ you will note that W, L, # and ? symbols are not supported as they are not standard Linux or SystemV crontab format. ============= =========== ================= =================== ============= Field Name Mandatory Allowed Values Special Characters Extra Values ============= =========== ================= =================== ============= Minutes Yes 0-59 \* / , - < > Hours Yes 0-23 \* / , - < > Day of month Yes 1-31 \* / , - < > Month Yes 1-12 or JAN-DEC \* / , - < > Day of week Yes 0-6 or SUN-SAT \* / , - < > ============= =========== ================= =================== ============= Extra Values are '<' for minimum value, such as 0 for minutes or 1 for months. And '>' for maximum value, such as 23 for hours or 12 for months. Supported special cases allow crontab lines to not use fields. These are the supported aliases which are not available in SystemV mode: =========== =========== Case Meaning =========== =========== @reboot Every boot @hourly 0 * * * * @daily 0 0 * * * @weekly 0 0 * * 0 @monthly 0 0 1 * * @yearly 0 0 1 1 * @annually 0 0 1 1 * @midnight 0 0 * * * =========== =========== How to Use the Module ===================== Getting access to a crontab can happen in five ways, three system methods that will work only on Unix and require you to have the right permissions:: from crontab import CronTab empty_cron = CronTab() my_user_cron = CronTab(user=True) users_cron = CronTab(user='username') And two ways from non-system sources that will work on Windows too:: file_cron = CronTab(tabfile='filename.tab') mem_cron = CronTab(tab=""" * * * * * command """) Special per-command user flag for vixie cron format (new in 1.9):: system_cron = CronTab(tabfile='/etc/crontab', user=False) job = system_cron[0] job.user != None system_cron.new(command='new_command', user='root') Creating a new job is as simple as:: job = cron.new(command='/usr/bin/echo') And setting the job's time restrictions:: job.minute.during(5,50).every(5) job.hour.every(4) job.day.on(4, 5, 6) job.dow.on('SUN') job.month.during('APR', 'NOV') Each time restriction will clear the previous restriction:: job.hour.every(10) # Set to * */10 * * * job.hour.on(2) # Set to * 2 * * * Appending restrictions is explicit:: job.hour.every(10) # Set to * */10 * * * job.hour.also.on(2) # Set to * 2,*/10 * * * Setting all time slices at once:: job.setall(2, 10, '2-4', '*/2', None) job.setall('2 10 * * *') Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Get the comment or command for a job:: command = job.command comment = job.comment Modify the comment or command on a job:: job.set_command("new_script.sh") job.set_comment("New ID or comment here") Disabled or Enable Job:: job.enable() job.enable(False) False == job.is_enabled() Validity Check:: True == job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command:: iter = cron.find_command('bar') Find an existing job by comment:: iter = cron.find_comment('ID or some text') Find an existing job by schedule:: iter = cron.find_time(2, 10, '2-4', '*/2', None) iter = cron.find_time("*/2 * * * *") Clean a job of all rules:: job.clear() Iterate through all jobs:: for job in cron: print job Iterate through all lines:: for line in cron.lines: print line Remove Items:: cron.remove( job ) cron.remove_all('echo') cron.remove_all(comment='foo') cron.remove_all(time='*/2') Clear entire cron of all jobs:: cron.remove_all() Write CronTab back to system or filename:: cron.write() Write CronTab to new filename:: cron.write( 'output.tab' ) Write to this user's crontab (unix only):: cron.write_to_user( user=True ) Write to some other user's crontab:: cron.write_to_user( user='bob' ) Proceeding Unit Confusion ========================= It is sometimes logical to think that job.hour.every(2) will set all proceeding units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling only the hours units and the minute column is unaffected. The real result would be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs. There is a special 'every' method on a job to clear the job's existing schedule and replace it with a simple single unit:: job.every(4).hours() == '0 */4 * * *' job.every().dom() == '0 0 * * *' job.every().month() == '0 0 0 * *' job.every(2).dows() == '0 0 * * */2' This is a convenience method only, it does normal things with the existing api. Frequency Calculation ===================== Every job's schedule has a frequency. We can attempt to calculate the number of times a job would execute in a give amount of time. We have three simple methods:: job.setall("1,2 1,2 * * *") job.frequency_per_day() == 4 The per year frequency method will tell you how many days a year the job would execute:: job.setall("* * 1,2 1,2 *") job.frequency_per_year(year=2010) == 4 These are combined to give the number of times a job will execute in any year:: job.setall("1,2 1,2 1,2 1,2 *") job.frequency(year=2010) == 16 Frequency can be quickly checked using python built-in operators:: job < "*/2 * * * *" job > job2 job.slices == "*/5" Log Functionality ================= The log functionality will read a cron log backwards to find you the last run instances of your crontab and cron jobs. The crontab will limit the returned entries to the user the crontab is for:: cron = CronTab(user='root') for d in cron.log: print d['pid'] + " - " + d['date'] Each job can return a log iterator too, these are filtered so you can see when the last execution was:: for d in cron.find_command('echo')[0].log: print d['pid'] + " - " + d['date'] Schedule Functionality ====================== If you have the croniter python module installed, you will have access to a schedule on each job. For example if you want to know when a job will next run:: schedule = job.schedule(date_from=datetime.now()) This creates a schedule croniter based on the job from the time specified. The default date_from is the current date/time if not specified. Next we can get the datetime of the next job:: datetime = schedule.get_next() Or the previous:: datetime = schedule.get_prev() The get methods work in the same way as the default croniter, except that they will return datetime objects by default instead of floats. If you want the original functionality, pass float into the method when calling:: datetime = schedule.get_current(float) If you don't have the croniter module installed, you'll get an ImportError when you first try using the schedule function on your cron job object. Extra Support ============= - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - Python 3.4 and Python 2.7/2.6 tested. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) python-crontab-1.9.3/AUTHORS0000664000175000017500000000004212344072461017242 0ustar doctormodoctormo00000000000000Martin Owens (doctormo@gmail.com) python-crontab-1.9.3/tests/0000775000175000017500000000000012504000222017320 5ustar doctormodoctormo00000000000000python-crontab-1.9.3/tests/test_compatibility.py0000664000175000017500000000467412477225056023644 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2012 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab interaction. """ import os import sys import unittest import crontab try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) INITAL_TAB = """ # First Comment */30 * * * * firstcommand """ class CompatTestCase(unittest.TestCase): """Test basic functionality of crontab.""" @classmethod def setUpClass(cls): crontab.SYSTEMV = True @classmethod def tearDownClass(cls): crontab.SYSTEMV = False def setUp(self): self.crontab = crontab.CronTab(tab=INITAL_TAB) def test_00_enabled(self): """Test Compatability Mode""" self.assertTrue(crontab.SYSTEMV) def test_01_addition(self): """New Job Rendering""" job = self.crontab.new('addition1') job.minute.during(0, 3) job.hour.during(21, 23).every(1) job.dom.every(1) self.assertEqual(job.render(), '0,1,2,3 21,22,23 * * * addition1') def test_02_addition(self): """New Job Rendering""" job = self.crontab.new(command='addition2') job.minute.during(4, 9) job.hour.during(2, 10).every(2) job.dom.every(10) self.assertNotEqual(job.render(), '4-9 2-10/2 */3 * * addition2') self.assertEqual(job.render(), '4,5,6,7,8,9 2,4,6,8,10 1,11,21,31 * * addition2') def test_03_specials(self): """Ignore Special Symbols""" tab = crontab.CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) self.assertEqual(tab.render(), """0 * * * * hourly 0 0 * * * daily 0 0 * * 0 weekly """) if __name__ == '__main__': test_support.run_unittest( CompatTestCase, ) python-crontab-1.9.3/tests/test_interaction.py0000664000175000017500000002424712477225453023311 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab interaction. """ import os import sys import unittest from crontab import CronTab, CronSlices, CronSlice, PY3 try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) if PY3: unicode = str COMMANDS = [ 'firstcommand', 'range', 'byweek', 'disabled', 'spaced', 'rebooted', ] RESULT_TAB = """# First Comment # Edit this line to test for mistaken checks # m h dom mon dow user command */30 * * * * firstcommand * 10-20/3 * * * range # Middle Comment * * * 10 * byweek # Comment One # * * * * * disabled 0 5 * * * spaced # Comment Two @reboot rebooted # re-id # Last Comment @has this # extra """ sys.stderr = open('/dev/null', 'w') class InteractionTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab')) def test_01_presevation(self): """All Entries Re-Rendered Correctly""" results = RESULT_TAB.split('\n') line_no = 0 for line in self.crontab.lines: self.assertEqual(str(line), results[line_no]) line_no += 1 def test_02_access(self): """All Entries Are Accessable""" line_no = 0 for job in self.crontab: self.assertEqual(str(job.command), COMMANDS[line_no]) line_no += 1 self.assertEqual(line_no, 6) def test_03_blank(self): """Render Blank""" job = self.crontab.new(command='blank') self.assertEqual(repr(job), '') self.assertEqual(job.render(), '* * * * * blank') def test_04_number(self): """Render Number""" job = self.crontab.new(command='number') job.minute.on(4) self.assertEqual(job.render(), '4 * * * * number') def test_05_fields(self): """Render Hours Days and Weeks""" job = self.crontab.new(command='fields') job.hour.on(4) self.assertEqual(job.render(), '* 4 * * * fields') job.dom.on(5) self.assertEqual(job.render(), '* 4 5 * * fields') job.month.on(6) self.assertEqual(job.render(), '* 4 5 6 * fields') job.dow.on(7) self.assertEqual(job.render(), '* 4 5 6 0 fields') def test_06_clear(self): """Render Hours Days and Weeks""" job = self.crontab.new(command='clear') job.minute.on(3) job.hour.on(4) job.dom.on(5) job.month.on(6) job.dow.on(7) self.assertEqual(job.render(), '3 4 5 6 0 clear') job.clear() self.assertEqual(job.render(), '* * * * * clear') def test_07_range(self): """Render Time Ranges""" job = self.crontab.new(command='range') job.minute.during(4,10) self.assertEqual(job.render(), '4-10 * * * * range') job.minute.during(15,19) self.assertEqual(job.render(), '15-19 * * * * range') job.minute.clear() self.assertEqual(job.render(), '* * * * * range') job.minute.during(15,19) self.assertEqual(job.render(), '15-19 * * * * range') job.minute.also.during(4,10) self.assertEqual(job.render(), '4-10,15-19 * * * * range') def test_08_sequence(self): """Render Time Sequences""" job = self.crontab.new(command='seq') job.hour.every(4) self.assertEqual(job.render(), '* */4 * * * seq') job.hour.during(2, 10) self.assertEqual(job.render(), '* 2-10 * * * seq') job.hour.clear() self.assertEqual(job.render(), '* * * * * seq') job.hour.during(2, 10).every(4) self.assertEqual(job.render(), '* 2-10/4 * * * seq') job.hour.also.during(1, 4) self.assertEqual(job.render(), '* 1-4,2-10/4 * * * seq') job.hour.also.every(4) self.assertEqual(job.render(), '* */4,1-4,2-10/4 * * * seq') def test_10_comment(self): """Render cron Comments""" job = self.crontab.new(command='com', comment='I love this') self.assertEqual(unicode(job), '* * * * * com # I love this') def test_11_disabled(self): """Disabled Job""" jobs = list(self.crontab.find_command('firstcommand')) self.assertTrue(jobs[0].enabled) jobs = list(self.crontab.find_command('disabled')) self.assertFalse(jobs[0].enabled) def test_12_disable(self): """Disable and Enable Job""" job = self.crontab.new(command='dis') job.enable(False) self.assertEqual(unicode(job), '# * * * * * dis') job.enable() self.assertEqual(unicode(job), '* * * * * dis') def test_13_plural(self): """Plural API""" job = self.crontab.new(command='plural') job.minutes.every(4) job.hours.on(5,6) job.day.on(4) job.months.on(2) self.assertEqual(unicode(job), '*/4 5,6 4 2 * plural') def test_14_valid(self): """Valid and Invalid""" job = self.crontab.new(command='valid') job.minute.every(2) job.valid = False with self.assertRaises(ValueError): unicode(job) def test_15_slices(self): """Invalid Slices""" mon = CronSlices('* * * * *') with self.assertRaises(ValueError): CronSlices('* * * */15 *') with self.assertRaises(AssertionError): mon.setall(mon) def test_16_slice(self): """Single Slice""" dow = CronSlice({'name': 'M', 'max': 7, 'min': 0, 'enum': ['a']}, '*/6') self.assertEqual(repr(dow), '') self.assertEqual(repr(dow._v('a')), 'a') with self.assertRaises(ValueError): dow._v('b') self.assertEqual(dow.get_range().render(), '*') with self.assertRaises(ValueError): dow.get_range('%') def test_17_range_cmp(self): """Compare ranges""" dow = CronSlice({'max': 5, 'min': 0}) three = dow.get_range(2, 4) self.assertGreater(three, 2) self.assertLess(three, 4) self.assertEqual(str(three), '2-4') def test_20_write(self): """Write CronTab to file""" self.crontab.write('output.tab') self.assertTrue(os.path.exists('output.tab')) os.unlink('output.tab') def test_21_multiuse(self): """Multiple Renderings""" cron = '# start of tab' for i in range(10): crontab = CronTab(tab=cron) job = list(crontab.new(command='multi')) cron = unicode(crontab) crontab = CronTab(tab=cron) list(crontab.find_command('multi'))[0].delete() cron = unicode(crontab) self.assertEqual(unicode(crontab), '# start of tab\n') def test_22_min(self): """Minimum Field Values""" job = self.crontab.new(command='min') job.minute.on('<') job.hour.on('<') job.dom.on('<') job.month.on('<') self.assertEqual(unicode(job), '@yearly min') def test_23_max(self): """Maximum Field Values""" job = self.crontab.new(command='max') job.minute.on('>') job.hour.on('>') job.dom.on('>') job.month.on('>') self.assertEqual(unicode(job), '59 23 31 12 * max') def test_24_special_r(self): """Read Specials""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials_enc.tab')) self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@daily midnight\n@weekly weekly\n@reboot reboot\n""") self.assertEqual(len(list(tab)), 5) def test_24_special_d(self): """Removal All Specials""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) tab.remove_all() self.assertEqual(len(list(tab)), 0) def test_24_special_w(self): """Write Specials""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@weekly weekly\n""") self.assertEqual(len(list(tab)), 3) def test_25_setall(self): """Set all values at once""" job = self.crontab.new(command='all') job.setall(1, '*/2', '2-4', '>', 'SUN') self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all') job.setall('*/2') self.assertEqual(unicode(job), '*/2 * * * * all') job.setall('1 */2 2-4 12 SUN') self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all') job.setall(['*']) self.assertEqual(unicode(job), '* * * * * all') def test_26_setall_obj(self): """Copy all values""" job = self.crontab.new(command='all') job2 = self.crontab.new(command='ignore') job2.setall("1 */2 2-4 12 SUN") job.setall(job2) self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all') job2.setall("2 */3 4-8 10 MON") job.setall(job2.slices) self.assertEqual(unicode(job), '2 */3 4-8 10 MON all') def test_27_commands(self): """Get all commands""" self.assertEqual(list(self.crontab.commands), [u'firstcommand', u'range', u'byweek', u'disabled', u'spaced', u'rebooted']) def test_28_comments(self): """Get all comments""" self.assertEqual(list(self.crontab.comments), ['Comment One', 'Comment Two', 're-id']) if __name__ == '__main__': test_support.run_unittest( InteractionTestCase, ) python-crontab-1.9.3/tests/data/0000775000175000017500000000000012504000222020231 5ustar doctormodoctormo00000000000000python-crontab-1.9.3/tests/data/basic.log0000664000175000017500000000015412477165645022053 0ustar doctormodoctormo00000000000000A really long line which can test log lines ability to put two bits together First Line 9 2 Sickem The End python-crontab-1.9.3/tests/data/test.tab0000664000175000017500000000047612464505670021735 0ustar doctormodoctormo00000000000000# First Comment # Edit this line to test for mistaken checks # m h dom mon dow user command */30 * * * * firstcommand * 10-20/3 * * * range # Middle Comment * * * 10 * byweek # Comment One # * * * * * disabled 00 5 * * * spaced # Comment Two @reboot rebooted # re-id # Last Comment @has this # extra python-crontab-1.9.3/tests/data/specials.tab0000664000175000017500000000006212344072461022542 0ustar doctormodoctormo000000000000000 * * * * hourly 0 0 * * * daily 0 0 * * 0 weekly python-crontab-1.9.3/tests/data/user.tab0000664000175000017500000000005212344072461021714 0ustar doctormodoctormo00000000000000 */4 * * * * user_command # user_comment python-crontab-1.9.3/tests/data/specials_enc.tab0000664000175000017500000000011512344072461023366 0ustar doctormodoctormo00000000000000@hourly hourly @daily daily @midnight midnight @weekly weekly @reboot reboot python-crontab-1.9.3/tests/data/basic.tab0000664000175000017500000000002712344072461022021 0ustar doctormodoctormo000000000000000 * * * * firstcommand python-crontab-1.9.3/tests/data/test.log0000664000175000017500000000455112344072461021740 0ustar doctormodoctormo00000000000000Apr 4 21:24:01 servername CRON[16490]: (user) CMD (userscript &> /dev/null) Apr 4 21:24:01 servername CRON[16489]: (root) CMD (rootscript &> /dev/null) Apr 4 21:24:01 servername CRON[16487]: (CRON) info (Cron information item) Apr 4 21:25:01 servername CRON[16496]: (user) CMD (userscript &> /dev/null) Apr 4 21:25:01 servername CRON[16497]: (root) CMD (shadowscript &> /dev/null) Apr 4 21:25:01 servername CRON[16494]: (CRON) info (Cron information item) Apr 4 21:26:01 servername CRON[16513]: (user) CMD (userscript &> /dev/null) Apr 4 21:26:01 servername CRON[16511]: (CRON) info (Cron information item) Apr 4 21:26:01 servername CRON[16514]: (root) CMD (rootscript &> /dev/null) Apr 4 21:27:01 servername CRON[16519]: (user) CMD (userscript &> /dev/null) Apr 4 21:27:01 servername CRON[16518]: (CRON) info (Cron information item) Apr 4 21:28:01 servername CRON[16523]: (user) CMD (userscript &> /dev/null) Apr 4 21:28:01 servername CRON[16522]: (root) CMD (rootscript &> /dev/null) Apr 4 21:28:01 servername CRON[16520]: (CRON) info (Cron information item) Apr 4 21:28:30 servername NOTCRON: Log entry that isn't a cron Apr 4 21:28:30 servername NOTCRON: entry in order to test Apr 4 21:28:31 servername NOTCRON: that these are ignored Apr 4 21:29:01 servername CRON[16539]: (user) CMD (userscript &> /dev/null) Apr 4 21:29:01 servername CRON[16538]: (CRON) info (Cron information item) Apr 4 21:30:01 servername CRON[16551]: (root) CMD (shadowscript &> /dev/null) Apr 4 21:30:01 servername CRON[16552]: (root) CMD (rootscript &> /dev/null) Apr 4 21:30:01 servername CRON[16554]: (user) CMD (userscript &> /dev/null) Apr 4 21:30:01 servername CRON[16548]: (CRON) info (Cron information item) Apr 4 21:31:01 servername CRON[16569]: (user) CMD (userscript &> /dev/null) Apr 4 21:31:01 servername CRON[16568]: (CRON) info (Cron information item) Apr 4 21:32:01 servername CRON[16573]: (user) CMD (userscript &> /dev/null) Apr 4 21:32:01 servername CRON[16574]: (root) CMD (rootscript &> /dev/null) Apr 4 21:32:01 servername CRON[16571]: (CRON) info (Cron information item) Apr 4 21:33:02 servername CRON[16588]: (user) CMD (userscript &> /dev/null) Apr 4 21:33:02 servername CRON[16587]: (CRON) info (Cron information item) Apr 4 21:34:01 servername CRON[16591]: (user) CMD (userscript &> /dev/null) Apr 4 21:34:01 servername CRON[16592]: (root) CMD (rootscript &> /dev/null) python-crontab-1.9.3/tests/data/crontest0000755000175000017500000000174212477226234022050 0ustar doctormodoctormo00000000000000#!/usr/bin/env python import os import sys args = sys.argv[1:] loc = os.path.dirname(__file__) user = 'user' if '-u' in args: user = args[args.index('-u')+1] if '-l' in args: if user == 'error': raise ValueError("Delibrate IO Error") if not os.path.exists(os.path.join(loc, '%s.tab' % user)): sys.stderr.write("no crontab for %s\n" % user) sys.exit(1) fhl = open(os.path.join(loc, '%s.tab' % user), 'r') print(fhl.read()) else: for filename in args: if filename[0] == '-' or filename == user: continue new_name = os.path.join(loc, '%s.tab' % user) if not os.path.exists(filename): raise KeyError("Can't find file: %s" % filename) if os.path.exists(new_name): raise KeyError("Can't write file: %s, already exists" % new_name) with open(filename, 'r') as source: with open(new_name, 'w') as output: output.write(source.read()) sys.exit(0) python-crontab-1.9.3/tests/test_croniter.py0000664000175000017500000000550412477222611022603 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test croniter extention to find out when items will next happen. """ import os import sys from datetime import datetime sys.path.insert(0, '../') import unittest import crontab try: from test import test_support except ImportError: from test import support as test_support INITAL_TAB = """ # Basic Comment 20 * * * * execute # comment """ class CroniterTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = crontab.CronTab(tab=INITAL_TAB) self.job = list(self.crontab.find_command('execute'))[0] try: import croniter except ImportError: self.skipTest("Croniter not installed") def test_00_nocroniter(self): """No Croniter""" # Remove croniter if importer before. sys.modules.pop('croniter', None) old, sys.path = sys.path, [] with self.assertRaises(ImportError): self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) sys.path = old def test_01_schedule(self): """Get Scheduler""" ct = self.job.schedule(datetime(2009, 10, 11, 5, 12, 10)) self.assertTrue(ct) def test_02_next(self): """Get Next Scheduled Items""" ct = self.job.schedule(datetime(2000, 10, 11, 5, 12, 10)) self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 5, 20, 0)) self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 6, 20, 0)) def test_03_prev(self): """Get Prev Scheduled Items""" ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_prev(), datetime(2001, 10, 11, 0, 20, 0)) self.assertEqual(ct.get_prev(), datetime(2001, 10, 10, 23, 20, 0)) def test_04_current(self): """Get Current Item""" ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10)) if __name__ == '__main__': test_support.run_unittest( CroniterTestCase, ) python-crontab-1.9.3/tests/test_frequency.py0000664000175000017500000001224012477161432022755 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redilenibute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is dilenibuted in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test frequency calculations """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab, PY3 try: from test import test_support except ImportError: from test import support as test_support if PY3: unicode = str START_TAB = """ """ class FrequencyTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=START_TAB.strip()) self.job = self.crontab.new(command='freq') def test_00_slice_frequency(self): """Each Slice Frequency""" # Make sure some of these overlap self.job.setall("13-16,14-15 6,*/3 2,3,4 * *") self.assertEqual(len(self.job[0]), 4) self.assertEqual(len(self.job[1]), 8) self.assertEqual(len(self.job[2]), 3) self.assertEqual(len(self.job[3]), 12) self.assertEqual(len(self.job[4]), 7) def test_01_per_day(self): """Frequency per Day""" self.job.setall("21-29 10-14 * * *") self.assertEqual(len(self.job[0]), 9) self.assertEqual(len(self.job[1]), 5) self.assertEqual(self.job.frequency_per_day(), 45) def test_02_days_per_year(self): """Frequency in Days per Year""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency_per_year(year=2012), 366) self.job.setall("1 1 11-20 1,4,6,8 0,6") self.assertEqual(len(self.job[2]), 10) self.assertEqual(len(self.job[3]), 4) self.assertEqual(len(self.job[4]), 2) self.assertEqual(self.job.frequency_per_year(year=2013), 11) self.assertEqual(self.job.frequency_per_year(year=2010), 12) def test_03_job(self): """Once Yearly""" self.job.setall("0 0 1 1 *") self.assertEqual(self.job.frequency(year=2010), 1) def test_04_twice(self): """Twice Yearly""" self.job.setall("0 0 1 1,6 *") self.assertEqual(self.job.frequency(year=2010), 2) def test_05_thrice(self): """Thrice Yearly""" self.job.setall("0 0 1 1,3,6 *") self.assertEqual(self.job.frequency(year=2010), 3) def test_06_quart(self): """Four Yearly""" self.job.setall("0 0 1 */3 *") self.assertEqual(self.job.frequency(year=2010), 4) def test_07_monthly(self): """Once a month""" self.job.setall("0 0 1 * *") self.assertEqual(self.job.frequency(year=2010), 12) def test_08_six_monthly(self): """Six a month""" self.job.setall("0 0 1,2,3,4,5,6 * *") self.assertEqual(self.job.frequency(year=2010), 72) def test_09_every_day(self): """Every Day""" self.job.setall("0 0 * * *") self.assertEqual(self.job.frequency(year=2010), 365) def test_10_every_hour(self): """Every Hour""" self.job.setall("0 * * * *") self.assertEqual(self.job.frequency(year=2010), 8760) def test_11_every_other_hour(self): """Every Other Hour""" self.job.setall("0 */2 * * *") self.assertEqual(self.job.frequency(year=2010), 4380) def test_12_every_minute(self): """Every Minute""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency(year=2010), 525600) def test_13_enum(self): """Enumerations""" self.job.setall("0 0 * * MON-WED") self.assertEqual(self.job.frequency(year=2010), 156) self.job.setall("0 0 * JAN-MAR *") self.assertEqual(self.job.frequency(year=2010), 90) def test_14_all(self): """Test Maximum""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency(), 525600) self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency_per_day(), 1440) self.job.setall("*") self.assertEqual(self.job.frequency_per_day(), 1440) self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency(), 525600) def test_15_compare(self): """Compare Times""" job = self.crontab.new(command='match') job.setall("*/2 * * * *") self.assertEqual( job.slices, "*/2 * * * *" ) self.assertEqual( job.slices, ["*/2"] ) self.assertLess( job, ["*"] ) self.assertGreater( job, "*/3" ) if __name__ == '__main__': test_support.run_unittest( FrequencyTestCase, ) python-crontab-1.9.3/tests/__init__.py0000664000175000017500000000000012477224330021440 0ustar doctormodoctormo00000000000000python-crontab-1.9.3/tests/test_utf8.py0000664000175000017500000000510212477225166021646 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2014 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab use of UTF-8 filenames and strings """ import os import sys import unittest from crontab import CronTab, PY3 try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) if PY3: unicode = str content = """ */4 * * * * ůțƒ_command # ůțƒ_comment """ filename = os.path.join(TEST_DIR, 'data', 'output-ůțƒ-8.tab') class Utf8TestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=content) def test_01_input(self): """Read UTF-8 contents""" self.assertTrue(self.crontab) def test_02_write(self): """Write/Read UTF-8 Filename""" self.crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertTrue(crontab) self.assertEqual(content, open(filename, "r").read()) os.unlink(filename) def test_04_command(self): """Read Command String""" self.assertEqual(self.crontab[0].command, u"ůțƒ_command") def test_05_comment(self): """Read Comment String""" self.assertEqual(self.crontab[0].comment, u'ůțƒ_comment') def test_06_unicode(self): """Write New via Unicode""" c = self.crontab.new(command=u"ůțƒ_command", comment=u'ůțƒ_comment') self.assertEqual(c.command, u"ůțƒ_command") self.assertEqual(c.comment, u"ůțƒ_comment") self.crontab.render() def test_07_utf8(self): """Write New via UTF-8""" c = self.crontab.new(command='\xc5\xaf\xc8\x9b\xc6\x92_command', comment='\xc5\xaf\xc8\x9b\xc6\x92_comment') self.crontab.render() if __name__ == '__main__': test_support.run_unittest( Utf8TestCase, ) python-crontab-1.9.3/tests/test_log.py0000664000175000017500000001235512477225342021545 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) YEAR Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test the cron log extention with a test syslog example data file. """ import os import sys from datetime import datetime, date import unittest from crontab import CronTab from cronlog import CronLog, LogReader try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) INITAL_TAB = """ * * * * * userscript &> /dev/null * * * * * rootscript &> /dev/null * * * * * shadowscript &> /dev/null """ ROOT_PIDS = ['16592', '16574', '16552', '16522', '16514', '16489'] SHAD_PIDS = ['16551', '16497'] USER_PIDS = ['16591', '16588', '16573', '16569', '16554', '16539', '16523',\ '16519','16513','16496','16490'] YEAR = date.today().year ROOT_DATES = [ datetime(YEAR, 4, 4, 21, 34, 1), datetime(YEAR, 4, 4, 21, 32, 1), datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 28, 1), datetime(YEAR, 4, 4, 21, 26, 1), datetime(YEAR, 4, 4, 21, 24, 1), ] SHAD_DATES = [ datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 25, 1), ] USER_DATES = [ datetime(YEAR, 4, 4, 21, 34, 1), datetime(YEAR, 4, 4, 21, 33, 2), datetime(YEAR, 4, 4, 21, 32, 1), datetime(YEAR, 4, 4, 21, 31, 1), datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 29, 1), datetime(YEAR, 4, 4, 21, 28, 1), datetime(YEAR, 4, 4, 21, 27, 1), datetime(YEAR, 4, 4, 21, 26, 1), datetime(YEAR, 4, 4, 21, 25, 1), datetime(YEAR, 4, 4, 21, 24, 1), ] LONG_LINE = 'A really long line which can test log lines ability to put two bits together' READ_LINE = [ 'The End', 'Sickem', '2', '9', 'First Line', LONG_LINE ] class BasicTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB, log=os.path.join(TEST_DIR, 'data', 'test.log')) def test_00_logreader(self): """Log Reader""" lines = READ_LINE[:] reader = LogReader(os.path.join(TEST_DIR, 'data', 'basic.log'), mass=50) for line in reader: self.assertEqual(line.strip(), lines.pop(0)) def test_01_cronreader(self): """Cron Log Lines""" lines = list(self.crontab.log.readlines()) self.assertEqual(len(lines), 32) self.assertEqual(lines[0][1], "Apr 4 21:34:01 servername CRON[16592]: (root) CMD (rootscript &> /dev/null)") self.assertEqual(lines[15][1], "Apr 4 21:28:31 servername NOTCRON: that these are ignored") self.assertEqual(lines[-1][1], "Apr 4 21:24:01 servername CRON[16490]: (user) CMD (userscript &> /dev/null)") def test_02_cronlog(self): """Cron Log Items""" entries = list(CronLog(os.path.join(TEST_DIR, 'data', 'test.log'))) self.assertEqual(len(entries), 19) self.assertEqual(entries[0]['pid'], "16592") self.assertEqual(entries[3]['pid'], "16574") self.assertEqual(entries[-1]['pid'], "16490") def test_03_crontab(self): """Cron Tab Items""" entries = list(self.crontab.log) self.assertEqual(len(entries), 8) self.assertEqual(entries[0]['pid'], "16592") self.assertEqual(entries[3]['pid'], "16551") self.assertEqual(entries[-1]['pid'], "16489") def test_04_root(self): """Cron Job Items""" pids, dates = ROOT_PIDS[:], ROOT_DATES[:] job = list(self.crontab.find_command('rootscript'))[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def tst_05_shadow(self): """Seperate Job Items""" pids, dates = SHAD_PIDS[:], SHAD_DATES[:] job = self.crontab.find_command('shadowscript')[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def tst_06_user(self): """Seperate User Crontab""" pids, dates = USER_PIDS[:], User_DATES[:] self.crontab.user = 'user' job = self.crontab.find_command('userscript')[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def test_08_readerror(self): """Cron Log Error""" self.crontab.log.pipe.close() with self.assertRaises(IOError): list(self.crontab.log.readlines()) if __name__ == '__main__': test_support.run_unittest( BasicTestCase, ) python-crontab-1.9.3/tests/test_range.py0000664000175000017500000000504312477161457022062 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab ranges. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab, PY3 try: from test import test_support except ImportError: from test import support as test_support if PY3: unicode = str class RangeTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab="") def test_01_atevery(self): """At Every""" tab = CronTab(tab=""" * * * * * command 61 * * * * command * 25 * * * command * * 32 * * command * * * 13 * command * * * * 8 command """) self.assertEqual(len(tab), 1) def test_02_withinevery(self): """Within Every""" tab = CronTab(tab=""" * * * * * command 1-61 * * * * command * 1-25 * * * command * * 1-32 * * command * * * 1-13 * command * * * * 1-8 command """) self.assertEqual(len(tab), 1) def test_03_outevery(self): """Out of Every""" tab = CronTab(tab=""" * * * * * command */61 * * * * command * */25 * * * command * * */32 * * command * * * */13 * command * * * * */8 command """) self.assertEqual(len(tab), 1) def test_04_zero_seq(self): tab = CronTab(tab=""" */0 * * * * command """) self.assertEqual(len(tab), 0) def test_05_sunday(self): tab = CronTab(tab=""" * * * * 7 command * * * * 5-7 command * * * * */7 command """) self.assertEqual(len(tab), 2) self.assertEqual(tab.render(), """ * * * * 0 command * * * * 5-6 command * * * * */7 command """) if __name__ == '__main__': test_support.run_unittest( RangeTestCase, ) python-crontab-1.9.3/tests/test_system_cron.py0000664000175000017500000000602012504000047023301 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2015 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ System cron is prefixed with the username the process should run under. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab try: from test import test_support except ImportError: from test import support as test_support INITAL_TAB = """ */30 * * * * palin one_cross_each """ class SystemCronTestCase(unittest.TestCase): """Test vixie cron user addition.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB, user=False) def test_01_read(self): """Read existing command""" jobs = 0 for job in self.crontab: self.assertEqual(job.user, 'palin') self.assertEqual(job.command, 'one_cross_each') jobs += 1 self.assertEqual(jobs, 1) def test_02_new(self): """Create a new job""" job = self.crontab.new(command='release_brian', user='pontus') self.assertEqual(job.user, 'pontus') self.assertEqual(job.command, 'release_brian') self.assertEqual(str(self.crontab), """ */30 * * * * palin one_cross_each * * * * * pontus release_brian """) def test_03_failure(self): """Fail when no user""" with self.assertRaises(ValueError): self.crontab.new(command='im_brian') cron = self.crontab.new(user='user', command='no_im_brian') cron.user = None with self.assertRaises(ValueError): cron.render() def test_04_remove(self): """Remove the user flag""" self.crontab._user = None self.assertEqual(str(self.crontab), """ */30 * * * * one_cross_each """) self.crontab.new(command='now_go_away') def test_05_comments(self): """Comment with six parts parses successfully""" crontab = CronTab(user=False, tab=""" #a system_comment that has six parts_will_fail_to_parse """) def test_06_recreation(self): """Input doesn't change on save""" crontab = CronTab(user=False, tab="* * * * * user command") self.assertEqual(str(crontab), "* * * * * user command\n") crontab = CronTab(user=False, tab="* * * * * user command\n") self.assertEqual(str(crontab), "* * * * * user command\n") if __name__ == '__main__': test_support.run_unittest( SystemCronTestCase, ) python-crontab-1.9.3/tests/test_removal.py0000664000175000017500000000520012477161473022424 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redilenibute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is dilenibuted in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test cron item removal """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab, PY3 try: from test import test_support except ImportError: from test import support as test_support if PY3: unicode = str START_TAB = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 """ class RemovalTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=START_TAB.strip()) def test_01_remove(self): """Remove Item""" self.assertEqual(len(self.crontab), 3) self.crontab.remove( self.crontab.crons[0] ) self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 69) def test_02_remove_all(self): """Remove All""" self.crontab.remove_all() self.assertEqual(len(self.crontab), 0) self.assertEqual(unicode(self.crontab), '') def test_03_remove_cmd(self): """Remove all with Command""" self.crontab.remove_all('command2') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) self.crontab.remove_all('command3') self.assertEqual(len(self.crontab), 1) self.assertEqual(len(self.crontab.render()), 33) def test_04_remove_id(self): """Remove all with Comment/ID""" self.crontab.remove_all(comment='CommentID B3') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 68) def test_05_remove_date(self): """Remove all with Time Code""" self.crontab.remove_all(time='2 * * * *') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) if __name__ == '__main__': test_support.run_unittest( RemovalTestCase, ) python-crontab-1.9.3/tests/test_every.py0000664000175000017500000000601412477225611022110 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test simple 'every' api. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab, PY3 try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) if PY3: unicode = str class EveryTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab')) def test_00_minutes(self): """Every Minutes""" for job in self.crontab: job.every(3).minutes() self.assertEqual(job.slices.clean_render(), '*/3 * * * *') def test_01_hours(self): """Every Hours""" for job in self.crontab: job.every(3).hours() self.assertEqual(job.slices.clean_render(), '0 */3 * * *') def test_02_dom(self): """Every Day of the Month""" for job in self.crontab: job.every(3).dom() self.assertEqual(job.slices.clean_render(), '0 0 */3 * *') def test_03_single(self): """Every Single Hour""" for job in self.crontab: job.every().hour() self.assertEqual(job.slices.clean_render(), '0 * * * *') def test_04_month(self): """Every Month""" for job in self.crontab: job.every(3).months() self.assertEqual(job.slices.clean_render(), '0 0 1 */3 *') def test_05_dow(self): """Every Day of the Week""" for job in self.crontab: job.every(3).dow() self.assertEqual(job.slices.clean_render(), '0 0 * * */3') def test_06_year(self): """Every Year""" for job in self.crontab: job.every().year() self.assertEqual(job.slices.render(), '@yearly') self.assertEqual(job.slices.clean_render(), '0 0 1 1 *') self.assertRaises(ValueError, job.every(2).year) def test_07_reboot(self): """Every Reboot""" for job in self.crontab: job.every_reboot() self.assertEqual(job.slices.render(), '@reboot') self.assertEqual(job.slices.clean_render(), '* * * * *') if __name__ == '__main__': test_support.run_unittest( EveryTestCase, ) python-crontab-1.9.3/tests/test_usage.py0000664000175000017500000000672712477225556022105 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2012 Jay Sigbrandt # Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab usage. """ import os import sys import unittest import crontab try: from test import test_support except ImportError: from test import support as test_support TEST_DIR = os.path.dirname(__file__) class DummyStdout(object): def write(self, text): pass BASIC = '@hourly firstcommand\n\n' USER = '\n*/4 * * * * user_command # user_comment\n\n\n' crontab.CRONCMD = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest')) def flush(): pass class UseTestCase(unittest.TestCase): """Test use documentation in crontab.""" def setUp(self): self.filenames = [] def test_01_empty(self): """Open system crontab""" cron = crontab.CronTab() self.assertEqual(cron.render(), "") self.assertEqual(cron.__unicode__(), "") def test_02_user(self): """Open a user's crontab""" cron = crontab.CronTab(user='basic') self.assertEqual(cron.render(), BASIC) def test_03_usage(self): """Dont modify crontab""" cron = crontab.CronTab(tab='') sys.stdout = DummyStdout() sys.stdout.flush = flush try: exec(crontab.__doc__) except ImportError: pass sys.stdout = sys.__stdout__ self.assertEqual(cron.render(), '') def test_04_username(self): """Username is True""" cron = crontab.CronTab(user=True) self.assertNotEqual(cron.user, True) self.assertEqual(cron.render(), USER) def test_05_nouser(self): """Username doesn't exist""" cron = crontab.CronTab(user='nouser') self.assertEqual(cron.render(), '') def test_06_touser(self): """Write to use API""" cron = crontab.CronTab(tab=USER) cron.write_to_user('bob') filename = os.path.join(TEST_DIR, 'data', 'bob.tab') self.filenames.append(filename) self.assertTrue(os.path.exists(filename)) def test_07_ioerror(self): """No filename ioerror""" with self.assertRaises(IOError): cron = crontab.CronTab(user='error') cron.read() def test_08_cronitem(self): """CronItem Standalone""" item = crontab.CronItem(line='noline') self.assertTrue(item.is_enabled()) with self.assertRaises(UnboundLocalError): item.delete() item.command = str('nothing') self.assertEqual(item.render(), '* * * * * nothing') def tearDown(self): for filename in self.filenames: if os.path.exists(filename): os.unlink(filename) if __name__ == '__main__': test_support.run_unittest( UseTestCase, ) python-crontab-1.9.3/tests/test_enums.py0000664000175000017500000000715112477224542022112 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2011 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Test crontab enumerations. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab try: from test import test_support except ImportError: from test import support as test_support INITAL_TAB = """ * * * JAN SAT enums * * * APR-MAR * ranges * * * * MON,FRI,WED multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ COMMANDS = [ 'enums', 'ranges', 'multiples', 'com1', 'com2', ] RESULT_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,WED,FRI multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ class EnumTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB) def test_01_presevation(self): """All Entries Re-Rendered Correctly""" self.crontab.write() results = RESULT_TAB.split('\n') line_no = 0 for line in self.crontab.intab.split('\n'): self.assertEqual(str(line), results[line_no]) line_no += 1 def test_02_simple_enum(self): """Simple Enumerations""" e = list(self.crontab.find_command('enums'))[0] self.assertEqual(e.month, 'JAN') self.assertEqual(e.month.render(True), '1') self.assertEqual(e.dow, 'SAT') self.assertEqual(e.dow.render(True), '6') def test_03_enum_range(self): """Enumeration Ranges""" e = list(self.crontab.find_command('ranges'))[0] self.assertEqual(e.month, 'MAR-APR') self.assertEqual(e.month.render(True), '3-4' ) def test_04_sets(self): """Enumeration Sets""" e = list(self.crontab.find_command('multiples'))[0] self.assertEqual(e.dow, 'MON,WED,FRI') self.assertEqual(e.dow.render(True), '1,3,5' ) def test_05_create(self): """Create by Enumeration""" job = self.crontab.new(command='new') job.month.on('JAN') job.dow.on('SUN') self.assertEqual(str(job), '* * * JAN SUN new') def test_06_create_range(self): """Created Enum Range""" job = self.crontab.new(command='new2') job.month.during('APR', 'NOV').every(2) self.assertEqual(str(job), '* * * APR-NOV/2 * new2') def test_07_create_set(self): """Created Enum Set""" job = self.crontab.new(command='new3') job.month.on('APR') job.month.also.on('NOV','JAN') self.assertEqual(str(job), '* * * JAN,APR,NOV * new3') def test_08_find_comment(self): """Comment Set""" jobs = list(self.crontab.find_comment('Comment One')) self.assertEqual(len(jobs), 2) for job in jobs: self.assertEqual(job.comment, 'Comment One') if __name__ == '__main__': test_support.run_unittest( EnumTestCase, ) python-crontab-1.9.3/MANIFEST.in0000664000175000017500000000026412344072461017736 0ustar doctormodoctormo00000000000000include AUTHORS include COPYING include README include MANIFEST.in include ChangeLog include setup.py include crontab.py include cronlog.py include tests/*.py include tests/data/* python-crontab-1.9.3/python_crontab.egg-info/0000775000175000017500000000000012504000222022701 5ustar doctormodoctormo00000000000000python-crontab-1.9.3/python_crontab.egg-info/dependency_links.txt0000664000175000017500000000000112504000222026747 0ustar doctormodoctormo00000000000000 python-crontab-1.9.3/python_crontab.egg-info/SOURCES.txt0000664000175000017500000000132212504000222024563 0ustar doctormodoctormo00000000000000AUTHORS COPYING ChangeLog MANIFEST.in README cronlog.py crontab.py setup.py python_crontab.egg-info/PKG-INFO python_crontab.egg-info/SOURCES.txt python_crontab.egg-info/dependency_links.txt python_crontab.egg-info/requires.txt python_crontab.egg-info/top_level.txt tests/__init__.py tests/test_compatibility.py tests/test_croniter.py tests/test_enums.py tests/test_every.py tests/test_frequency.py tests/test_interaction.py tests/test_log.py tests/test_range.py tests/test_removal.py tests/test_system_cron.py tests/test_usage.py tests/test_utf8.py tests/data/basic.log tests/data/basic.tab tests/data/crontest tests/data/specials.tab tests/data/specials_enc.tab tests/data/test.log tests/data/test.tab tests/data/user.tabpython-crontab-1.9.3/python_crontab.egg-info/requires.txt0000664000175000017500000000005112504000222025275 0ustar doctormodoctormo00000000000000python-dateutil [cron-schedule] croniterpython-crontab-1.9.3/python_crontab.egg-info/PKG-INFO0000664000175000017500000002651212504000222024004 0ustar doctormodoctormo00000000000000Metadata-Version: 1.1 Name: python-crontab Version: 1.9.3 Summary: Python Crontab API Home-page: https://launchpad.net/python-crontab Author: Martin Owens Author-email: doctormo@gmail.com License: GPLv3 Description: .. image:: https://launchpadlibrarian.net/134092458/python-cron-192.png :class: floating-box :alt: Python Crontab Logo Bug Reports and Development =========================== Please report any problems to the `launchpad bug tracker `_. Please use Bazaar and push patches to the `launchpad project code hosting `_. **Note:** If you get the error ``TypeError: __init__() takes exactly 2 arguments`` when using CronTab, you have the wrong module installed. You need to install ``python-crontab`` and not ``crontab`` from pypi or your local package manager and try again. Description =========== Crontab module for read and writing crontab files and accessing the system cron automatically and simply using a direct API. Comparing the `below chart `_ you will note that W, L, # and ? symbols are not supported as they are not standard Linux or SystemV crontab format. ============= =========== ================= =================== ============= Field Name Mandatory Allowed Values Special Characters Extra Values ============= =========== ================= =================== ============= Minutes Yes 0-59 \* / , - < > Hours Yes 0-23 \* / , - < > Day of month Yes 1-31 \* / , - < > Month Yes 1-12 or JAN-DEC \* / , - < > Day of week Yes 0-6 or SUN-SAT \* / , - < > ============= =========== ================= =================== ============= Extra Values are '<' for minimum value, such as 0 for minutes or 1 for months. And '>' for maximum value, such as 23 for hours or 12 for months. Supported special cases allow crontab lines to not use fields. These are the supported aliases which are not available in SystemV mode: =========== =========== Case Meaning =========== =========== @reboot Every boot @hourly 0 * * * * @daily 0 0 * * * @weekly 0 0 * * 0 @monthly 0 0 1 * * @yearly 0 0 1 1 * @annually 0 0 1 1 * @midnight 0 0 * * * =========== =========== How to Use the Module ===================== Getting access to a crontab can happen in five ways, three system methods that will work only on Unix and require you to have the right permissions:: from crontab import CronTab empty_cron = CronTab() my_user_cron = CronTab(user=True) users_cron = CronTab(user='username') And two ways from non-system sources that will work on Windows too:: file_cron = CronTab(tabfile='filename.tab') mem_cron = CronTab(tab=""" * * * * * command """) Special per-command user flag for vixie cron format (new in 1.9):: system_cron = CronTab(tabfile='/etc/crontab', user=False) job = system_cron[0] job.user != None system_cron.new(command='new_command', user='root') Creating a new job is as simple as:: job = cron.new(command='/usr/bin/echo') And setting the job's time restrictions:: job.minute.during(5,50).every(5) job.hour.every(4) job.day.on(4, 5, 6) job.dow.on('SUN') job.month.during('APR', 'NOV') Each time restriction will clear the previous restriction:: job.hour.every(10) # Set to * */10 * * * job.hour.on(2) # Set to * 2 * * * Appending restrictions is explicit:: job.hour.every(10) # Set to * */10 * * * job.hour.also.on(2) # Set to * 2,*/10 * * * Setting all time slices at once:: job.setall(2, 10, '2-4', '*/2', None) job.setall('2 10 * * *') Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Get the comment or command for a job:: command = job.command comment = job.comment Modify the comment or command on a job:: job.set_command("new_script.sh") job.set_comment("New ID or comment here") Disabled or Enable Job:: job.enable() job.enable(False) False == job.is_enabled() Validity Check:: True == job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command:: iter = cron.find_command('bar') Find an existing job by comment:: iter = cron.find_comment('ID or some text') Find an existing job by schedule:: iter = cron.find_time(2, 10, '2-4', '*/2', None) iter = cron.find_time("*/2 * * * *") Clean a job of all rules:: job.clear() Iterate through all jobs:: for job in cron: print job Iterate through all lines:: for line in cron.lines: print line Remove Items:: cron.remove( job ) cron.remove_all('echo') cron.remove_all(comment='foo') cron.remove_all(time='*/2') Clear entire cron of all jobs:: cron.remove_all() Write CronTab back to system or filename:: cron.write() Write CronTab to new filename:: cron.write( 'output.tab' ) Write to this user's crontab (unix only):: cron.write_to_user( user=True ) Write to some other user's crontab:: cron.write_to_user( user='bob' ) Proceeding Unit Confusion ========================= It is sometimes logical to think that job.hour.every(2) will set all proceeding units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling only the hours units and the minute column is unaffected. The real result would be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs. There is a special 'every' method on a job to clear the job's existing schedule and replace it with a simple single unit:: job.every(4).hours() == '0 */4 * * *' job.every().dom() == '0 0 * * *' job.every().month() == '0 0 0 * *' job.every(2).dows() == '0 0 * * */2' This is a convenience method only, it does normal things with the existing api. Frequency Calculation ===================== Every job's schedule has a frequency. We can attempt to calculate the number of times a job would execute in a give amount of time. We have three simple methods:: job.setall("1,2 1,2 * * *") job.frequency_per_day() == 4 The per year frequency method will tell you how many days a year the job would execute:: job.setall("* * 1,2 1,2 *") job.frequency_per_year(year=2010) == 4 These are combined to give the number of times a job will execute in any year:: job.setall("1,2 1,2 1,2 1,2 *") job.frequency(year=2010) == 16 Frequency can be quickly checked using python built-in operators:: job < "*/2 * * * *" job > job2 job.slices == "*/5" Log Functionality ================= The log functionality will read a cron log backwards to find you the last run instances of your crontab and cron jobs. The crontab will limit the returned entries to the user the crontab is for:: cron = CronTab(user='root') for d in cron.log: print d['pid'] + " - " + d['date'] Each job can return a log iterator too, these are filtered so you can see when the last execution was:: for d in cron.find_command('echo')[0].log: print d['pid'] + " - " + d['date'] Schedule Functionality ====================== If you have the croniter python module installed, you will have access to a schedule on each job. For example if you want to know when a job will next run:: schedule = job.schedule(date_from=datetime.now()) This creates a schedule croniter based on the job from the time specified. The default date_from is the current date/time if not specified. Next we can get the datetime of the next job:: datetime = schedule.get_next() Or the previous:: datetime = schedule.get_prev() The get methods work in the same way as the default croniter, except that they will return datetime objects by default instead of floats. If you want the original functionality, pass float into the method when calling:: datetime = schedule.get_current(float) If you don't have the croniter module installed, you'll get an ImportError when you first try using the schedule function on your cron job object. Extra Support ============= - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - Python 3.4 and Python 2.7/2.6 tested. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) Platform: linux Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Provides: crontab Provides: cronlog python-crontab-1.9.3/python_crontab.egg-info/top_level.txt0000664000175000017500000000002012504000222025423 0ustar doctormodoctormo00000000000000cronlog crontab python-crontab-1.9.3/setup.py0000755000175000017500000000447412477224323017725 0ustar doctormodoctormo00000000000000#!/usr/bin/env python # # Copyright (C) 2008 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # from setuptools import setup from crontab import __version__, __pkgname__ import os # remove MANIFEST. distutils doesn't properly update it when the # contents of directories change. if os.path.exists('MANIFEST'): os.remove('MANIFEST') # Grab description for Pypi with open('README') as fhl: description = fhl.read() setup( name = __pkgname__, version = __version__, description = 'Python Crontab API', long_description = description, author = 'Martin Owens', url = 'https://launchpad.net/python-crontab', author_email = 'doctormo@gmail.com', test_suite = 'tests', platforms = 'linux', license = 'GPLv3', py_modules = [ 'crontab', 'cronlog' ], provides = [ 'crontab', 'cronlog' ], install_requires = [ 'python-dateutil' ], extras_require = { 'cron-schedule': ['croniter'] }, classifiers = [ 'Development Status :: 5 - Production/Stable', 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: SunOS/Solaris', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', ] ) python-crontab-1.9.3/COPYING0000664000175000017500000010451312344072461017235 0ustar doctormodoctormo00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . python-crontab-1.9.3/setup.cfg0000664000175000017500000000007312504000222017777 0ustar doctormodoctormo00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0