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 <[email protected]>
+
+- 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 = '[email protected]',
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"""