commit python-python-crontab for openSUSE:Factory
Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-python-crontab for openSUSE:Factory checked in at 2024-01-03 12:28:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-python-crontab (Old) and /work/SRC/openSUSE:Factory/.python-python-crontab.new.28375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-python-crontab" Wed Jan 3 12:28:00 2024 rev:8 rq:1136017 version:3.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-python-crontab/python-python-crontab.changes 2023-02-28 12:49:50.336904563 +0100 +++ /work/SRC/openSUSE:Factory/.python-python-crontab.new.28375/python-python-crontab.changes 2024-01-03 12:28:00.974113554 +0100 @@ -1,0 +2,10 @@ +Mon Jan 1 20:28:24 UTC 2024 - Dirk Müller <dmueller@suse.com> + +- update to 3.0.0: + * Add frequency checks at specific timestamp + * Fix lots of pylint errors and improve test coverage + * Improve schedule running with more information about what was + returned + * Cause an error when setting an invalid frequency + +------------------------------------------------------------------- Old: ---- python-crontab-2.7.1.tar.gz New: ---- python-crontab-3.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-python-crontab.spec ++++++ --- /var/tmp/diff_new_pack.q52vru/_old 2024-01-03 12:28:01.614136927 +0100 +++ /var/tmp/diff_new_pack.q52vru/_new 2024-01-03 12:28:01.614136927 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-python-crontab # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -16,15 +16,17 @@ # +%{?sle15_python_module_pythons} Name: python-python-crontab -Version: 2.7.1 +Version: 3.0.0 Release: 0 Summary: Python Crontab API License: LGPL-3.0-only Group: Development/Languages/Python URL: https://gitlab.com/doctormo/python-crontab/ Source: https://files.pythonhosted.org/packages/source/p/python-crontab/python-crontab-%{version}.tar.gz -BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module pip} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-python-dateutil @@ -50,10 +52,10 @@ %setup -q -n python-crontab-%{version} %build -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitelib} %check @@ -63,7 +65,8 @@ } export PATH=$PWD/build/bin:$PATH # test_07_non_posix_shell - only for Windows -%pytest -k "not test_07_non_posix_shell" +# test_20_frequency_at_year - broken test which fails in leap years +%pytest -k "not test_07_non_posix_shell and not test_20_frequency_at_year" %files %{python_files} %doc README.rst @@ -71,6 +74,6 @@ %{python_sitelib}/cronlog.py %{python_sitelib}/crontab.py %{python_sitelib}/crontabs.py -%{python_sitelib}/python_crontab-%{version}*-info +%{python_sitelib}/python_crontab-%{version}.dist-info %pycache_only %{python_sitelib}/__pycache__ ++++++ python-crontab-2.7.1.tar.gz -> python-crontab-3.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/PKG-INFO new/python-crontab-3.0.0/PKG-INFO --- old/python-crontab-2.7.1/PKG-INFO 2022-12-22 05:48:55.191304000 +0100 +++ new/python-crontab-3.0.0/PKG-INFO 2023-07-13 16:53:01.829805600 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-crontab -Version: 2.7.1 +Version: 3.0.0 Summary: Python Crontab API Home-page: https://gitlab.com/doctormo/python-crontab/ Author: Martin Owens @@ -26,8 +26,9 @@ Provides: crontab Provides: crontabs Provides: cronlog -Provides-Extra: cron-description +Description-Content-Type: text/x-rst Provides-Extra: cron-schedule +Provides-Extra: cron-description License-File: COPYING License-File: AUTHORS @@ -345,7 +346,9 @@ tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): - print("This was printed to stdout by the process.") + print("Return code: {result.returncode}") + print("Standard Out: {result.stdout}") + print("Standard Err: {result.stderr}") Do not do this, it won't work because it returns generator function:: @@ -361,15 +364,20 @@ 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:: +of times a job would execute in a give amount of time. We have two variants +`frequency_per_*` and `frequency_at_*` calculations. The `freqency_at_*` +always returnes *times* a job would execute and is aware of leap years. + + +`frequency_per_*` +----------------- +For `frequency_per_*` 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 +The per year frequency method will tell you how many **days** a year the job would execute:: job.setall("* * 1,2 1,2 *") @@ -386,6 +394,43 @@ job > job2 job.slices == "*/5" + +`frequency_at_*` +---------------- +For `frequency_at_*` We have four simple methods. + +The at per hour frequency method will tell you how many times the job would +execute at a given hour:: + + job.setall("*/2 0 * * *") + job.frequency_at_hour() == 30 + job.frequency_at_hour(year=2010, month=1, day=1, hour=0) == 30 # even hour + job.frequency_at_hour(year=2010, month=1, day=1, hour=1) == 0 # odd hour + +The at day frequency method parameterized tells you how many times the job +would execute at a given day:: + + job.setall("0 0 * * 1,2") + job.frequency_at_day(year=2010, month=1, day=18) == 24 # Mon Jan 18th 2020 + job.frequency_at_day(year=2010, month=1, day=21) == 0 # Thu Jan 21th 2020 + +The at month frequency method will tell you how many times the job would +execute at a given month:: + + job.setall("0 0 * * *") + job.frequency_at_month() == <output_of_current_month> + job.frequency_at_month(year=2010, month=1) == 31 + job.frequency_at_month(year=2010, month=2) == 28 + job.frequency_at_month(year=2012, month=2) == 29 # leap year + +The at year frequency method will tell you how many times a year the +job would execute:: + + job.setall("* * 3,29 2 *") + job.frequency_at_year(year=2021) == 24 + job.frequency_at_year(year=2024) == 48 # leap year + + Log Functionality ================= @@ -472,5 +517,3 @@ - Python 3 (3.7, 3.8, 3.10) tested, python 2.6, 2.7 removed from support. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/README.rst new/python-crontab-3.0.0/README.rst --- old/python-crontab-2.7.1/README.rst 2022-12-22 05:32:19.000000000 +0100 +++ new/python-crontab-3.0.0/README.rst 2023-07-13 16:51:49.000000000 +0200 @@ -312,7 +312,9 @@ tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): - print("This was printed to stdout by the process.") + print("Return code: {result.returncode}") + print("Standard Out: {result.stdout}") + print("Standard Err: {result.stderr}") Do not do this, it won't work because it returns generator function:: @@ -328,15 +330,20 @@ 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:: +of times a job would execute in a give amount of time. We have two variants +`frequency_per_*` and `frequency_at_*` calculations. The `freqency_at_*` +always returnes *times* a job would execute and is aware of leap years. + + +`frequency_per_*` +----------------- +For `frequency_per_*` 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 +The per year frequency method will tell you how many **days** a year the job would execute:: job.setall("* * 1,2 1,2 *") @@ -353,6 +360,43 @@ job > job2 job.slices == "*/5" + +`frequency_at_*` +---------------- +For `frequency_at_*` We have four simple methods. + +The at per hour frequency method will tell you how many times the job would +execute at a given hour:: + + job.setall("*/2 0 * * *") + job.frequency_at_hour() == 30 + job.frequency_at_hour(year=2010, month=1, day=1, hour=0) == 30 # even hour + job.frequency_at_hour(year=2010, month=1, day=1, hour=1) == 0 # odd hour + +The at day frequency method parameterized tells you how many times the job +would execute at a given day:: + + job.setall("0 0 * * 1,2") + job.frequency_at_day(year=2010, month=1, day=18) == 24 # Mon Jan 18th 2020 + job.frequency_at_day(year=2010, month=1, day=21) == 0 # Thu Jan 21th 2020 + +The at month frequency method will tell you how many times the job would +execute at a given month:: + + job.setall("0 0 * * *") + job.frequency_at_month() == <output_of_current_month> + job.frequency_at_month(year=2010, month=1) == 31 + job.frequency_at_month(year=2010, month=2) == 28 + job.frequency_at_month(year=2012, month=2) == 29 # leap year + +The at year frequency method will tell you how many times a year the +job would execute:: + + job.setall("* * 3,29 2 *") + job.frequency_at_year(year=2021) == 24 + job.frequency_at_year(year=2024) == 48 # leap year + + Log Functionality ================= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/crontab.py new/python-crontab-3.0.0/crontab.py --- old/python-crontab-2.7.1/crontab.py 2022-12-22 05:43:07.000000000 +0100 +++ new/python-crontab-3.0.0/crontab.py 2023-07-13 15:57:37.000000000 +0200 @@ -93,12 +93,13 @@ import platform import subprocess as sp +from calendar import monthrange from time import sleep from datetime import time, date, datetime, timedelta from collections import OrderedDict __pkgname__ = 'python-crontab' -__version__ = '2.7.1' +__version__ = '3.0.0' ITEMREX = re.compile(r'^\s*([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)' r'\s+([^@#\s]+)\s+([^\n]*?)(\s+#\s*([^\n]*)|$)') @@ -158,22 +159,6 @@ """Returns the username of the current user""" return pwd.getpwuid(os.getuid())[0] -def open_pipe(cmd, *args, **flags): - """Runs a program and orders the arguments for compatability. - - a. keyword args are flags and always appear /before/ arguments for bsd - """ - cmd_args = tuple(shlex.split(cmd, posix=flags.pop('posix', POSIX))) - env = flags.pop('env', None) - for (key, value) in flags.items(): - if len(key) == 1: - cmd_args += (("-%s" % key),) - if value is not None: - cmd_args += (str(value),) - else: - cmd_args += (("--%s=%s" % (key, value)),) - args = tuple(arg for arg in (cmd_args + tuple(args)) if arg) - return sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE, env=env) def _str(text): """Convert to the best string format for this python version""" @@ -182,6 +167,53 @@ return text +class Process: + """Runs a program and orders the arguments for compatability. + + a. keyword args are flags and always appear /before/ arguments for bsd + """ + def __init__(self, cmd, *args, **flags): + cmd_args = tuple(shlex.split(cmd, posix=flags.pop('posix', POSIX))) + self.env = flags.pop('env', None) + for (key, value) in flags.items(): + if len(key) == 1: + cmd_args += (f"-{key}",) + if value is not None: + cmd_args += (str(value),) + else: + cmd_args += (f"--{key}={value}",) + self.args = tuple(arg for arg in (cmd_args + tuple(args)) if arg) + self.has_run = False + self.stdout = None + self.stderr = None + self.returncode = None + + def _run(self): + """Run this process and return the popen process object""" + return sp.Popen(self.args, stdout=sp.PIPE, stderr=sp.PIPE, env=self.env) + + def run(self): + """Run this process and store whatever is returned""" + process = self._run() + (out, err) = process.communicate() + self.returncode = process.returncode + self.stdout = out.decode("utf-8") + self.stderr = err.decode("utf-8") + return self + + def __str__(self): + return self.stdout.strip() + + def __repr__(self): + return f"Process({self.args})" + + def __int__(self): + return self.returncode + + def __eq__(self, other): + return str(self) == other + + class CronTab: """ Crontab object which can access any time based cron using the standard. @@ -252,7 +284,7 @@ elif name == 'crons' and value: raise AttributeError("You can NOT set crons attribute directly") else: - super(CronTab, self).__setattr__(name, value) + super().__setattr__(name, value) def read(self, filename=None): """ @@ -273,12 +305,10 @@ lines = fhl.readlines() elif self.user: - (out, err) = open_pipe(self.cron_command, 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") + process = Process(self.cron_command, l='', **self.user_opt).run() + if process.stderr and 'no crontab for' not in process.stderr: + raise IOError(f"Read crontab {self.user}: {process.stderr}") + lines = process.stdout.split("\n") self.lines = lines @@ -305,8 +335,8 @@ cron_id = self.crons.index(before) line_id = self.lines.index(before) - except ValueError: - raise ValueError("Can not find CronItem in crontab to insert before") + except ValueError as err: + raise ValueError("Can not find CronItem in crontab to insert before") from err if item.is_valid(): item.env.update(self._parked_env) @@ -349,7 +379,7 @@ return if self.filen: - fileh = open(self.filen, 'wb') + fileh = open(self.filen, 'wb') # pylint: disable=consider-using-with else: filed, path = tempfile.mkstemp() fileh = os.fdopen(filed, 'wb') @@ -363,11 +393,11 @@ os.unlink(path) raise IOError("Please specify user or filename to write.") - proc = open_pipe(self.cron_command, path, **self.user_opt) + proc = Process(self.cron_command, path, **self.user_opt)._run() ret = proc.wait() if ret != 0: - raise IOError("Program Error: {} returned {}: {}".format( - self.cron_command, ret, proc.stderr.read())) + msg = proc.stderr.read() + raise IOError(f"Program Error: {self.cron_command} returned {ret}: {msg}") proc.stdout.close() proc.stderr.close() os.unlink(path) @@ -383,17 +413,17 @@ if ret not in [None, -1]: yield ret - def run_scheduler(self, timeout=-1, **kwargs): + def run_scheduler(self, timeout=-1, cadence=60, warp=False): """Run the CronTab as an internal scheduler (generator)""" count = 0 while count != timeout: now = datetime.now() - if 'warp' in kwargs: + if warp: now += timedelta(seconds=count * 60) for value in self.run_pending(now=now): yield value - sleep(kwargs.get('cadence', 60)) + sleep(cadence) count += 1 def render(self, errors=False, specials=True): @@ -413,7 +443,7 @@ elif not errors: crons.append('# DISABLED LINE\n# ' + line) else: - raise ValueError("Invalid line: %s" % line) + raise ValueError(f"Invalid line: {line}") elif isinstance(line, CronItem): if not line.is_valid() and not errors: line.enabled = False @@ -421,12 +451,12 @@ # Environment variables are attached to cron lines so order will # always work no matter how you add lines in the middle of the stack. - result = str(self.env) + u'\n'.join(crons) - if result and result[-1] not in (u'\n', u'\r'): - result += u'\n' + result = str(self.env) + '\n'.join(crons) + if result and result[-1] not in ('\n', '\r'): + result += '\n' return result - def new(self, command='', comment='', user=None, pre_comment=False, before=None): + def new(self, command='', comment='', user=None, pre_comment=False, before=None): # pylint: disable=too-many-arguments """ Create a new CronItem and append it to the cron. @@ -541,12 +571,12 @@ def __repr__(self): kind = 'System ' if self._user is False else '' if self.filen: - return "<%sCronTab '%s'>" % (kind, self.filen) + return f"<{kind}CronTab '{self.filen}'>" if self.user and not self.user_opt: return "<My CronTab>" if self.user: - return "<User CronTab '%s'>" % self.user - return "<Unattached %sCronTab>" % kind + return f"<User CronTab '{self.user}'>" + return f"<Unattached {kind}CronTab>" def __iter__(self): """Return generator so we can track jobs after removal""" @@ -695,23 +725,24 @@ if not self.is_valid() and self.enabled: raise ValueError('Refusing to render invalid crontab.' ' Disable to continue.') - command = _str(self.command).replace(u'%', u'\\%') + command = _str(self.command).replace('%', '\\%') 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" % (self.slices.render(specials=specials), user, command) + rend = self.slices.render(specials=specials) + result = f"{rend} {user}{command}" if self.stdin: result += ' %' + self.stdin.replace('\n', '%') if not self.enabled: - result = u"# " + result + result = "# " + result if self.comment: comment = self.comment = _str(self.comment) if self.marker: - comment = u"#%s: %s" % (self.marker, comment) + comment = f"#{self.marker}: {comment}" else: - comment = u"# " + comment + comment = "# " + comment if SYSTEMV or self.pre_comment or self.stdin: result = comment + "\n" + result @@ -757,6 +788,34 @@ """ return self.slices.frequency(year=year) + def frequency_at_hour(self, year=None, month=None, day=None, hour=None): + """Returns the number of times this item will execute in a given hour + (defaults to this hour) + """ + return self.slices.frequency_at_hour(year=year, month=month, day=day, hour=hour) + + def frequency_at_day(self, year=None, month=None, day=None): + """Returns the number of times this item will execute in a given day + (defaults to today) + """ + return self.slices.frequency_at_day(year=year, month=month, day=day) + + def frequency_at_month(self, year=None, month=None): + """Returns the number of times this item will execute in a given month + (defaults to this month) + """ + return self.slices.frequency_at_month(year=year, month=month) + + def frequency_at_year(self, year=None): + """Returns the number of times this item will execute in a given year + (defaults to this year) + """ + return self.slices.frequency_at_year(year=year) + + def frequency(self, year=None): + """Return frequence per year times frequency per day""" + return self.frequency_per_year(year=year) * self.frequency_per_day() + def frequency_per_year(self, year=None): """Returns the number of /days/ this item will execute on in a year (defaults to this year) @@ -789,10 +848,10 @@ env = os.environ.copy() env.update(self.env.all()) shell = self.env.get('SHELL', SHELL) - (out, err) = open_pipe(shell, '-c', self.command, env=env).communicate() - if err: - LOG.error(err.decode("utf-8")) - return out.decode("utf-8").strip() + process = Process(shell, '-c', self.command, env=env).run() + if process.stderr: + LOG.error(process.stderr) + return process def schedule(self, date_from=None): """Return a croniter schedule if available.""" @@ -801,9 +860,9 @@ try: # Croniter is an optional import from croniter.croniter import croniter # pylint: disable=import-outside-toplevel - except ImportError: + except ImportError as err: raise ImportError("Croniter not available. Please install croniter" - " python module via pip or your package manager") + " python module via pip or your package manager") from err return croniter(self.slices.clean_render(), date_from, ret_type=datetime) def description(self, **kw): @@ -814,9 +873,9 @@ """ try: from cron_descriptor import ExpressionDescriptor # pylint: disable=import-outside-toplevel - except ImportError: + except ImportError as err: raise ImportError("cron_descriptor not available. Please install"\ - "cron_descriptor python module via pip or your package manager") + "cron_descriptor python module via pip or your package manager") from err exdesc = ExpressionDescriptor(self.slices.clean_render(), **kw) return exdesc.get_description() @@ -874,7 +933,7 @@ return self.slices[4] def __repr__(self): - return "<CronItem '%s'>" % str(self) + return f"<CronItem '{self}'>" def __len__(self): return len(str(self)) @@ -922,7 +981,7 @@ def year(self): """Special every year target""" if self.unit > 1: - raise ValueError("Invalid value '%s', outside 1 year" % self.unit) + raise ValueError(f"Invalid value '{self.unit}', outside 1 year") self.slices.setall('@yearly') @@ -932,7 +991,7 @@ month requency and finally day of the week frequency. """ def __init__(self, *args): - super(CronSlices, self).__init__([CronSlice(info) for info in S_INFO]) + super().__init__([CronSlice(info) for info in S_INFO]) self.special = None self.setall(*args) self.is_valid = self.is_self_valid @@ -977,7 +1036,8 @@ # It might be possible to later understand timedelta objects # but there's no convincing mathematics to do the conversion yet. if not isinstance(value, (list, tuple)): - raise ValueError("Unknown type: {}".format(type(value).__name__)) + typ = type(value).__name__ + raise ValueError(f"Unknown type: {typ}") return value, None @staticmethod @@ -986,10 +1046,10 @@ key = value.lstrip('@').lower() if value.count(' ') == 4: return value.strip().split(' '), None - if key in SPECIALS.keys(): + if key in SPECIALS: return SPECIALS[key].split(' '), '@' + key if value.startswith('@'): - raise ValueError("Unknown special '{}'".format(value)) + raise ValueError(f"Unknown special '{value}'") return [value], None def clean_render(self): @@ -1006,7 +1066,7 @@ if not SYSTEMV and specials is True: for (name, value) in SPECIALS.items(): if value == slices and name not in SPECIAL_IGNORE: - return "@%s" % name + return f"@{name}" return slices def clear(self): @@ -1045,6 +1105,82 @@ """Returns the number of times this item will execute in any hour""" return len(self[0]) + def frequency_at_year(self, year=None): + """Returns the number of /days/ this item will execute + in a given year (default is this year)""" + if not year: + year = date.today().year + + total = 0 + for month in range(1, 13): + total += self.frequency_at_month(year, month) + return total + + def frequency_at_month(self, year=None, month=None): + """Returns the number of times this item will execute in given month + (default: current month) + """ + if year is None and month is None: + year = date.today().year + month = date.today().month + elif year is None or month is None: + raise ValueError( + f"One of more arguments undefined: year={year}, month={month}") + + total = 0 + if month in self[3]: + # Calculate amount of days of specific month + days = monthrange(year, month)[1] + for day in range(1, days + 1): + total += self.frequency_at_day(year, month, day) + return total + + def frequency_at_day(self, year=None, month=None, day=None): + """Returns the number of times this item will execute in a day + (default: any executed day) + """ + # If arguments provided, all needs to be provided + test_none = [x is None for x in [year, month, day]] + + if all(test_none): + return len(self[0]) * len(self[1]) + + if any(test_none): + raise ValueError( + f"One of more arguments undefined: year={year}, month={month}, day={day}") + + total = 0 + if day in self[2]: + for hour in range(24): + total += self.frequency_at_hour(year, month, day, hour) + return total + + def frequency_at_hour(self, year=None, month=None, day=None, hour=None): + """Returns the number of times this item will execute in a hour + (default: any executed hour) + """ + # If arguments provided, all needs to be provided + test_none = [x is None for x in [year, month, day, hour]] + + if all(test_none): + return len(self[0]) + + if any(test_none): + raise ValueError( + f"One of more arguments undefined: year={year}, month={month}, day={day}, hour={hour}") + + result = 0 + weekday = date(year, month, day).weekday() + + # Check if scheduled for execution at defined moment + if hour in self[1] and \ + day in self[2] and \ + month in self[3] and \ + ((weekday + 1) % 7) in self[4]: + result = len(self[0]) + + return result + def __str__(self): return self.render() @@ -1098,7 +1234,7 @@ continue self.parts.append(self.parse_value(part, sunday=0)) - def render(self, resolve=False, specials=True): + def render(self, resolve=False): """Return the slice rendered as a crontab. resolve - return integer values instead of enums (default False) @@ -1109,7 +1245,7 @@ return _render_values(self.parts, ',', resolve) def __repr__(self): - return "<CronSlice '%s'>" % str(self) + return f"<CronSlice '{self}'>" def __eq__(self, value): return str(self) == str(value) @@ -1182,10 +1318,10 @@ val = self.min try: out = get_cronvalue(val, self.enum) - except ValueError: - raise ValueError("Unrecognised %s: '%s'" % (self.name, val)) - except KeyError: - raise KeyError("No enumeration for %s: '%s'" % (self.name, val)) + except ValueError as err: + raise ValueError(f"Unrecognised {self.name}: '{val}'") from err + except KeyError as err: + raise KeyError(f"No enumeration for {self.name}: '{val}'") from err if self.max == 6 and int(out) == 7: if sunday is not None: @@ -1193,7 +1329,7 @@ raise SundayError("Detected Sunday as 7 instead of 0!") if int(out) < self.min or int(out) > self.max: - raise ValueError("'{1}', not in {0.min}-{0.max} for {0.name}".format(self, val)) + raise ValueError(f"'{val}', not in {self.min}-{self.max} for {self.name}") return out @@ -1240,7 +1376,7 @@ return value.render(resolve) if resolve: return str(int(value)) - return str(u'{:02d}'.format(value) if ZERO_PAD else value) + return str(f'{value:02d}' if ZERO_PAD else value) class CronRange: @@ -1286,11 +1422,11 @@ self.dangling = 0 self.vto = self.slice.parse_value(vto, sunday=6) if self.vto < self.vfrom: - raise ValueError("Bad range '{0.vfrom}-{0.vto}'".format(self)) + raise ValueError(f"Bad range '{self.vfrom}-{self.vto}'") elif value == '*': self.all() else: - raise ValueError('Unknown cron range value "%s"' % value) + raise ValueError(f'Unknown cron range value "{value}"') def all(self): """Set this slice to all units between the miniumum and maximum""" @@ -1306,7 +1442,7 @@ else: value = _render_values([self.vfrom, self.vto], '-', resolve) if self.seq != 1: - value += "/%d" % self.seq + value += f"/{self.seq:d}" if value != '*' and SYSTEMV: value = ','.join([str(val) for val in self.range()]) return value @@ -1344,7 +1480,7 @@ """ def __init__(self, *args, **kw): self.job = kw.pop('job', None) - super(OrderedVariableList, self).__init__(*args, **kw) + super().__init__(*args, **kw) @property def previous(self): @@ -1370,10 +1506,10 @@ def __getitem__(self, key): previous = self.previous if key in self: - return super(OrderedVariableList, self).__getitem__(key) + return super().__getitem__(key) if previous is not None: return previous.all()[key] - raise KeyError("Environment Variable '%s' not found." % key) + raise KeyError(f"Environment Variable '{key}' not found.") def __str__(self): """Constructs to variable list output used in cron jobs""" @@ -1383,7 +1519,7 @@ if self.previous.all().get(key, None) == value: continue if ' ' in str(value) or value == '': - value = '"%s"' % value - ret.append("%s=%s" % (key, str(value))) + value = f'"{value}"' + ret.append(f"{key}={value}") ret.append('') return "\n".join(ret) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/python_crontab.egg-info/PKG-INFO new/python-crontab-3.0.0/python_crontab.egg-info/PKG-INFO --- old/python-crontab-2.7.1/python_crontab.egg-info/PKG-INFO 2022-12-22 05:48:55.000000000 +0100 +++ new/python-crontab-3.0.0/python_crontab.egg-info/PKG-INFO 2023-07-13 16:53:01.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-crontab -Version: 2.7.1 +Version: 3.0.0 Summary: Python Crontab API Home-page: https://gitlab.com/doctormo/python-crontab/ Author: Martin Owens @@ -26,8 +26,9 @@ Provides: crontab Provides: crontabs Provides: cronlog -Provides-Extra: cron-description +Description-Content-Type: text/x-rst Provides-Extra: cron-schedule +Provides-Extra: cron-description License-File: COPYING License-File: AUTHORS @@ -345,7 +346,9 @@ tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): - print("This was printed to stdout by the process.") + print("Return code: {result.returncode}") + print("Standard Out: {result.stdout}") + print("Standard Err: {result.stderr}") Do not do this, it won't work because it returns generator function:: @@ -361,15 +364,20 @@ 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:: +of times a job would execute in a give amount of time. We have two variants +`frequency_per_*` and `frequency_at_*` calculations. The `freqency_at_*` +always returnes *times* a job would execute and is aware of leap years. + + +`frequency_per_*` +----------------- +For `frequency_per_*` 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 +The per year frequency method will tell you how many **days** a year the job would execute:: job.setall("* * 1,2 1,2 *") @@ -386,6 +394,43 @@ job > job2 job.slices == "*/5" + +`frequency_at_*` +---------------- +For `frequency_at_*` We have four simple methods. + +The at per hour frequency method will tell you how many times the job would +execute at a given hour:: + + job.setall("*/2 0 * * *") + job.frequency_at_hour() == 30 + job.frequency_at_hour(year=2010, month=1, day=1, hour=0) == 30 # even hour + job.frequency_at_hour(year=2010, month=1, day=1, hour=1) == 0 # odd hour + +The at day frequency method parameterized tells you how many times the job +would execute at a given day:: + + job.setall("0 0 * * 1,2") + job.frequency_at_day(year=2010, month=1, day=18) == 24 # Mon Jan 18th 2020 + job.frequency_at_day(year=2010, month=1, day=21) == 0 # Thu Jan 21th 2020 + +The at month frequency method will tell you how many times the job would +execute at a given month:: + + job.setall("0 0 * * *") + job.frequency_at_month() == <output_of_current_month> + job.frequency_at_month(year=2010, month=1) == 31 + job.frequency_at_month(year=2010, month=2) == 28 + job.frequency_at_month(year=2012, month=2) == 29 # leap year + +The at year frequency method will tell you how many times a year the +job would execute:: + + job.setall("* * 3,29 2 *") + job.frequency_at_year(year=2021) == 24 + job.frequency_at_year(year=2024) == 48 # leap year + + Log Functionality ================= @@ -472,5 +517,3 @@ - Python 3 (3.7, 3.8, 3.10) tested, python 2.6, 2.7 removed from support. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/setup.py new/python-crontab-3.0.0/setup.py --- old/python-crontab-2.7.1/setup.py 2022-12-22 05:20:28.000000000 +0100 +++ new/python-crontab-3.0.0/setup.py 2023-07-13 16:52:41.000000000 +0200 @@ -40,6 +40,7 @@ release = RELEASE, description = 'Python Crontab API', long_description = description, + long_description_content_type = "text/x-rst", author = 'Martin Owens', url = 'https://gitlab.com/doctormo/python-crontab/', author_email = 'doctormo@gmail.com', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/tests/test_compatibility.py new/python-crontab-3.0.0/tests/test_compatibility.py --- old/python-crontab-2.7.1/tests/test_compatibility.py 2020-05-17 19:13:48.000000000 +0200 +++ new/python-crontab-3.0.0/tests/test_compatibility.py 2023-07-13 15:44:06.000000000 +0200 @@ -116,9 +116,9 @@ def test_07_non_posix_shell(self): """Shell in windows environments is split correctly""" - from crontab import open_pipe + from crontab import Process winfile = os.path.join(TEST_DIR, 'data', "bash\\win.exe") - pipe = open_pipe("{sys.executable} {winfile}".format(winfile=winfile, sys=sys), 'SLASHED', posix=False) + pipe = Process("{sys.executable} {winfile}".format(winfile=winfile, sys=sys), 'SLASHED', posix=False)._run() self.assertEqual(pipe.wait(), 0, 'Windows shell command not found!') (out, err) = pipe.communicate() self.assertEqual(out, b'Double Glazing Installed:SLASHED\n') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/tests/test_frequency.py new/python-crontab-3.0.0/tests/test_frequency.py --- old/python-crontab-2.7.1/tests/test_frequency.py 2022-12-22 05:40:45.000000000 +0100 +++ new/python-crontab-3.0.0/tests/test_frequency.py 2022-12-31 20:51:32.000000000 +0100 @@ -153,6 +153,42 @@ job.setall("*/2 * * * *") self.assertEqual(job.frequency_per_hour(), 30) + def test_17_frequency_at_hour(self): + """Frequency at hour at given moment""" + job = self.crontab.new(command='at_hour') + job.setall("*/2 10 * * *") + self.assertEqual(job.frequency_at_hour(2021, 7, 9, 10), 30) + self.assertEqual(job.frequency_at_hour(2021, 7, 9, 11), 0) + self.assertEqual(job.frequency_at_hour(), 30) + self.assertRaises(ValueError, job.frequency_at_hour, 2021) + + def test_18_frequency_at_day(self): + """Frequency per day at given moment""" + job = self.crontab.new(command='at_day') + job.setall("2,4 7 9,14 * *") + self.assertEqual(job.frequency_at_day(2021, 7, 9), 2) + self.assertEqual(job.frequency_at_day(2021, 7, 10), 0) + self.assertEqual(job.frequency_at_day(), 2) + self.assertRaises(ValueError, job.frequency_at_day, 2021) + + def test_19_frequency_at_month(self): + """Frequency per month at moment""" + job = self.crontab.new(command='at_month') + job.setall("2,4 9 7,14 10,11 *") + self.assertEqual(job.frequency_at_month(2021, 10), 4) + self.assertEqual(job.frequency_at_month(2021, 12), 0) + self.assertEqual(job.frequency_at_month(), 0) + self.assertRaises(ValueError, job.frequency_at_month, 2021) + + def test_20_frequency_at_year(self): + """Frequency at leap year day""" + job = self.crontab.new(command='at_year') + job.setall("0 * 3,29 2 *") + self.assertEqual(job.frequency_at_year(2021), 24) + self.assertEqual(job.frequency_at_year(2024), 48) + self.assertEqual(job.frequency_at_year(), 24) + + if __name__ == '__main__': test_support.run_unittest( FrequencyTestCase, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-crontab-2.7.1/tests/test_usage.py new/python-crontab-3.0.0/tests/test_usage.py --- old/python-crontab-2.7.1/tests/test_usage.py 2022-12-22 05:45:50.000000000 +0100 +++ new/python-crontab-3.0.0/tests/test_usage.py 2023-07-13 16:00:42.000000000 +0200 @@ -210,13 +210,14 @@ self.assertEqual(cronitem.render(specials=None), '@daily true') self.assertEqual(cronitem.render(specials=False), '0 0 * * * true') - def test_25_open_pipe(self): + def test_25_process(self): """Test opening pipes""" - from crontab import open_pipe, CRON_COMMAND - pipe = open_pipe(CRON_COMMAND, h=None, a='one', abc='two') - (out, err) = pipe.communicate() - self.assertEqual(err, b'') - self.assertEqual(out, b'--abc=two|-a|-h|one\n') + from crontab import Process, CRON_COMMAND + process = Process(CRON_COMMAND, h=None, a='one', abc='two').run() + self.assertEqual(int(process), 0) + self.assertEqual(repr(process)[:8], "Process(") + self.assertEqual(process.stderr, '') + self.assertEqual(process.stdout, '--abc=two|-a|-h|one\n') def test_07_zero_padding(self): """Can we get zero padded output"""
participants (1)
-
Source-Sync