././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5411143 python_crontab-3.2.0/0000775000175000017500000000000014640626622015055 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/AUTHORS0000664000175000017500000000004212640154132016107 0ustar00doctormodoctormoMartin Owens (doctormo@gmail.com) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1671683596.0 python_crontab-3.2.0/CHANGES.md0000644000175000017500000003435214350757014016452 0ustar00doctormodoctormo# Release notes This is a generated CHANGES file, based on the Git history. ## Version 2.5.7 (2019-07-11) - Updated RADME and special time setting fixes. - Attempt a fix of issue #27 (no test suite test) - Merge branch 'master' into 'master' - update README.rst file - Fix #48, specials can't be changed to another time. - Bump version for pypi update - Fix #41 dead link to github - Ignore mypy_cache - Fix #39 - Tab next to username will parse correctly - Merge remote-tracking branch 'flotus/patch-1' - Improve explaination about using the write function - fixed environment ## Version 2.3.5 (2018-09-03) - Bump version only for pypi - Bump version to fix pypi issues ## Version 2.3.4 (2018-09-03) - Fix some issues - Fix some pylint issues and adjust CronItem API - Update readme to point to GitLab - Fix #35: Test for specific time as reported. ## Version 2.3.3 (2018-05-25) - Fix remove command when using generators and bad types - Protect remove command from bad type use and allow generators to use it - Name of repr should be CronItem not CronJob ## Version 2.3.2 (2018-05-25) - Fix logging issues - Remove impropper log configuration from module to fix #32 ## Version 2.3.1 (2018-05-09) - Add line setter attribute handler - Provide a way to set lines directly ## Version 2.3.0 (2018-05-09) - Version 2.3.0 - Add logging tests, bump version, fix pylint issues and depricate py2.6 - Fix #22 with new test and range - Fix #16 by detecting mips uses - Add logo and title back in. ## Version 2.2.8 (light) - New version, rst readme and some bug fixes - Fix remaining ReST issues. - We're moving back to restructured text for the readme - Add description for environment variables - Merge commit '64461023c56916b2186086b4880ebd9c548dd40d' - Add explaination to parsing lines - Merge commit '637f85d1bf5be35ccfca8a8d8b6cc2513c5202c5' - Nope - Merge commit 'c7e1628dd209d30a7eb1a87b5cbf03a35edd35af' - Unpick the merge and use a global variable for zero padding. - Merge commit 'cf097be' - Fix #15 with a slightly modified rex ## Version 2.2.7 (2017-11-18) - ansible compatibility - Commit changes for 2.2.7 with ansible compatibility - Output environment variables as they were input - Do not try to parse comments - Do not try to attach comments to jobs - Allow zero-padding of non-dow items - Remove old changelog, use git log ## Version 2.2.6 (2017-11-02) - v2.2.6 - Fix issues with allowing lines that will cause errors to be rewritten back - Fix unlink command - Add tests from issues #11 ## Version 2.2.5 (2017-10-20) - quotes on empty variables - Fix #11 by always using quotes on empty variables ## Version 2.2.4 (2017-08-03) - env controls and schedule logic - Bump version - Add test for Rudolf's env patch - Actually pass crontab env variables to jobs - Immediately run pending cronjobs, don't defer them for 2 minutes ## Version 2.2.3 (2017-06-13) - sibling management - Bump version - Fix #7 by managing siblings better - Small improvements to the readme ## Version 2.2.2 (2017-05-04) - regex and spacing - Bump version number - Fix #6 with the addition of regular expression support in finding commands - Fix #5 with better space and quote management ## Version 2.2.1 (2017-04-28) - env fixes - Bump version to 2.2.1 - Fix #4 using a slighly modified moubctez patch - Add test for python3 mutations bug - Fix #3 which thought an empty env meant no link to previous env ## Version 2.2.0 (2017-04-22) - environment variables - Bump to new version for new feature - Remove env var test since it's not the right place and has been superseeded. - Use system wide coverage modules instead (not sure about this) - Update README with new information for env variable access - Add new environment handling to fix issue #1 ## Version 2.1.2 (2017-04-20) - -m - Up a version to push new github links to pypi - Improve markdown for github (will break pypi) - Move to github in readme - Warn when a cron range is backwards - Improve support for older python versions (2.6) with ordereddict use. - Fix iteration issue during removal of items. fixed bug #1628844 - Change timeout to -1, which means infinate no timeout for scheduler - Improve readme to help with bug#1638533 - Small fixes to tests ## Version 2.1.1 (2016-07-07) - unicode support - Bump version to 2.1.1. - Create test for and fix unicode issues with code from ~hung-allan fixes lp:1599372 - Add backwards range test - Add descriptor as optional setup.py extra ## Version 2.1.0 (2016-06-11) - cron-descriptor support - Add cron-descriptor support and fix wishlist bug - Improve cronslice code and tests ## Version 2.0.2 (2016-06-02) - sunday write loops - Fix sunday issues where cron ranges use ranges incorrectly. - try a few other asserts to see if it'll fail - Add a new test for bug 1172712 to make sure it's saving. - Add comment for new release variable - bdist_rpm support - Commit patch from Costas for new line variables bug lp:1542272 - Fix bug lp:1539178 and make iterators work a bit better - Improve readme to explain write command at the start. ## Version 2.0.1 (2016-01-04) - schedular - Version bump - Cover shell - Protect against an API misuse (from old documentation) - Allow python3 to work with tests, fix year issue (damn you leap year) - Add basic scheduler functionality, improve logging and shell support - Improve cronlog testings coverage ## Version 2.0.0 (2015-12-30) - system and test - Tests for python2 and python3 finally agree and don't warn. - Fix issues with python3 testing and python3 errors - Updated append so they all use append. - Improve code coverage and small refacotring of pipe - Updated readme for new crontabs module - Improve pylint a little bit in crontab module. - Add new crontabs module which allows listing and crontab discovery on a filesystem - Refactor spool directory in tests - Add CronTab object repr for system and user crontabs - Seperate out appending git items - Ignore swap files - Fix env variable name in test - Update readme to git - Add git ignore doing conversion - Update readme and change env api name - Tentative support for crontab variables - Make remove_all less agressive by making kwargs smarter - Improve user selection and stop specifying user if we are already that user. - Fix regression where comment data is duplicated. Test for it. - Support vixie/systemv crontab comments which proceed tabs - Add ability to understand date and datetime objects when setting cron slices - Small refectoring of the write_to_user to allow write to handle it anyway. - Make sure writing with no user/filename causes an error - Add test for new frequency ## Version 1.9.5 (2015-12-21) - testing too - Bump version - Add an explict frequency per hour - Remove weird monkey - Test the utf8 support better - Improve readme for new validation feature - Add slices validator to help when it's needed. Fix validation of special @based values which were very forgiving and could have led to once a day crons being saved as once a minute. ## Version 1.9.4 (2015-07-30) - Relicense from GPLv3 to LGPLv3 - Slight improvement to testing of every api use - Make the multiple field setting clearer and add a test - Allow croniter import test to work in python3 - Check for UTF-8 system language before doing utf-8 checks - Move project to LGPLv3 from GPLv3 to improve use of this module as a library ## Version 1.9.3 (2015-03-23) - Tests, coverage and fixes - Add test for user replication - Merge in Alan Wong's fix for the 6 part comment issue. - Handle system cron edge case when parsing 6-part comments - Add coverage testing script (not included in installer) - Fix cronlog line error discovered recently - Upgrade test coverage, runner and compatability. - Add comment headers - Fix python 3 compat issue. ## Version 1.9.2 (2015-01-19) - write_to_user fix - Fix write_to_user method and improve test case. Fix #1412316 ## Version 1.9.1 (2015-01-05) - username requirement - Fix username requirement for certain platforms ## Version 1.9.0 (2015-01-02) - vixie cron - Add vixie cron system crontab compatability mode ## Version 1.8.2 (2014-12-07) - UTF testing bugs - Fix utf filename bug when tests run in pip - Commit Jordan Metzmeier's testing flag patch - Add fixes from debian distribution. utf-8 file opening and some test tweaks ## Version 1.8.1 (2014-06-18) - Sundays, utf8 and zero sequences - Version bump - Add check for divide by zero on sequences - Fix sunday support and add some more testing for ranges - Merge in patch from Jakub Bug #1328951 and add test and more robustness for utf-8 handling. ## Version 1.8.0 (2014-05-22) - Set comment, tuples and utf8 - Updated readme and add set_comment function. - Improve UTF8 support with a new test suite and py2 and py3 uupdates - Check the tuples for empty attributes ## Version 1.7.3 (2014-05-05) - Warnings, pipe method and fixes - Revise user command to use a more generic pipe method - Flake8 fixes - Add warning about TypeError when using the wrong package. ## Version 1.7.2 (2014-03-04) - User tab testing and fixes - Add test for bug #1287412 and fix the error. bump version to 1.7.2 ## Version 1.7.1 (2014-02-06) - Windows critical error - Fix windows critical error. Bug #1276040 - Add write_to_user to readme - Bump python version support - Fix find by time and allow removal all by time - Add the final comparison feature - Further completion of the refactoring process - Add pylintrc and ignore debian - Put two frequency methods back which broke the documented api - Seperate out CronSlices from a simple list into it's own class, REFACTOR ## Version 1.7.0 (2014-01-04) - Parsed cron, frequency checking, saving as a user - Improve writing capability with write_to_user(user=X) function - Add missing specials test data - Fix log test for 2014 because it's not year sensitive - Merge in the very kind patch from Kevin Waddle - Fix no user cron exists error from rev 75 for bug #1258926 - Fix issue with non-iterable elements - Add a check to the specials test to make sure it can be iterated over - Apply patch from Harun and add test to the frequency suite - Fix no user cron exists error - Add is_match functions and find by match functions. - Update readme file for frequency features - Implement frequency feature and set 0.6.0 version - Fix missing day reference - New username detection added - Attempt to clean up according to pylint - Add test of frequency feature for future. - Some interesting cleanups - Add no fork request and cleaner header comment - Remove command complexity and test the commands and comments generators - Make comment setting/getting more sane and intergrate jonames() feature from ansible's python-crontab copy. - Add len (not sure about it though) - Fix some puthon3 issues and introduce a length for a crontab - Allow setall to set a parsed cron string ## Version 1.6.0 (2013-11-24) - frequency methods, username,pylint - Complete adding new features, a setall() method, a parse() method, and a flexible remove_all() method. With tests. ## Version 1.5.2 (2013-11-01) - Test count - Test segments with a count rather than a find - Add specials tab to test compatability - 23 hours not 24 ## Version 1.5.1 (2013-10-12) - Min and max and fixes - Improve min and max checking and add extra value to set them. Better testing and more fixes. - Updated docs for compatability mode - Improve compatability for SystemV crontabs - Move cronlog dep to allow setup to function ## Version 1.5.0 (2013-10-12) - Docs and API upgrades - Improve docs and impliment 'also' clause, bump version to 1.5 - Updated branding into the archive, but not the package, updated README with branding image. - Updated readme for pypi documentation web formatin ## Version 1.4.4 (2013-10-11) - Every method - Add every method to job to improve convience ## Version 1.4.3 (2013-07-28) - Nudges - Improve testing for python3 and patch in some nudges ## Version 1.4.2 (2013-05-28) - Line feeds and stderr - Bump version for next release when ready - Make sure out ending line-feed doesn't proliferate. - Pipe stderr in ## Version 1.4.1 (2013-04-10) - Fix dateutils and deps - Fix dateutils - Fix deps for setup utils - Move testing data into a directory - Updated tests for opening crontab ## Version 1.4.0 (2013-04-06) - Log reading and croniter functions - Complete log testing and fixes - First progress towards log parsing - Attempt to help windows users - Add optional croniter support, clean up some items, write test and update readme - Allow for empty lines - Protect comments that break detection - Minor corrections - Bug fixed by George Cox, thanks for the patch ## Version 1.3.2 (2013-03-13) - is_enabled and fixes - Spelling mistake - Enable and disable documentation - Add is_enabled ## Version 1.3.1 (2013-03-13) - Disable jobs - Disable and enable crontab jobs - Update readme file - Update readme file ## Version 1.3.0 (2013-03-12) - Comment finding - Ver 1.3 add comment finding and test rendering of comments. - Fix - Post readme to pypi - Update readme - Add loading of tabs from files - Ignore cache - Reinstate python3 comptability ## Version 1.2.0 (2012-09-25) - Enumerations - Create test for enums, fix enums and MANY other fixes - merge ## Version 1.1.0 (2012-09-19) - Python3 - Full python3 compatability built in - fix warnings in setup.py - fix usage test - Modify the mechanisms for internal communication to not convert strings so much. - Make sure the tests pass. - Add usage and improve compat testing - Add usage and improve compat testing - Update changelog ## Version 1.0.0 (2012-08-17) - New API - Bump version because of new api - Improve testing and api ## Version 0.9.7 (2012-08-15) - Older UNIX - Add in support for older unix machines thanks to Jay Sigbrandt for bring the problem to attention. - Fixed empty crontab bug, thanks to James. ## Version 0.9.6 (2010-12-17) - Fixes - Update with a new text. ## Version 0.9.5 (2010-10-20) - Net entries - Fix weird issue with making new entries. ## Version 0.9.4 (2010-02-20) - Find command - Update to latest - Update changelog ## Version 0.9.3 (2009-09-18) - Returns - Make sure the crontab has a CR at the end. ## Version 0.9.2 (2009-07-07) - Parse spaced entries - Make sure we can parse spaced entries ## Version 0.9.1 (2009-07-07) - Import to bzr - Inital import to bzr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/COPYING0000664000175000017500000001674312640154132016111 0ustar00doctormodoctormo GNU LESSER 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. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1584981178.0 python_crontab-3.2.0/MANIFEST.in0000644000175000017500000000050213636162272016606 0ustar00doctormodoctormoinclude AUTHORS include COPYING include README.rst include MANIFEST.in include CHANGES.md include setup.py include crontab.py include cronlog.py recursive-include tests *.empty recursive-include tests *.py recursive-include tests *.sh recursive-include tests *.txt include tests/data/spool/* include tests/data/crontabs/* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5411143 python_crontab-3.2.0/PKG-INFO0000644000175000017500000004160214640626622016153 0ustar00doctormodoctormoMetadata-Version: 2.1 Name: python-crontab Version: 3.2.0 Summary: Python Crontab API Home-page: https://gitlab.com/doctormo/python-crontab/ Author: Martin Owens Author-email: doctormo@gmail.com License: LGPLv3 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 Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Provides: crontab Provides: crontabs Provides: cronlog Description-Content-Type: text/x-rst License-File: COPYING License-File: AUTHORS Requires-Dist: python-dateutil Provides-Extra: cron-schedule Requires-Dist: croniter; extra == "cron-schedule" Provides-Extra: cron-description Requires-Dist: cron-descriptor; extra == "cron-description" Python Crontab -------------- .. image:: https://gitlab.com/doctormo/python-crontab/raw/master/branding.svg .. image:: https://badge.fury.io/py/python-crontab.svg :target: https://badge.fury.io/py/python-crontab .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg :target: https://gitlab.com/doctormo/python-crontab/raw/master/COPYING Bug Reports and Development =========================== Please report any problems to the `GitLab issues tracker `_. Please use Git and push patches to the `GitLab project code hosting `_. **Note:** If you get the error ``got an unexpected keyword argument 'user'`` 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 reading 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 ===================== Here is a simple example of how python-crontab is typically used. First the CronTab class is used to instantiate a cron object, then the cron object is used to declaratively manipulate the cron (spawning a new job in this case). Lastly, declared changes get written to the crontab by calling write on the object:: from crontab import CronTab cron = CronTab(user='root') job = cron.new(command='echo hello_world') job.minute.every(1) cron.write() Alternatively, you can use the with context manager which will automatically call write on the cron object upon exit:: with CronTab(user='root') as cron: job = cron.new(command='echo hello_world') job.minute.every(1) print('cron.write() was just executed') **Note:** Several users have reported their new crontabs not saving automatically or that the module doesn't do anything. You **MUST** use write() if you want your edits to be saved out. See below for full details on the use of the write function. 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.dow.on('SUN', 'FRI') 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 * * *') Setting the slice to a python date object:: job.setall(time(10, 2)) job.setall(date(2000, 4, 2)) job.setall(datetime(2000, 4, 2, 10, 2)) Run a jobs command. Running the job here will not effect it's existing schedule with another crontab process:: job_standard_output = job.run() Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Creating a job in the middle of the crontab:: job = cron.new('/bin/a', before='someID') job = cron.new('/bin/b', before=jobItem) job = cron.new('/bin/c', before=re.compile('id*')) job = cron.new('/bin/d', before=cron.find_command('/usr/bin/existing')) 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 is job.is_enabled() Validity Check:: True is job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command sub-match or regular expression:: iter = cron.find_command('bar') # matches foobar1 iter = cron.find_command(re.compile(r'b[ab]r$')) Find an existing job by comment exact match or regular expression:: iter = cron.find_comment('ID or some text') iter = cron.find_comment(re.compile(' or \w')) 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, this includes disabled (commented out) cron jobs:: for job in cron: print(job) Iterate through all lines, this includes all comments and empty 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' ) Validate a cron time string:: from crontab import CronSlices bool = CronSlices.is_valid('0/2 * * * *') Compare list of cron objects against another and return the difference:: difference = set([CronItem1, CronItem2, CronItem3]) - set([CronItem2, CronItem3]) Compare two CronItems for equality:: CronItem1 = CronTab(tab="* * * * * COMMAND # Example Job") CronItem2 = CronTab(tab="10 * * * * COMMAND # Example Job 2") if CronItem1 != CronItem2: print("Cronjobs do not match") Environment Variables ===================== Some versions of vixie cron support variables outside of the command line. Sometimes just update the envronment when commands are run, the Cronie fork of vixie cron also supports CRON_TZ which looks like a regular variable but actually changes the times the jobs are run at. Very old vixie crons don't support per-job variables, but most do. Iterate through cron level environment variables:: for (name, value) in cron.env.items(): print(name) print(value) Create new or update cron level environment variables:: print(cron.env['SHELL']) cron.env['SHELL'] = '/bin/bash' print(cron.env) Each job can also have a list of environment variables:: for job in cron: job.env['NEW_VAR'] = 'A' print(job.env) 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. Running the Scheduler ===================== The module is able to run a cron tab as a daemon as long as the optional croniter module is installed; each process will block and errors will be logged (new in 2.0). (note this functionality is new and not perfect, if you find bugs report them!) Running the scheduler:: tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): 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:: tab.run_scheduler() Timeout and cadence can be changed for testing or error management:: for result in tab.run_scheduler(timeout=600): print("Will run jobs every 1 minutes for ten minutes from now()") for result in tab.run_scheduler(cadence=1, warp=True): print("Will run jobs every 1 second, counting each second as 1 minute") 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 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 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" `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() == 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 ================= 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']) All System CronTabs Functionality ================================= The crontabs (note the plural) module can attempt to find all crontabs on the system. This works well for Linux systems with known locations for cron files and user spolls. It will even extract anacron jobs so you can get a picture of all the jobs running on your system:: from crontabs import CronTabs for cron in CronTabs(): print(repr(cron)) All jobs can be brought together to run various searches, all jobs are added to a CronTab object which can be used as documented above:: jobs = CronTabs().all.find_command('foo') 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. Descriptor Functionality ======================== If you have the cron-descriptor module installed, you will be able to ask for a translated string which describes the frequency of the job in the current locale language. This should be mostly human readable. print(job.description(use_24hour_time_format=True)) See cron-descriptor for details of the supported languages and options. Extra Support ============= - Customise the location of the crontab command by setting the global CRON_COMMAND or the per-object cron_command attribute. - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1689259909.0 python_crontab-3.2.0/README.rst0000664000175000017500000003700714454007605016550 0ustar00doctormodoctormoPython Crontab -------------- .. image:: https://gitlab.com/doctormo/python-crontab/raw/master/branding.svg .. image:: https://badge.fury.io/py/python-crontab.svg :target: https://badge.fury.io/py/python-crontab .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg :target: https://gitlab.com/doctormo/python-crontab/raw/master/COPYING Bug Reports and Development =========================== Please report any problems to the `GitLab issues tracker `_. Please use Git and push patches to the `GitLab project code hosting `_. **Note:** If you get the error ``got an unexpected keyword argument 'user'`` 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 reading 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 ===================== Here is a simple example of how python-crontab is typically used. First the CronTab class is used to instantiate a cron object, then the cron object is used to declaratively manipulate the cron (spawning a new job in this case). Lastly, declared changes get written to the crontab by calling write on the object:: from crontab import CronTab cron = CronTab(user='root') job = cron.new(command='echo hello_world') job.minute.every(1) cron.write() Alternatively, you can use the with context manager which will automatically call write on the cron object upon exit:: with CronTab(user='root') as cron: job = cron.new(command='echo hello_world') job.minute.every(1) print('cron.write() was just executed') **Note:** Several users have reported their new crontabs not saving automatically or that the module doesn't do anything. You **MUST** use write() if you want your edits to be saved out. See below for full details on the use of the write function. 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.dow.on('SUN', 'FRI') 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 * * *') Setting the slice to a python date object:: job.setall(time(10, 2)) job.setall(date(2000, 4, 2)) job.setall(datetime(2000, 4, 2, 10, 2)) Run a jobs command. Running the job here will not effect it's existing schedule with another crontab process:: job_standard_output = job.run() Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Creating a job in the middle of the crontab:: job = cron.new('/bin/a', before='someID') job = cron.new('/bin/b', before=jobItem) job = cron.new('/bin/c', before=re.compile('id*')) job = cron.new('/bin/d', before=cron.find_command('/usr/bin/existing')) 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 is job.is_enabled() Validity Check:: True is job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command sub-match or regular expression:: iter = cron.find_command('bar') # matches foobar1 iter = cron.find_command(re.compile(r'b[ab]r$')) Find an existing job by comment exact match or regular expression:: iter = cron.find_comment('ID or some text') iter = cron.find_comment(re.compile(' or \w')) 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, this includes disabled (commented out) cron jobs:: for job in cron: print(job) Iterate through all lines, this includes all comments and empty 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' ) Validate a cron time string:: from crontab import CronSlices bool = CronSlices.is_valid('0/2 * * * *') Compare list of cron objects against another and return the difference:: difference = set([CronItem1, CronItem2, CronItem3]) - set([CronItem2, CronItem3]) Compare two CronItems for equality:: CronItem1 = CronTab(tab="* * * * * COMMAND # Example Job") CronItem2 = CronTab(tab="10 * * * * COMMAND # Example Job 2") if CronItem1 != CronItem2: print("Cronjobs do not match") Environment Variables ===================== Some versions of vixie cron support variables outside of the command line. Sometimes just update the envronment when commands are run, the Cronie fork of vixie cron also supports CRON_TZ which looks like a regular variable but actually changes the times the jobs are run at. Very old vixie crons don't support per-job variables, but most do. Iterate through cron level environment variables:: for (name, value) in cron.env.items(): print(name) print(value) Create new or update cron level environment variables:: print(cron.env['SHELL']) cron.env['SHELL'] = '/bin/bash' print(cron.env) Each job can also have a list of environment variables:: for job in cron: job.env['NEW_VAR'] = 'A' print(job.env) 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. Running the Scheduler ===================== The module is able to run a cron tab as a daemon as long as the optional croniter module is installed; each process will block and errors will be logged (new in 2.0). (note this functionality is new and not perfect, if you find bugs report them!) Running the scheduler:: tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): 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:: tab.run_scheduler() Timeout and cadence can be changed for testing or error management:: for result in tab.run_scheduler(timeout=600): print("Will run jobs every 1 minutes for ten minutes from now()") for result in tab.run_scheduler(cadence=1, warp=True): print("Will run jobs every 1 second, counting each second as 1 minute") 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 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 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" `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() == 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 ================= 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']) All System CronTabs Functionality ================================= The crontabs (note the plural) module can attempt to find all crontabs on the system. This works well for Linux systems with known locations for cron files and user spolls. It will even extract anacron jobs so you can get a picture of all the jobs running on your system:: from crontabs import CronTabs for cron in CronTabs(): print(repr(cron)) All jobs can be brought together to run various searches, all jobs are added to a CronTab object which can be used as documented above:: jobs = CronTabs().all.find_command('foo') 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. Descriptor Functionality ======================== If you have the cron-descriptor module installed, you will be able to ask for a translated string which describes the frequency of the job in the current locale language. This should be mostly human readable. print(job.description(use_24hour_time_format=True)) See cron-descriptor for details of the supported languages and options. Extra Support ============= - Customise the location of the crontab command by setting the global CRON_COMMAND or the per-object cron_command attribute. - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1671684114.0 python_crontab-3.2.0/cronlog.py0000664000175000017500000000727514350760022017074 0ustar00doctormodoctormo# # Copyright 2013, Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Access logs in known locations to find information about them. """ import os import re import codecs import platform 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.*)\)' class LogReader(object): """Opens a Log file, reading backwards and watching for changes""" def __init__(self, filename, mass=4096): self.filename = filename self.mass = mass self.size = -1 self.read = -1 self.pipe = None def __enter__(self): self.size = os.stat(self.filename)[6] self.pipe = codecs.open(self.filename, 'r', encoding='utf-8') return self def __exit__(self, error_type, value, traceback): self.pipe.close() def __iter__(self): if self.pipe is None: with self as reader: for (offset, line) in reader.readlines(): yield line else: for (offset, line) in self.readlines(): yield line 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. 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) class CronLog(LogReader): """Use the LogReader to make a Cron specific log reader""" def __init__(self, filename='/var/log/syslog', user=None): LogReader.__init__(self, filename) self.user = user def for_program(self, command): """Return log entries for this specific command name""" return ProgramLog(self, command) def __iter__(self): for line in super(CronLog, self).__iter__(): match = re.match(MATCHER, str(line)) datum = match and match.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'] == str(self.command): yield entry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872403.0 python_crontab-3.2.0/crontab.py0000664000175000017500000014555114640625623017072 0ustar00doctormodoctormo# # Copyright 2021, Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # # pylint: disable=logging-format-interpolation,too-many-lines """ 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 shlex import types import codecs import logging import tempfile 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 from shutil import which __pkgname__ = 'python-crontab' __version__ = '3.2.0' 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 = [ {'max': 59, 'min': 0, 'name': 'Minutes'}, {'max': 23, 'min': 0, 'name': 'Hours'}, {'max': 31, 'min': 1, 'name': 'Day of Month'}, {'max': 12, 'min': 1, 'name': 'Month', 'enum': MONTH_ENUM}, {'max': 6, 'min': 0, 'name': 'Day of Week', 'enum': WEEK_ENUM}, ] # Detect Python3 and which OS for temperments. WINOS = platform.system() == 'Windows' POSIX = os.name == 'posix' SYSTEMV = not WINOS and os.uname()[0] in ["SunOS", "AIX", "HP-UX"] SYSTEMV = not WINOS and ( os.uname()[0] in ["SunOS", "AIX", "HP-UX"] or os.uname()[4] in ["mips"] ) # Switch this on if you want your crontabs to have zero padding. ZERO_PAD = False LOG = logging.getLogger('crontab') CRON_COMMAND = which("crontab") or "/usr/bin/crontab" SHELL = os.environ.get('SHELL', '/bin/sh') # The shell won't actually work on windows here, but # it should be updated later in the below conditional. # pylint: disable=W0622,invalid-name,too-many-public-methods # pylint: disable=function-redefined,too-many-instance-attributes current_user = lambda: None if not WINOS: import pwd def current_user(): """Returns the username of the current user""" return pwd.getpwuid(os.getuid())[0] def _str(text): """Convert to the best string format for this python version""" if isinstance(text, bytes): return text.decode('utf-8') 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. 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 self.cron_command = CRON_COMMAND self.env = None self._parked_env = OrderedDict() # 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.tabfile = tabfile self.read(tabfile) self._log = log def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.write() @property def log(self): """Returns the CronLog object for this tab (user or root tab only)""" from cronlog import CronLog # pylint: disable=import-outside-toplevel if self._log is None or isinstance(self._log, str): self._log = CronLog(self._log, user=self.user or 'root') return self._log @property def user(self): """Return user's username of this crontab if applicable""" if self._user is True: return current_user() return self._user @property def user_opt(self): """Returns the user option for the crontab commandline""" # 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: if self._user != current_user(): return {'u': self._user} return {} def __setattr__(self, name, value): """Catch setting crons and lines directly""" if name == 'lines' and value: for line in value: self.append(CronItem.from_line(line, cron=self), line, read=True) elif name == 'crons' and value: raise AttributeError("You can NOT set crons attribute directly") else: super().__setattr__(name, value) 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 = [] self.env = OrderedVariableList() 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: try: process = Process(self.cron_command, l='', **self.user_opt).run() except FileNotFoundError: raise IOError(f"Can't read crontab; No crontab program '{self.cron_command}'") 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 def append(self, item, line='', read=False, before=None): """Append a CronItem object to this CronTab Keyword arguments: item - The CronItem object to append line - The textual line which this item is. read - Internal use only before - Append before this CronItem, comment regex or generator """ cron_id = len(self.crons) line_id = len(self.lines) if isinstance(before, (str, type(ITEMREX))): before = self.find_comment(before) try: if isinstance(before, (list, tuple, types.GeneratorType)): *_, before = before if before is not None: cron_id = self.crons.index(before) line_id = self.lines.index(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) self._parked_env = OrderedDict() if read and not item.comment and self.lines and \ self.lines[-1] and self.lines[-1][0] == '#': item.set_comment(self.lines.pop()[1:].strip(), True) self.crons.insert(cron_id, item) self.lines.insert(line_id, item) elif '=' in line: if ' ' not in line or line.index('=') < line.index(' '): (name, value) = line.split('=', 1) value = value.strip() for quot in "\"'": if value[0] == quot and value[-1] == quot: value = value.strip(quot) break self._parked_env[name.strip()] = value else: if not self.crons and self._parked_env: self.env.update(self._parked_env) self._parked_env = OrderedDict() self.lines.append(line.replace('\n', '')) def write(self, filename=None, user=None, errors=False): """Write the crontab to it's source or a given filename.""" if filename: self.filen = filename elif user is not None: self.filen = None self.intab = None self._user = user # 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') # pylint: disable=consider-using-with else: filed, path = tempfile.mkstemp() fileh = os.fdopen(filed, 'wb') fileh.write(self.render(errors=errors).encode('utf-8')) fileh.close() if not self.filen: # Add the entire crontab back to the user crontab if not self.user: os.unlink(path) raise IOError("Please specify user or filename to write.") try: proc = Process(self.cron_command, path, **self.user_opt)._run() except FileNotFoundError: raise IOError(f"Can't write crontab, no crontab program '{self.cron_command}'") ret = proc.wait() if ret != 0: msg = proc.stderr.read() raise IOError(f"Program Error: {self.cron_command} returned {ret}: {msg}") proc.stdout.close() proc.stderr.close() os.unlink(path) def write_to_user(self, user=True): """Write the crontab to a user (or root) instead of a file.""" return self.write(user=user) def run_pending(self, **kwargs): """Run all commands in this crontab if pending (generator)""" for job in self: ret = job.run_pending(**kwargs) if ret not in [None, -1]: yield ret 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: now += timedelta(seconds=count * 60) for value in self.run_pending(now=now): yield value sleep(cadence) count += 1 def render(self, errors=False, specials=True): """Render this crontab as it would be in the crontab. errors - Should we not comment out invalid entries and cause errors? specials - Turn known times into keywords such as "@daily" True - (default) force all values to be converted (unless SYSTEMV) False - force all values back from being a keyword None - don't change the special keyword use """ crons = [] for line in self.lines: if isinstance(line, (str, str)): if line.strip().startswith('#') or not line.strip(): crons.append(line.strip()) elif not errors: crons.append('# DISABLED LINE\n# ' + line) else: raise ValueError(f"Invalid line: {line}") elif isinstance(line, CronItem): if not line.is_valid() and not errors: line.enabled = False crons.append(line.render(specials=specials).strip()) # 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) + '\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): # pylint: disable=too-many-arguments """ Create a new CronItem and append it to the cron. Keyword arguments: command - The command that will be run. comment - The comment that should be associated with this command. user - For system cron tabs, the user this command should run as. pre_comment - If true the comment will apear just before the command line. before - Append this command before this item instead of at the end. Returns the new CronItem object. """ if not user and self.user is False: raise ValueError("User is required for system crontabs.") item = CronItem(command, comment, user=user, pre_comment=pre_comment) item.cron = self self.append(item, before=before) return item def find_command(self, command): """Return an iter of jobs matching any part of the command.""" for job in list(self.crons): if isinstance(command, type(ITEMREX)): if command.findall(job.command): yield job elif 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 list(self.crons): if isinstance(comment, type(ITEMREX)): if comment.findall(job.comment): yield job elif comment == job.comment: yield job def find_time(self, *args): """Return an iter of jobs that match this time pattern""" for job in list(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, *args, **kwargs): """Removes all crons using the stated command OR that have the stated comment OR removes everything if no arguments specified. command - Remove all with this command comment - Remove all with this comment or ID time - Remove all with this time code """ if args: raise AttributeError("Invalid use: remove_all(command='cmd')") if 'command' in kwargs: return self.remove(*self.find_command(kwargs['command'])) if 'comment' in kwargs: return self.remove(*self.find_comment(kwargs['comment'])) if 'time' in kwargs: return self.remove(*self.find_time(kwargs['time'])) return self.remove(*self.crons[:]) def remove(self, *items): """Remove a selected cron from the crontab.""" result = 0 for item in items: if isinstance(item, (list, tuple, types.GeneratorType)): for subitem in item: result += self._remove(subitem) elif isinstance(item, CronItem): result += self._remove(item) else: raise TypeError("You may only remove CronItem objects, "\ "please use remove_all() to specify by name, id, etc.") return result def _remove(self, item): """Internal removal of an item""" # Manage siblings when items are deleted for sibling in self.lines[self.lines.index(item)+1:]: if isinstance(sibling, CronItem): env = sibling.env sibling.env = item.env sibling.env.update(env) sibling.env.job = sibling break if sibling != '': break self.lines.remove(sibling) self.crons.remove(item) self.lines.remove(item) return 1 def __repr__(self): kind = 'System ' if self._user is False else '' if self.filen: return f"<{kind}CronTab '{self.filen}'>" if self.user and not self.user_opt: return "" if self.user: return f"" return f"" def __iter__(self): """Return generator so we can track jobs after removal""" for job in list(self.crons.__iter__()): yield job def __getitem__(self, i): return self.crons[i] def __len__(self): return len(self.crons) def __str__(self): return self.render() class CronItem: """ An item which objectifies a single line of a crontab and May be considered to be a cron job object. """ def __init__(self, command='', comment='', user=None, pre_comment=False): self.cron = None self.user = user self.valid = False self.enabled = True self.special = False self.comment = None self.command = None self.last_run = None self.env = OrderedVariableList(job=self) # Marker labels Ansible jobs etc self.pre_comment = False self.marker = None self.stdin = None self._log = None # Initalise five cron slices using static info. self.slices = CronSlices() self.set_comment(comment, pre_comment) if command: self.set_command(command) def __hash__(self): return hash((self.command, self.comment, self.hour, self.minute, self.dow)) def __eq__(self, other): if not isinstance(other, CronItem): return False return self.__hash__() == other.__hash__() @classmethod def from_line(cls, line, user=None, cron=None): """Generate CronItem from a cron-line and parse out command and comment""" obj = cls(user=user) obj.cron = cron obj.parse(line.strip()) return obj def delete(self): """Delete this item and remove it from it's parent""" if not self.cron: raise UnboundLocalError("Cron item is not in a crontab!") self.cron.remove(self) def set_command(self, cmd, parse_stdin=False): """Set the command and filter as needed""" if parse_stdin: cmd = cmd.replace('%', '\n').replace('\\\n', '%') if '\n' in cmd: cmd, self.stdin = cmd.split('\n', 1) self.command = _str(cmd.strip()) self.valid = True def set_comment(self, cmt, pre_comment=False): """Set the comment and don't filter, pre_comment indicates comment appears before the cron, otherwise it appears ont he same line after the command. """ if cmt and cmt[:8] == 'Ansible:': self.marker = 'Ansible' cmt = cmt[8:].lstrip() pre_comment = True self.comment = cmt self.pre_comment = pre_comment def parse(self, line): """Parse a cron line string and save the info as the objects.""" line = _str(line) if not line or line[0] == '#': self.enabled = False line = line.strip().lstrip('#').strip() # We parse all lines so we can detect disabled entries. self._set_parse(ITEMREX.findall(line), line) self._set_parse(SPECREX.findall(line), line) def _set_parse(self, result, line=""): """Set all the parsed variables into the item""" if not result: return self.comment = result[0][-1] if self.cron.user is False: # Special flag to look for per-command user ret = result[0][-3].split(None, 1) self.set_command(ret[-1], True) if len(ret) == 2: self.user = ret[0] else: # Disabled jobs might be ordinary comments, so log as DEBUG level = logging.ERROR if self.enabled else logging.DEBUG self.valid = False self.enabled = False LOG.log(level, str("Missing user or command in system cron %s: %s"), '' if self.cron is None else (self.cron.tabfile or ''), line) else: self.set_command(result[0][-3], True) try: self.setall(*result[0][:-3]) except (ValueError, KeyError) as err: if self.enabled: LOG.error(str(err)) self.valid = False self.enabled = False 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, specials=True): """Render this set cron-job to a string""" if not self.is_valid() and self.enabled: raise ValueError('Refusing to render invalid crontab.' ' Disable to continue.') 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 + ' ' rend = self.slices.render(specials=specials) result = f"{rend} {user}{command}" if self.stdin: result += ' %' + self.stdin.replace('\n', '%') if not self.enabled: result = "# " + result if self.comment: comment = self.comment = _str(self.comment) if self.marker: comment = f"#{self.marker}: {comment}" else: comment = "# " + comment if SYSTEMV or self.pre_comment or self.stdin: result = comment + "\n" + result else: result += ' ' + comment return str(self.env) + 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 Every(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_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) """ 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 frequency_per_hour(self): """Returns the number of times this item will execute in any hour""" return self.slices.frequency_per_hour() def run_pending(self, now=None): """Runs the command if scheduled""" now = now or datetime.now() if self.is_enabled(): if self.last_run is None: self.last_run = now next_time = self.schedule(self.last_run).get_next() if next_time < now: self.last_run = now return self.run() return -1 def run(self): """Runs the given command as a pipe""" env = os.environ.copy() env.update(self.env.all()) shell = self.env.get('SHELL', SHELL) 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.""" if not date_from: date_from = datetime.now() try: # Croniter is an optional import from croniter.croniter import croniter # pylint: disable=import-outside-toplevel except ImportError as err: raise ImportError("Croniter not available. Please install croniter" " 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): """ Returns a description of the crontab's schedule (if available) **kw - Keyword arguments to pass to cron_descriptor (see docs) """ try: from cron_descriptor import ExpressionDescriptor # pylint: disable=import-outside-toplevel except ImportError as err: raise ImportError("cron_descriptor not available. Please install"\ "cron_descriptor python module via pip or your package manager") from err exdesc = ExpressionDescriptor(self.slices.clean_render(), **kw) return exdesc.get_description() @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 the day slice""" 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 f"" def __len__(self): return len(str(self)) def __getitem__(self, key): return self.slices[key] 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.render() class Every: """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 (key, name) in enumerate(['minute', 'hour', 'dom', 'month', 'dow', 'min', 'hour', 'day', 'moon', 'weekday']): setattr(self, name, self.set_attr(key % 5)) setattr(self, name+'s', self.set_attr(key % 5)) def set_attr(self, target): """Inner set target, returns function""" 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 key in range(target == 4 and 2 or target): self.slices[key].on('<') self.slices[target].every(self.unit) return innercall def year(self): """Special every year target""" if self.unit > 1: raise ValueError(f"Invalid value '{self.unit}', outside 1 year") 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): super().__init__([CronSlice(info) for info in S_INFO]) self.special = None self.setall(*args) self.is_valid = self.is_self_valid def is_self_valid(self, *args): """Object version of is_valid""" return CronSlices.is_valid(*(args or (self,))) @classmethod def is_valid(cls, *args): #pylint: disable=method-hidden """Returns true if the arguments are valid cron pattern""" try: return bool(cls(*args)) except (ValueError, KeyError): return False def setall(self, *slices): """Parses the various ways date/time frequency can be specified""" self.clear() if len(slices) == 1: (slices, self.special) = self._parse_value(slices[0]) if slices[0] == '@reboot': return if id(slices) == id(self): raise AssertionError("Can not set cron to itself!") for set_a, set_b in zip(self, slices): set_a.parse(set_b) @staticmethod def _parse_value(value): """Parse a single value into an array of slices""" if isinstance(value, str) and value: return CronSlices._parse_str(value) if isinstance(value, CronItem): return value.slices, None if isinstance(value, datetime): return [value.minute, value.hour, value.day, value.month, '*'], None if isinstance(value, time): return [value.minute, value.hour, '*', '*', '*'], None if isinstance(value, date): return [0, 0, value.day, value.month, '*'], None # 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)): typ = type(value).__name__ raise ValueError(f"Unknown type: {typ}") return value, None @staticmethod def _parse_str(value): """Parse a string which contains slice information""" key = value.lstrip('@').lower() if value.count(' ') == 4: return value.strip().split(' '), None if key in SPECIALS: return SPECIALS[key].split(' '), '@' + key if value.startswith('@'): raise ValueError(f"Unknown special '{value}'") return [value], None def clean_render(self): """Return just numbered parts of this crontab""" return ' '.join([str(s) for s in self]) def render(self, specials=True): "Return just the first part of a cron job (the numbers or special)" slices = self.clean_render() if self.special and specials is not False: if self.special == '@reboot' or \ SPECIALS[self.special.strip('@')] == slices: return self.special if not SYSTEMV and specials is True: for (name, value) in SPECIALS.items(): if value == slices and name not in SPECIAL_IGNORE: return f"@{name}" return slices def clear(self): """Clear the special and set values""" self.special = None for item in self: item.clear() 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 times this item will execute in a given year (default is this year)""" 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() + 1) % 7 in weekdays: result += 1 except ValueError: continue return result def frequency_per_day(self): """Returns the number of times this item will execute in any day""" return len(self[0]) * len(self[1]) def frequency_per_hour(self): """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() def __eq__(self, arg): return self.render() == CronSlices(arg).render() class SundayError(KeyError): """Sunday was specified as 7 instead of 0""" class Also: """Link range values together (appending instead of replacing)""" def __init__(self, obj): self.obj = obj def every(self, *a): """Also every one of these""" return self.obj.every(*a, also=True) def on(self, *a): """Also on these""" return self.obj.on(*a, also=True) def during(self, *a): """Also during these""" return self.obj.during(*a, also=True) class CronSlice: """Cron slice object which shows a time pattern""" def __init__(self, info, value=None): if isinstance(info, int): info = S_INFO[info] 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 __hash__(self): return hash(str(self)) def parse(self, value): """Set values into the slice.""" self.clear() if value is not None: for part in str(value).split(','): if part.find("/") > 0 or part.find("-") > 0 or part == '*': self.parts += self.get_range(part) continue self.parts.append(self.parse_value(part, sunday=0)) def render(self, resolve=False): """Return the slice rendered as a crontab. resolve - return integer values instead of enums (default False) """ if not self.parts: return '*' return _render_values(self.parts, ',', resolve) def __repr__(self): return f"" def __eq__(self, value): return str(self) == str(value) def __str__(self): return self.render() def every(self, n_value, also=False): """Set the every X units value""" n_value = self.test_value(n_value) if not also: self.clear() self.parts += 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 set_a in n_value: self.parts += (self.parse_value(set_a, sunday=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 += self.get_range(str(vfrom) + '-' + str(vto)) return self.parts[-1] @property def also(self): """Appends rather than replaces the new values""" return Also(self) def clear(self): """clear the slice ready for new vaues""" self.parts = [] def get_range(self, *vrange): """Return a cron range for this slice""" ret = CronRange(self, *vrange) if ret.dangling is not None: return [ret.dangling, ret] return [ret] def __iter__(self): """Return the entire element as an iterable""" ret = {} # 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(): ret[bit] = 1 else: ret[int(part)] = 1 for val in ret: yield val def __len__(self): """Returns the number of times this slice happens in it's range""" return len(list(self.__iter__())) def parse_value(self, val, sunday=None): """Parse the value of the cron slice and raise any errors needed""" if val == '>': val = self.max elif val == '<': val = self.min try: out = get_cronvalue(val, self.enum) 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 return self.test_value(out, sunday=sunday) def test_value(self, value, sunday=None): """Test the value is within range for this slice""" if self.max == 6 and int(value) == 7: if sunday is not None: return sunday raise SundayError("Detected Sunday as 7 instead of 0!") if int(value) < self.min or int(value) > self.max: raise ValueError(f"'{value}', not in {self.min}-{self.max} for {self.name}") return value def get_cronvalue(value, enums): """Returns a value as int (pass-through) or a special enum value""" if isinstance(value, int): return value if str(value).isdigit(): return int(str(value)) if not enums: raise KeyError("No enumeration allowed") return CronValue(str(value), enums) class CronValue: # pylint: disable=too-few-public-methods """Represent a special value in the cron line""" def __init__(self, value, enums): self.text = 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.text 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(f'{value:02d}' if ZERO_PAD else value) class CronRange: """A range between one value and another for a time range.""" def __init__(self, vslice, *vrange): # holds an extra dangling entry, for example sundays. self.dangling = None self.slice = vslice self.cron = None self.seq = 1 if not vrange: self.all() elif isinstance(vrange[0], str): self.parse(vrange[0]) elif isinstance(vrange[0], (int, 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('/') try: self.seq = self.slice.parse_value(seq) except SundayError: self.seq = 1 value = "0-0" if self.seq < 1 or self.seq > self.slice.max: raise ValueError("Sequence can not be divided by zero or max") if value.count('-') == 1: vfrom, vto = value.split('-') self.vfrom = self.slice.parse_value(vfrom, sunday=0) try: self.vto = self.slice.parse_value(vto) except SundayError: if self.vfrom == 1: self.vfrom = 0 else: self.dangling = 0 self.vto = self.slice.parse_value(vto, sunday=6) if self.vto < self.vfrom: raise ValueError(f"Bad range '{self.vfrom}-{self.vto}'") elif value == '*': self.all() else: raise ValueError(f'Unknown cron range value "{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: if self.vfrom == self.vto: value = str(self.vfrom) else: value = _render_values([self.vfrom, self.vto], '-', resolve) if self.seq != 1: value += f"/{self.seq:d}" if value != '*' and SYSTEMV: value = ','.join([str(val) for val in 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.render() class OrderedVariableList(OrderedDict): """An ordered dictionary with a linked list containing the previous OrderedVariableList which this list depends. Duplicates in this list are weeded out in favour of the previous list in the chain. This is all in aid of the ENV variables list which must exist one per job in the chain. """ def __init__(self, *args, **kw): self.job = kw.pop('job', None) super().__init__(*args, **kw) @property def previous(self): """Returns the previous env in the list of jobs in the cron""" if self.job is not None and self.job.cron is not None: index = self.job.cron.crons.index(self.job) if index == 0: return self.job.cron.env return self.job.cron[index-1].env return None def all(self): """ Returns the full dictionary, everything from this dictionary plus all those in the chain above us. """ if self.job is not None: ret = self.previous.all().copy() ret.update(self) return ret return self.copy() def __getitem__(self, key): previous = self.previous if key in self: return super().__getitem__(key) if previous is not None: return previous.all()[key] raise KeyError(f"Environment Variable '{key}' not found.") def __str__(self): """Constructs to variable list output used in cron jobs""" ret = [] for key, value in self.items(): if self.previous: if self.previous.all().get(key, None) == value: continue if ' ' in str(value) or value == '': value = f'"{value}"' ret.append(f"{key}={value}") ret.append('') return "\n".join(ret) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716356270.0 python_crontab-3.2.0/crontabs.py0000664000175000017500000001073214623302256017240 0ustar00doctormodoctormo# # Copyright 2016, Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ The crontabs manager will list all available crontabs on the system. """ import os import sys import itertools from os import stat, access, X_OK from crontab import CronTab, WINOS getpwuid = None if not WINOS: from pwd import getpwuid class UserSpool(list): """Generates all user crontabs, yields both owned and abandoned tabs""" def __init__(self, loc, tabs=None): for username in self.listdir(loc): tab = self.generate(loc, username) if tab: self.append(tab) if not self: tab = CronTab(user=True) if tab: self.append(tab) def listdir(self, loc): try: return os.listdir(loc) except OSError: return [] def get_owner(self, path): """Returns user file at path""" if not getpwuid: raise OSError("This functionality is not available on Windows") try: return getpwuid(stat(path).st_uid).pw_name except KeyError: return def generate(self, loc, username): path = os.path.join(loc, username) if username != self.get_owner(path): # Abandoned crontab pool entry! return CronTab(tabfile=path) return CronTab(user=username) class SystemTab(list): """Generates all system tabs""" def __init__(self, loc, tabs=None): if os.path.isdir(loc): for item in os.listdir(loc): if item[0] == '.': continue path = os.path.join(loc, item) self.append(CronTab(user=False, tabfile=path)) elif os.path.isfile(loc): self.append(CronTab(user=False, tabfile=loc)) class AnaCronTab(list): """Attempts to digest anacron entries (if possible)""" def __init__(self, loc, tabs=None): if tabs and os.path.isdir(loc): self.append(CronTab(user=False)) jobs = list(tabs.all.find_command(loc)) if jobs: for item in os.listdir(loc): self.add(loc, item, jobs[0]) jobs[0].delete() def add(self, loc, item, anajob): path = os.path.join(loc, item) if item in ['0anacron'] or item[0] == '.' or not access(path, X_OK): return job = self[0].new(command=path, user=anajob.user) job.set_comment('Anacron %s' % loc.split('.')[-1]) job.setall(anajob) return job # Files are direct, directories are listed (recursively) KNOWN_LOCATIONS = [ # Known linux locations (Debian, RedHat, etc) (UserSpool, '/var/spool/cron/crontabs/'), (SystemTab, '/etc/crontab'), (SystemTab, '/etc/cron.d/'), # Anacron digestion (we want to know more) (AnaCronTab, '/etc/cron.hourly'), (AnaCronTab, '/etc/cron.daily'), (AnaCronTab, '/etc/cron.weekly'), (AnaCronTab, '/etc/cron.monthly'), # Known MacOSX locations # None # Other (windows, bsd) # None ] class CronTabs(list): """Singleton dictionary of all detectable crontabs""" _all = None _self = None def __new__(cls, *args, **kw): if not cls._self: cls._self = super(CronTabs, cls).__new__(cls, *args, **kw) return cls._self def __init__(self): if not self: for loc in KNOWN_LOCATIONS: self.add(*loc) def add(self, cls, *args): for tab in cls(*args, tabs=self): self.append(tab) self._all = None @property def all(self): """Return a CronTab object with all jobs (read-only)""" if self._all is None: self._all = CronTab(user=False) for tab in self: for job in tab: if job.user is None: job.user = tab.user or 'unknown' self._all.append(job) return self._all ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5411143 python_crontab-3.2.0/python_crontab.egg-info/0000775000175000017500000000000014640626622021600 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872913.0 python_crontab-3.2.0/python_crontab.egg-info/PKG-INFO0000644000175000017500000004160214640626621022675 0ustar00doctormodoctormoMetadata-Version: 2.1 Name: python-crontab Version: 3.2.0 Summary: Python Crontab API Home-page: https://gitlab.com/doctormo/python-crontab/ Author: Martin Owens Author-email: doctormo@gmail.com License: LGPLv3 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 Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Provides: crontab Provides: crontabs Provides: cronlog Description-Content-Type: text/x-rst License-File: COPYING License-File: AUTHORS Requires-Dist: python-dateutil Provides-Extra: cron-schedule Requires-Dist: croniter; extra == "cron-schedule" Provides-Extra: cron-description Requires-Dist: cron-descriptor; extra == "cron-description" Python Crontab -------------- .. image:: https://gitlab.com/doctormo/python-crontab/raw/master/branding.svg .. image:: https://badge.fury.io/py/python-crontab.svg :target: https://badge.fury.io/py/python-crontab .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg :target: https://gitlab.com/doctormo/python-crontab/raw/master/COPYING Bug Reports and Development =========================== Please report any problems to the `GitLab issues tracker `_. Please use Git and push patches to the `GitLab project code hosting `_. **Note:** If you get the error ``got an unexpected keyword argument 'user'`` 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 reading 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 ===================== Here is a simple example of how python-crontab is typically used. First the CronTab class is used to instantiate a cron object, then the cron object is used to declaratively manipulate the cron (spawning a new job in this case). Lastly, declared changes get written to the crontab by calling write on the object:: from crontab import CronTab cron = CronTab(user='root') job = cron.new(command='echo hello_world') job.minute.every(1) cron.write() Alternatively, you can use the with context manager which will automatically call write on the cron object upon exit:: with CronTab(user='root') as cron: job = cron.new(command='echo hello_world') job.minute.every(1) print('cron.write() was just executed') **Note:** Several users have reported their new crontabs not saving automatically or that the module doesn't do anything. You **MUST** use write() if you want your edits to be saved out. See below for full details on the use of the write function. 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.dow.on('SUN', 'FRI') 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 * * *') Setting the slice to a python date object:: job.setall(time(10, 2)) job.setall(date(2000, 4, 2)) job.setall(datetime(2000, 4, 2, 10, 2)) Run a jobs command. Running the job here will not effect it's existing schedule with another crontab process:: job_standard_output = job.run() Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Creating a job in the middle of the crontab:: job = cron.new('/bin/a', before='someID') job = cron.new('/bin/b', before=jobItem) job = cron.new('/bin/c', before=re.compile('id*')) job = cron.new('/bin/d', before=cron.find_command('/usr/bin/existing')) 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 is job.is_enabled() Validity Check:: True is job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command sub-match or regular expression:: iter = cron.find_command('bar') # matches foobar1 iter = cron.find_command(re.compile(r'b[ab]r$')) Find an existing job by comment exact match or regular expression:: iter = cron.find_comment('ID or some text') iter = cron.find_comment(re.compile(' or \w')) 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, this includes disabled (commented out) cron jobs:: for job in cron: print(job) Iterate through all lines, this includes all comments and empty 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' ) Validate a cron time string:: from crontab import CronSlices bool = CronSlices.is_valid('0/2 * * * *') Compare list of cron objects against another and return the difference:: difference = set([CronItem1, CronItem2, CronItem3]) - set([CronItem2, CronItem3]) Compare two CronItems for equality:: CronItem1 = CronTab(tab="* * * * * COMMAND # Example Job") CronItem2 = CronTab(tab="10 * * * * COMMAND # Example Job 2") if CronItem1 != CronItem2: print("Cronjobs do not match") Environment Variables ===================== Some versions of vixie cron support variables outside of the command line. Sometimes just update the envronment when commands are run, the Cronie fork of vixie cron also supports CRON_TZ which looks like a regular variable but actually changes the times the jobs are run at. Very old vixie crons don't support per-job variables, but most do. Iterate through cron level environment variables:: for (name, value) in cron.env.items(): print(name) print(value) Create new or update cron level environment variables:: print(cron.env['SHELL']) cron.env['SHELL'] = '/bin/bash' print(cron.env) Each job can also have a list of environment variables:: for job in cron: job.env['NEW_VAR'] = 'A' print(job.env) 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. Running the Scheduler ===================== The module is able to run a cron tab as a daemon as long as the optional croniter module is installed; each process will block and errors will be logged (new in 2.0). (note this functionality is new and not perfect, if you find bugs report them!) Running the scheduler:: tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): 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:: tab.run_scheduler() Timeout and cadence can be changed for testing or error management:: for result in tab.run_scheduler(timeout=600): print("Will run jobs every 1 minutes for ten minutes from now()") for result in tab.run_scheduler(cadence=1, warp=True): print("Will run jobs every 1 second, counting each second as 1 minute") 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 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 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" `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() == 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 ================= 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']) All System CronTabs Functionality ================================= The crontabs (note the plural) module can attempt to find all crontabs on the system. This works well for Linux systems with known locations for cron files and user spolls. It will even extract anacron jobs so you can get a picture of all the jobs running on your system:: from crontabs import CronTabs for cron in CronTabs(): print(repr(cron)) All jobs can be brought together to run various searches, all jobs are added to a CronTab object which can be used as documented above:: jobs = CronTabs().all.find_command('foo') 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. Descriptor Functionality ======================== If you have the cron-descriptor module installed, you will be able to ask for a translated string which describes the frequency of the job in the current locale language. This should be mostly human readable. print(job.description(use_24hour_time_format=True)) See cron-descriptor for details of the supported languages and options. Extra Support ============= - Customise the location of the crontab command by setting the global CRON_COMMAND or the per-object cron_command attribute. - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872913.0 python_crontab-3.2.0/python_crontab.egg-info/SOURCES.txt0000664000175000017500000000213614640626621023465 0ustar00doctormodoctormoAUTHORS CHANGES.md COPYING MANIFEST.in README.rst cronlog.py crontab.py crontabs.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_context.py tests/test_croniter.py tests/test_crontabs.py tests/test_description.py tests/test_enums.py tests/test_env.py tests/test_equality.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_scheduler.py tests/test_system_cron.py tests/test_usage.py tests/test_utf8.py tests/utils.py tests/data/basic.log tests/data/crontab tests/data/crontest tests/data/specials.tab tests/data/specials_enc.tab tests/data/test.log tests/data/test.tab tests/data/anacron/an_command.sh tests/data/anacron/not_command.txt tests/data/crontabs/.empty tests/data/crontabs/system_one tests/data/crontabs/system_two tests/data/spool/basic tests/data/spool/hgreen tests/data/spool/jgreen tests/data/spool/user././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872913.0 python_crontab-3.2.0/python_crontab.egg-info/dependency_links.txt0000664000175000017500000000000114640626621025645 0ustar00doctormodoctormo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872913.0 python_crontab-3.2.0/python_crontab.egg-info/requires.txt0000664000175000017500000000011614640626621024175 0ustar00doctormodoctormopython-dateutil [cron-description] cron-descriptor [cron-schedule] croniter ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872913.0 python_crontab-3.2.0/python_crontab.egg-info/top_level.txt0000664000175000017500000000003114640626621024323 0ustar00doctormodoctormocronlog crontab crontabs ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5411143 python_crontab-3.2.0/setup.cfg0000664000175000017500000000004614640626622016676 0ustar00doctormodoctormo[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1689259961.0 python_crontab-3.2.0/setup.py0000775000175000017500000000557514454007671016606 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2008-2018 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # # pylint: disable=bad-whitespace """Setup the crontab module""" import os from setuptools import setup from crontab import __version__, __pkgname__ # 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.rst') as fhl: description = fhl.read() # Used for rpm building RELEASE = "1" setup( name = __pkgname__, version = __version__, 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', test_suite = 'tests', platforms = 'linux', license = 'LGPLv3', py_modules = ['crontab', 'crontabs', 'cronlog'], provides = ['crontab', 'crontabs', 'cronlog'], install_requires = ['python-dateutil'], extras_require = { 'cron-schedule': ['croniter'], 'cron-description': ['cron-descriptor'], }, 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 Lesser General Public License v3 or later (LGPLv3+)', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: SunOS/Solaris', 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ], options = { 'bdist_rpm': { 'build_requires': [ 'python', 'python-setuptools', ], 'release': RELEASE, }, }, ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719872913.521114 python_crontab-3.2.0/tests/0000775000175000017500000000000014640626622016217 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451423358.0 python_crontab-3.2.0/tests/__init__.py0000664000175000017500000000000112640573176020322 0ustar00doctormodoctormo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5371144 python_crontab-3.2.0/tests/data/0000775000175000017500000000000014640626622017130 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5371144 python_crontab-3.2.0/tests/data/anacron/0000775000175000017500000000000014640626622020551 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451339331.0 python_crontab-3.2.0/tests/data/anacron/an_command.sh0000755000175000017500000000000012640327103023156 0ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451339346.0 python_crontab-3.2.0/tests/data/anacron/not_command.txt0000664000175000017500000000000012640327122023565 0ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/basic.log0000664000175000017500000000015412640154132020702 0ustar00doctormodoctormoA really long line which can test log lines ability to put two bits together First Line 9 2 Sickem The End ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451329044.0 python_crontab-3.2.0/tests/data/crontab0000664000175000017500000000006112640303024020462 0ustar00doctormodoctormo * * * * * peter parker * 2 * * * driver parker ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5371144 python_crontab-3.2.0/tests/data/crontabs/0000775000175000017500000000000014640626622020743 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451339223.0 python_crontab-3.2.0/tests/data/crontabs/.empty0000664000175000017500000000000012640326727022071 0ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451326648.0 python_crontab-3.2.0/tests/data/crontabs/system_one0000664000175000017500000000006212640276270023047 0ustar00doctormodoctormo * * * * * bilbo baggins * 2 * * * frodo baggins ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451328298.0 python_crontab-3.2.0/tests/data/crontabs/system_two0000664000175000017500000000003412640301452023065 0ustar00doctormodoctormo 1 2 3 * * plastic baggins ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1671683431.0 python_crontab-3.2.0/tests/data/crontest0000775000175000017500000000252614350756547020734 0ustar00doctormodoctormo#!/usr/bin/env python3 import os import sys def main(): """Run this""" args = sys.argv[1:] loc = os.path.dirname(__file__) if '-h' in args: print('|'.join(sorted(args))) return elif '-e' in args: sys.stderr.write('|'.join(sorted(args)) + "\n") return elif '-ev' in args: print(os.environ.get('CR_VAR', 'FAILED') + '\n') return user = 'user' if '-u' in args: user = args[args.index('-u')+1] if user == 'john': sys.stderr.write("Program Error") sys.exit(2) if '-l' in args: if user == 'error': raise ValueError("Delibrate IO Error") if not os.path.exists(os.path.join(loc, 'spool', user)): sys.stderr.write("no crontab for %s\n" % user) sys.exit(1) fhl = open(os.path.join(loc, 'spool', user), 'r') print(fhl.read()) return for filename in args: if filename[0] == '-' or filename == user: continue new_name = os.path.join(loc, 'spool', user) if not os.path.exists(filename): raise KeyError("Can't find file: {}".format(filename)) with open(filename, 'r') as source: with open(new_name, 'w') as output: output.write(source.read()) if __name__ == '__main__': main() sys.exit(0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/specials.tab0000664000175000017500000000006212640154132021407 0ustar00doctormodoctormo0 * * * * hourly 0 0 * * * daily 0 0 * * 0 weekly ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/specials_enc.tab0000664000175000017500000000011512640154132022233 0ustar00doctormodoctormo@hourly hourly @daily daily @midnight midnight @weekly weekly @reboot reboot ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719872913.5411143 python_crontab-3.2.0/tests/data/spool/0000775000175000017500000000000014640626622020264 5ustar00doctormodoctormo././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/spool/basic0000664000175000017500000000002712640154132021255 0ustar00doctormodoctormo0 * * * * firstcommand ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451329564.0 python_crontab-3.2.0/tests/data/spool/hgreen0000664000175000017500000000012212640304034021436 0ustar00doctormodoctormo # Hank Green's crontab 0 0 5 */2 * start_company 0 0 * * 6 do_vlog_brothers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451329395.0 python_crontab-3.2.0/tests/data/spool/jgreen0000664000175000017500000000011712640303563021452 0ustar00doctormodoctormo # John Green's crontab 0 */2 * * * write_book 0 0 * * 2 do_vlog_brothers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/spool/user0000664000175000017500000000005212640154132021150 0ustar00doctormodoctormo */4 * * * * user_command # user_comment ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1451284570.0 python_crontab-3.2.0/tests/data/test.log0000664000175000017500000000455112640154132020605 0ustar00doctormodoctormoApr 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716357742.0 python_crontab-3.2.0/tests/data/test.tab0000664000175000017500000000072714623305156020602 0ustar00doctormodoctormo# 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 # monitorCron */59 * * * * python /example_app/testing.py @reboot rebooted # re-id # Last Comment @has this # extra # percent_one * * * * * echo \% escaped # percent_two * * * * * cat \%\% % stdin%with%lines ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_compatibility.py0000664000175000017500000001025314515021651022472 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2012 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab interaction. """ import os import sys import unittest import crontab TEST_DIR = os.path.dirname(__file__) INITAL_TAB = """ # First Comment 0,30 * * * * firstcommand """ class UserTestCase(unittest.TestCase): def test_01_self_user(self): tab = crontab.CronTab(user='foo') self.assertEqual(tab.user_opt, {'u': 'foo'}) tab = crontab.CronTab(user=crontab.current_user()) self.assertEqual(tab.user_opt, {}) 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 """) def test_04_comments(self): """Comments should be on their own lines""" self.assertEqual(self.crontab[0].comment, 'First Comment') self.assertEqual(self.crontab.render(), INITAL_TAB) job = self.crontab.new('command', comment="Test comment") self.assertEqual(job.render(), "# Test comment\n* * * * * command") def test_05_ansible(self): """Crontab shouldn't break ansible cronjobs""" cron = crontab.CronTab(tab=""" #Ansible: {job_name} * * * * * {command} """) self.assertEqual(cron[0].comment, '{job_name}') self.assertEqual(cron[0].command, '{command}') self.assertEqual(str(cron[0]), '#Ansible: {job_name}\n* * * * * {command}') def test_06_escaped_chars(self): """Do escaped chars parse correctly when read in""" cron = crontab.CronTab(tab=""" * * * * * cmd arg_with_\\#_character # comment """) self.assertEqual(cron[0].command, 'cmd arg_with_\\#_character') self.assertEqual(cron[0].comment, 'comment') def test_07_non_posix_shell(self): """Shell in windows environments is split correctly""" from crontab import Process winfile = os.path.join(TEST_DIR, 'data', "bash\\win.exe") 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') self.assertEqual(err, b'') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( CompatTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_context.py0000664000175000017500000000403414515021651021305 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2011 Martin Owens, 2019 Jordan Miller # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab context. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab INITAL_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,FRI,WED multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ RESULT_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,WED,FRI multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ class ContextTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB) def test_01_context(self): """context matches non-context""" self.crontab.write() results = RESULT_TAB.split('\n') for line_no, line in enumerate(self.crontab.intab.split('\n')): self.assertEqual(str(line), results[line_no]) with CronTab(tab=INITAL_TAB) as crontab_context: self.crontab_context = crontab_context for line_no, line in enumerate(self.crontab_context.intab.split('\n')): self.assertEqual(str(line), results[line_no]) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( ContextTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_croniter.py0000664000175000017500000000555314515021651021455 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ 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 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 imported before. for i in range(1, 4): name = '.'.join((['croniter'] * i)) if name in sys.modules: del sys.modules[name] 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__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( CroniterTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_crontabs.py0000664000175000017500000000736714515021651021450 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontabs listing all available crontabs. """ import os import sys import unittest import crontab import crontabs TEST_DIR = os.path.dirname(__file__) class fake_getpwuid(object): def __init__(self, st): self.pw_name = st if st == 'error': raise KeyError("Expected Error") class fake_stat(object): def __init__(self, path): self.st_uid = os.path.basename(path).split('_')[0] crontabs.getpwuid = fake_getpwuid crontabs.stat = fake_stat class AnacronSourceTab(list): """This enables the anacron detection""" slices = "56 4 * * 4" def __init__(self, tabs=None): self.append(crontab.CronTab(user=False, tab="%s root anacron_cmd %s" \ % (self.slices, os.path.join(TEST_DIR, 'data', 'anacron')))) crontabs.KNOWN_LOCATIONS = [ (AnacronSourceTab,), (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'spool')), (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'bad_spool')), (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontab')), (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontabs')), (crontabs.AnaCronTab, os.path.join(TEST_DIR, 'data', 'anacron')), ] crontab.CRON_COMMAND = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest')) class CronTabsTestCase(unittest.TestCase): """Test use documentation in crontab.""" def setUp(self): self.tabs = crontabs.CronTabs() def assertInTabs(self, command, *users): jobs = list(self.tabs.all.find_command(command)) self.assertEqual(len(jobs), len(users)) users = sorted([job.user for job in jobs]) self.assertEqual(users, sorted(users)) return jobs def assertNotInTabs(self, command): return self.assertInTabs(command) def test_05_spool(self): """Test a user spool""" self.assertInTabs('do_vlog_brothers', 'hgreen', 'jgreen') def test_06_bad_spool(self): """Test no access to spool (non-root)""" # IMPORTANT! This is testing the fact that the bad-spool will load # the user's own crontab, in this instance this is 'user' from the # crontest script. This tab is already loaded by the previous User # spool and so we expect to find two of them. self.assertInTabs('user_command', 'user', 'user') def test_10_crontab_dir(self): """Test crontabs loaded from system directory""" self.assertInTabs('baggins', 'bilbo', 'frodo', 'plastic') def test_11_crontab_file(self): """Test a single crontab file loaded from system tab""" self.assertInTabs('parker', 'driver', 'peter') def test_20_anacron(self): """Anacron digested""" self.assertNotInTabs('anacron_cmd') jobs = self.assertInTabs('an_command.sh', 'root') self.assertEqual(str(jobs[0].slices), AnacronSourceTab.slices) self.assertNotInTabs('not_command.txt') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(CronTabsTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_description.py0000664000175000017500000000401214515021651022140 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test optional crontab description intergration. """ import sys sys.path.insert(0, '../') import unittest import crontab INITAL_TAB = """ # Basic Comment 20 10-20 */2 * * execute # At 20 minutes past the hour, between 10:00 AM and 08:59 PM, every 2 days """ class DescriptorTestCase(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 cron_descriptor except ImportError: self.skipTest("Cron-descriptor module not installed") def test_00_no_module(self): """No module found""" # Remove module if imported already for i in range(1, 4): name = '.'.join((['cron_descriptor'] * i)) if name in sys.modules: del sys.modules[name] old, sys.path = sys.path, [] with self.assertRaises(ImportError): self.job.description() sys.path = old def test_01_description(self): """Get Job Description""" self.assertEqual(self.job.description(), self.job.comment) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(DescriptorTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_enums.py0000664000175000017500000000704114515021651020751 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2011 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab enumerations. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab INITAL_TAB = """ * * * JAN SAT enums * * * MAR-APR * 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__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( EnumTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716358184.0 python_crontab-3.2.0/tests/test_env.py0000664000175000017500000001407114623306050020412 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2017 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test the creation, reading and writing on environment variables. """ import os import sys sys.path.insert(0, '../') from .test_usage import TEST_DIR import unittest from collections import OrderedDict from crontab import CronTab INITIAL_TAB = """PERSONAL_VAR=bar CRON_VAR=fork 34 12 * * * eat_soup CRON_VAR=spoon 35 12 * * * eat_salad CRON_VAR=knife 36 12 * * * eat_icecream SECONDARY=fork 38 12 * * * eat_steak """ class EnvTestCase(unittest.TestCase): """Test vixie cron user addition.""" def setUp(self): self.crontab = CronTab(tab=INITIAL_TAB) def test_01_consistancy(self): """Read in crontab and write out the same""" self.assertEqual(INITIAL_TAB, str(self.crontab)) def test_02_top_vars(self): """Whole file env variables""" crontab = CronTab(tab="SHELL=dash\n") self.assertEqual(str(crontab), "SHELL=dash\n") self.assertEqual(crontab.env['SHELL'], 'dash') crontab.env['SHELL'] = 'bash' self.assertEqual(str(crontab), "SHELL=bash\n") self.assertEqual(crontab.env['SHELL'], 'bash') def test_03_get_job_var(self): """Test each of the job env structures""" for job, expected in zip(self.crontab, [ {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'fork'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'spoon'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife', 'SECONDARY': 'fork'}, ]): self.assertEqual(OrderedDict(job.env.all()), expected) def test_04_set_job_var(self): """Test that variables set are applied correctly""" self.crontab[1].env['CRON_VAR'] = 'javlin' self.assertEqual(self.crontab[1].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'}) self.crontab[2].env['CRON_VAR'] = 'javlin' self.assertEqual(self.crontab[2].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'}) self.assertEqual(self.crontab[3].env['PERSONAL_VAR'], 'bar') self.assertEqual(self.crontab[3].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin', 'SECONDARY': 'fork'}) self.crontab.env['PERSONAL_VAR'] = 'foo' self.assertEqual(self.crontab[2].env.all(), {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'javlin'}) self.crontab.env['CRON_VAR'] = 'fork' self.assertEqual(self.crontab[0].env.all(), {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'fork'}) self.assertEqual(str(self.crontab), """PERSONAL_VAR=foo CRON_VAR=fork 34 12 * * * eat_soup CRON_VAR=javlin 35 12 * * * eat_salad 36 12 * * * eat_icecream SECONDARY=fork 38 12 * * * eat_steak """) def test_05_no_env(self): """Test that we get an error asking for no var""" with self.assertRaises(KeyError): self.crontab.env['BLUE_BOTTLE'] with self.assertRaises(KeyError): self.crontab[0].env['RED_BOTTLE'] def test_06_env_access(self): cron = CronTab(tab=""" MYNAME='Random' * * * * * echo "first: $MYNAME" * * * * * echo "second: $MYNAME" * * * * * echo "third: $MYNAME" """) for job in cron: self.assertEqual(job.env['MYNAME'], "Random") def test_07_mutated_dict(self): """Test when the ordered dict is changed during loop""" cron = CronTab(tab=""" ALL='all' ABCD='first' * * * * * echo "first" """) def test_08_space_quotes(self): """Test that spaces and quotes are handled correctly""" cron = CronTab(tab=""" A= 123 B=" 123 " C=' 123 ' D= " 123 " E= 1 2 3 """) self.assertEqual(cron.env['A'], '123') self.assertEqual(cron.env['B'], ' 123 ') self.assertEqual(cron.env['C'], ' 123 ') self.assertEqual(cron.env['D'], ' 123 ') self.assertEqual(cron.env['E'], '1 2 3') self.assertEqual(str(cron), """A=123 B=" 123 " C=" 123 " D=" 123 " E="1 2 3" """) def test_09_delete_middle(self): """Test that a delete doesn't remove vars""" self.crontab.remove_all(command='eat_icecream') self.crontab.remove_all(command='eat_soup') self.assertEqual(str(self.crontab), """PERSONAL_VAR=bar CRON_VAR=spoon 35 12 * * * eat_salad CRON_VAR=knife SECONDARY=fork 38 12 * * * eat_steak """) def test_10_empty_env(self): """Test when an env is an empty string it should have quotes""" tab='MAILTO=""\n' self.assertEqual(str(CronTab(tab=tab)), tab) def test_11_empty_flow(self): """Test what happends when an env is involved in flow""" tab = """ # A # B MAILTO="" # C */10 * * * * /home/pi/job.py # any job """ cron = CronTab(tab=tab) job = cron.new('update.py', 'update') job.setall('1 12 * * 3') self.assertTrue(job.is_valid()) self.assertEqual(str(cron), """MAILTO="" # A # B # C */10 * * * * /home/pi/job.py # any job 1 12 * * 3 update.py # update """) cron.remove_all(comment='update') cron.write_to_user("bob") filename = os.path.join(TEST_DIR, 'data', 'spool', 'bob') with open(filename, 'r') as fhl: self.assertEqual(fhl.read(), """MAILTO="" # A # B # C */10 * * * * /home/pi/job.py # any job """) os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(EnvTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_equality.py0000775000175000017500000000405614515021651021465 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2019 Kyle Bakker # Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test equality between CronItems """ import unittest from crontab import CronTab CRONTAB1 = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 """ CRONTAB2 = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 4 * * * * command5 # CommentID K """ class EqualityTestCase(unittest.TestCase): """ Testing equality against CronItems""" def setUp(self): self.crontab1 = CronTab(tab=CRONTAB1) self.crontab2 = CronTab(tab=CRONTAB2) def test_equality(self): """Test equality""" self.assertEqual(self.crontab1[0], self.crontab2[0]) def test_inequality(self): """Test inequality""" self.assertNotEqual(self.crontab1[0], self.crontab2[1]) def test_listdifference(self): """Test diference between lists""" self.assertEqual(set(self.crontab2) - set(self.crontab1), {self.crontab2[3]}) def test_hash(self): """Test object hashing""" self.assertEqual(self.crontab1[0].__hash__(), hash((self.crontab1[0]))) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(EqualityTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_every.py0000664000175000017500000000653314515021651020761 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test simple 'every' api. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab TEST_DIR = os.path.dirname(__file__) 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 * * * *') job.minutes.every(5) self.assertEqual(job.slices.clean_render(), '*/5 * * * *') 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(), '* * * * *') def test_08_newitem(self): """Every on New Item""" job = self.crontab.new(command='hourly') job.every().hour() self.assertEqual(job.slices.render(), '@hourly') job = self.crontab.new(command='firstly') job.hours.every(2) self.assertEqual(job.slices.render(), '* */2 * * *') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( EveryTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716355788.0 python_crontab-3.2.0/tests/test_frequency.py0000664000175000017500000001626514623301314021630 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test frequency calculations """ import os import sys from calendar import isleap from datetime import date sys.path.insert(0, '../') import unittest from crontab import CronTab 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=2010), 11) self.assertEqual(self.job.frequency_per_year(year=2013), 12) self.assertEqual(self.job.frequency_per_year(year=2020), 13) 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(2010), 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(2010), 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") def test_16_frequency_per_hour(self): """Count per hour""" job = self.crontab.new(command='per_hour') 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.assertIn(job.frequency_at_month(), [0, 4]) 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, 48][isleap(date.today().year)]) def test_21_bad_frequency(self): """Frequency must be within range""" job = self.crontab.new(command='at_year') self.assertRaises(ValueError, job.hour.every, 72) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( FrequencyTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_interaction.py0000664000175000017500000003310614515021651022142 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab interaction. """ import re import os import unittest from crontab import CronTab, CronSlices, CronSlice from .utils import LoggingMixin TEST_DIR = os.path.dirname(__file__) COMMANDS = [ 'firstcommand', 'range', 'byweek', 'disabled', 'spaced', 'python /example_app/testing.py', 'rebooted', 'echo % escaped', 'cat %%', ] 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 # monitorCron\\n*/59 * * * * python /example_app/testing.py @reboot rebooted # re-id # Last Comment @has this # extra # percent_one\\n* * * * * echo \\% escaped # percent_two\\n* * * * * cat \\%\\% % stdin%with%lines """ class InteractionTestCase(LoggingMixin, unittest.TestCase): """Test basic functionality of crontab.""" log_name = 'crontab' def setUp(self): super(InteractionTestCase, self).setUp() 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') for line_no, line in enumerate(self.crontab.lines): self.assertEqual(str(line), results[line_no].replace('\\n', '\n')) def test_02_access(self): """All Entries Are Accessable""" for line_no, job in enumerate(self.crontab): self.assertEqual(str(job.command), COMMANDS[line_no]) else: # pylint: disable=useless-else-on-loop self.assertEqual(line_no + 1, 9) 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_05_multiple_fields(self): """Test mutliple named fields""" job = self.crontab.new(command='fields') job.hour.on(4, 6, 7) self.assertEqual(job.render(), '* 4,6,7 * * * fields') job.dow.on('SUN', 'FRI') self.assertEqual(job.render(), '* 4,6,7 * * SUN,FRI 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_09_new(self): """Adding new Items at any order""" thing1 = self.crontab.new('red', comment='thing1') self.assertEqual(self.crontab[-1], thing1) thing2 = self.crontab.new('blue', before=thing1) self.assertEqual(self.crontab[-1], thing1) self.assertEqual(self.crontab[-2], thing2) thing3 = self.crontab.new('green', before='thing1') self.assertEqual(self.crontab[-1], thing1) self.assertEqual(self.crontab[-2], thing3) self.assertEqual(self.crontab[-3], thing2) thing4 = self.crontab.new('purple', before=re.compile('^thing')) self.assertEqual(self.crontab[-2], thing4) thing5 = self.crontab.new('black', before=self.crontab.find_command('blue')) self.assertEqual(self.crontab[-5], thing5) self.assertRaises(ValueError, self.crontab.new, 'fail', before='fail') def test_10_comment(self): """Render cron Comments""" job = self.crontab.new(command='com', comment='I love this') self.assertEqual(str(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(str(job), '# * * * * * dis') job.enable() self.assertEqual(str(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(str(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): str(job) # Disabled jobs still work job.enabled = False str(job) job.enabled = True with self.assertRaises(ValueError): self.crontab.render(errors=True) self.crontab.render(errors=False) self.assertFalse(job.enabled) job = self.crontab.new() self.assertFalse(job.is_valid()) job.set_command("now_valid") self.assertTrue(job.is_valid()) 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.parse_value('a')), 'a') with self.assertRaises(ValueError): dow.parse_value('b') self.assertEqual(dow.get_range()[0].render(), '*') with self.assertRaises(ValueError): dow.get_range('%') def test_17_slice_id(self): """Single slice by Id""" self.assertEqual(CronSlice(1).max, 23) def test_18_range_cmp(self): """Compare ranges""" dow = CronSlice({'max': 5, 'min': 0}) three = dow.get_range(2, 4)[0] self.assertGreater(three, 2) self.assertLess(three, 4) self.assertEqual(str(three), '2-4') def test_18_find(self): """Find a command and comments by name""" cmds = list(self.crontab.find_command('byweek')) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].comment, 'Comment One') cmds = list(self.crontab.find_command(re.compile(r'stc\w'))) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].command, 'firstcommand') cmds = list(self.crontab.find_command('Comment One')) self.assertEqual(len(cmds), 0) cmds = list(self.crontab.find_comment('Comment One')) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].command, 'byweek') cmds = list(self.crontab.find_comment(re.compile(r'om+en\w O'))) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].comment, 'Comment One') cmds = list(self.crontab.find_comment(re.compile(r'stc\w'))) self.assertEqual(len(cmds), 0) 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\n' for i in range(10): crontab = CronTab(tab=cron) p = list(crontab.new(command='multi%d' % i)) cron = str(crontab) crontab = CronTab(tab=cron) list(crontab.find_command('multi%d' % i))[0].delete() cron = str(crontab) self.assertEqual(str(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(str(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(str(job), '59 23 31 12 * max') def test_24_special_r(self): """Special formats are read and written""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials_enc.tab')) self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@midnight 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(str(job), '1 */2 2-4 12 SUN all') job.setall('*/2') self.assertEqual(str(job), '*/2 * * * * all') job.setall('1 */2 2-4 12 SUN') self.assertEqual(str(job), '1 */2 2-4 12 SUN all') job.setall(['*']) self.assertEqual(str(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(str(job), '1 */2 2-4 12 SUN all') job2.setall("2 */3 4-8 10 MON") job.setall(job2.slices) self.assertEqual(str(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'python /example_app/testing.py', u'rebooted', u'echo % escaped', u'cat %%' ]) def test_28_comments(self): """Get all comments""" self.assertEqual(list(self.crontab.comments), ['Comment One', 'Comment Two', 'monitorCron', 're-id', 'percent_one', 'percent_two']) def test_29_set_lines(self): """Set crontab lines directly""" cron = CronTab(tab='') cron.lines = [ str(self.crontab.lines[5]), ] self.assertEqual(str(cron), '\n*/30 * * * * firstcommand\n') with self.assertRaises(AttributeError): cron.crons = ['A'] if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(InteractionTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_log.py0000664000175000017500000001276414515021651020413 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) YEAR Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ 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 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.log = os.path.join(TEST_DIR, 'data', 'test.log') self.crontab = CronTab(tab=INITAL_TAB, log=self.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""" with self.crontab.log as log: lines = list(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_01_iter(self): """Iterate directly over log""" with self.crontab.log as log: self.assertEqual(len(list(log)), 8) 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 = False with self.assertRaises(IOError): list(self.crontab.log.readlines()) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( BasicTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_range.py0000664000175000017500000001120214515021651020710 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab ranges. """ import unittest from crontab import CronTab, CronSlice from .utils import LoggingMixin class RangeTestCase(LoggingMixin, unittest.TestCase): """Test basic functionality of crontab.""" log_name = 'crontab' def setUp(self): super(RangeTestCase, self).setUp() 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) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") 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) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") 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) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") def test_03_inevery(self): """Inside of Every""" tab = CronTab(tab=""" * * * * * command */59 * * * * command * */23 * * * command * * */30 * * command * * * */11 * command * * * * */7 command """) self.assertEqual(len(tab), 6, str(tab)) def test_04_zero_seq(self): """Zero divisor in range""" tab = CronTab(tab=""" */0 * * * * command """) self.assertEqual(len(tab), 0) self.assertLog("error", "Sequence can not be divided by zero or max") def test_14_invalid_range(self): """No numerator in range""" tab = CronTab(tab="/10 * * * * command") self.assertEqual(len(tab), 0) with self.assertRaises(ValueError): tab.render(errors=True) self.assertEqual(str(tab), "# DISABLED LINE\n# /10 * * * * command\n") self.assertLog('error', u"No enumeration for Minutes: '/10'") def test_05_sunday(self): """Test all possible day of week combinations""" for (a, b) in (\ ("7", "0"), ("5-7", "0,5-6"), ("1-7", "*"), ("*/7", "0"),\ ("0-6", "*"), ("2-7", "0,2-6"), ("1-5", "1-5"), ("0-5", "0-5")): v = str(CronSlice(4, a)) self.assertEqual(v, b, "%s != %s, from %s" % (v, b, a)) def test_06_backwards(self): """Test backwards ranges for error""" tab = CronTab(tab="* * * * 3-1 command") self.assertEqual(str(tab), "# DISABLED LINE\n# * * * * 3-1 command\n") self.assertLog("error", "Bad range '3-1'") if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(RangeTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_removal.py0000664000175000017500000001200014515021651021256 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test cron item removal """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab 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.filenames = [] 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(str(self.crontab), '') def test_03_remove_cmd(self): """Remove all with Command""" self.crontab.remove_all(command='command2') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) self.crontab.remove_all(command='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) def test_05_remove_all_error(self): """Remove all with old arg""" with self.assertRaises(AttributeError): self.crontab.remove_all('command') def test_06_removal_of_none(self): """Remove all respects None as a possible value""" self.crontab[1].set_comment(None) self.crontab.remove_all(comment=None) self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) def test_07_removal_in_loop(self): """Remove items in a loop""" for job in self.crontab: self.crontab.remove(job) self.assertEqual(len(self.crontab), 0) def test_08_removal_write_loop(self): """Remove items in a loop and save each time""" filename = self.get_new_file('removal') crontab = CronTab() for i in range(10): crontab.new('test', comment=str(i)) crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertEqual(len(crontab), 1) self.assertNotEqual(str(crontab), '') crontab.remove_all(comment=str(i)) crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertEqual(len(crontab), 0) self.assertEqual(str(crontab), '') def test_09_removal_during_iter(self): crontab = CronTab() for x in range(0, 5, 1): job = crontab.new(command="cmd", comment="SAME_ID") job.setall("%d * * * *" % (x + 1)) for item in crontab.find_comment("SAME_ID"): crontab.remove(item) self.assertEqual(len(crontab), 0) def test_11_remove_generator(self): """Remove jobs from the find generator""" tabs = self.crontab.find_command('command2') self.crontab.remove(tabs) self.assertEqual(len(self.crontab), 2) def test_12_remove_nonsense(self): """Fail to remove bad type""" self.assertRaises(TypeError, self.crontab.remove, 5) self.assertRaises(TypeError, self.crontab.remove, "foo") def get_new_file(self, name): """Gets a filename and records it for deletion""" this_dir = os.path.dirname(__file__) filename = os.path.join(this_dir, 'data', 'spool', name) self.filenames.append(filename) return filename def tearDown(self): for filename in self.filenames: if os.path.isfile(filename): os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( RemovalTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1719872641.0 python_crontab-3.2.0/tests/test_scheduler.py0000664000175000017500000000671714640626201021612 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test any internal scheduling """ import os import sys import datetime import unittest import crontab import logging import string import random try: from StringIO import StringIO except ImportError: from io import StringIO logger = logging.getLogger() logger.level = logging.WARNING TEST_DIR = os.path.dirname(__file__) COMMAND = os.path.join(TEST_DIR, 'data', 'crontest ') class SchedulerTestCase(unittest.TestCase): """Test scheduling functions of CronTab.""" def setUp(self): self.tab = crontab.CronTab() try: import croniter except ImportError: self.skipTest("Croniter not installed") self.handlers = [] self.log = crontab.LOG self.log.setLevel(logging.DEBUG) self.log.propagate = False for handler in self.log.handlers: self.handlers.append(handler) self.log.removeHandler(handler) self.err = StringIO() self.handler = logging.StreamHandler(self.err) self.log.addHandler(self.handler) def tearDown(self): logger.removeHandler(self.handler) for handler in self.handlers: logger.addHandler(handler) #self.err.close() def assertLog(self, text): self.handler.flush() self.assertEqual(self.err.getvalue().strip(), text) def assertSchedule(self, slices, count, result): uid = random.choice(string.ascii_letters) self.tab.new(command=COMMAND + '-h ' + uid).setall(slices) ret = list(self.tab.run_scheduler(count, cadence=0.01, warp=True)) self.assertEqual(len(ret), result) if count > 0: self.assertEqual(ret[0], '-h|' + uid) def test_01_run(self): """Run the command""" self.tab.env['SHELL'] = crontab.SHELL ret = self.tab.new(command=COMMAND+'-h A').run() self.assertEqual(ret, '-h|A') def test_02_run_error(self): """Run with errors""" ret = self.tab.new(command=COMMAND+'-e B').run() self.assertEqual(ret, '') self.assertLog('-e|B') def test_03_schedule(self): """Simple Schedule""" self.assertSchedule("* * * * *", 5, 4) def test_04_schedule_ten(self): """Every Ten Minutes""" # If on the 10 minute mark, two runs are expected exact = (datetime.datetime.now().minute % 10) == 0 self.assertSchedule("*/10 * * * *", 10, 1 + exact) def test_05_env_passed(self): """Environment is passed in""" self.tab.env['CR_VAR'] = 'BABARIAN' ret = self.tab.new(command=COMMAND+' -ev').run() self.assertEqual(ret, 'BABARIAN') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(SchedulerTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_system_cron.py0000664000175000017500000001005414515021651022165 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2015 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ 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 TEST_FILE = os.path.join(os.path.dirname(__file__), 'data', 'crontab') INITIAL_TAB = """ */30 * * * * palin one_cross_each */30 * * * * matias\ttab_user_error """ class SystemCronTestCase(unittest.TestCase): """Test vixie cron user addition.""" def setUp(self): self.crontab = CronTab(tab=INITIAL_TAB, user=False) def test_00_repr(self): """System crontab repr""" self.assertEqual(repr(self.crontab), "") def test_01_read(self): """Read existing command""" job = self.crontab[0] self.assertEqual(job.user, 'palin') self.assertEqual(job.command, 'one_cross_each') job = self.crontab[1] self.assertEqual(job.user, 'matias') self.assertEqual(job.command, 'tab_user_error') self.assertEqual(len(self.crontab), 2) 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), INITIAL_TAB.replace('\t', ' ') + """ * * * * * pontus release_brian """) def test_03_new_tab(self): """Create a new system crontab""" tab = CronTab(user=False) tab.env['SHELL'] = '/usr/bin/roman' job = tab.new(command='release_bwian', user='pontus') self.assertEqual(str(tab), """SHELL=/usr/bin/roman * * * * * pontus release_bwian """) def test_04_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_05_remove(self): """Remove the user flag""" self.crontab._user = None self.assertEqual(str(self.crontab), """ */30 * * * * one_cross_each */30 * * * * tab_user_error """) self.crontab.new(command='now_go_away') def test_06_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_07_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") def test_09_resaving(self): """Cycle rendering to show no changes""" for i in range(10): self.crontab = CronTab(tab=str(self.crontab)) self.assertEqual(INITIAL_TAB.replace('\t', ' '), str(self.crontab)) def test_10_system_file(self): """Load system crontab from a file""" crontab = CronTab(user=False, tabfile=TEST_FILE) self.assertEqual(repr(crontab), "" % TEST_FILE) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(SystemCronTestCase) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_usage.py0000664000175000017500000002206314515021651020727 0ustar00doctormodoctormo#!/usr/bin/env python # # Copyright (C) 2012 Jay Sigbrandt # Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab usage. """ import os import sys import unittest import crontab from datetime import date, time, datetime, timedelta crontab.LOG.setLevel(crontab.logging.ERROR) 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.CRON_COMMAND = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest')) def flush(): pass class Attribute(object): def __init__(self, obj, attr, value): self.obj = obj self.attr = attr self.value = value def __enter__(self, *args, **kw): if hasattr(self.obj, self.attr): self.previous = getattr(self.obj, self.attr) setattr(self.obj, self.attr, self.value) def __exit__(self, *args, **kw): if hasattr(self, 'previous'): setattr(self.obj, self.attr, self.previous) else: delattr(self.obj, self.attr) 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.__str__(), "") self.assertEqual(repr(cron), "") def test_02_user(self): """Open a user's crontab""" cron = crontab.CronTab(user='basic') self.assertEqual(cron.render(), BASIC) self.assertEqual(repr(cron), "") 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) self.assertEqual(repr(cron), "") 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 user API""" cron = crontab.CronTab(tab=USER) self.assertEqual(repr(cron), "") cron.write_to_user('bob') filename = os.path.join(TEST_DIR, 'data', 'spool', 'bob') self.filenames.append(filename) self.assertTrue(os.path.exists(filename)) self.assertEqual(repr(cron), "") def test_06_touser_fail(self): """Write to user failed""" cron = crontab.CronTab(tab=USER) self.assertRaises(IOError, cron.write_to_user, 'john') filename = os.path.join(TEST_DIR, 'data', 'spool', 'john') if os.path.exists(filename): os.unlink(filename) self.assertTrue(filename) def test_07_ioerror_read(self): """No filename ioerror""" with self.assertRaises(IOError): cron = crontab.CronTab(user='error') cron.read() def test_07_ioerror_write(self): """User not specified, nowhere to write to""" cron = crontab.CronTab() with self.assertRaises(IOError): cron.write() def test_08_cronitem(self): """CronItem Standalone""" item = crontab.CronItem.from_line('noline') self.assertTrue(item.is_enabled()) with self.assertRaises(UnboundLocalError): item.delete() item.set_command('nothing') self.assertEqual(item.render(), '* * * * * nothing') def test_10_time_object(self): """Set slices using time object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(time(1, 2)) self.assertEqual(str(item.slices), '2 1 * * *') self.assertTrue(item.is_valid()) item.setall(time(0, 30, 0, 0)) self.assertEqual(str(item.slices), '30 0 * * *') self.assertTrue(item.is_valid()) self.assertEqual(str(item), '30 0 * * * cmd') def test_11_date_object(self): """Set slices using date object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(date(2010, 6, 7)) self.assertEqual(str(item.slices), '0 0 7 6 *') self.assertTrue(item.is_valid()) def test_12_datetime_object(self): """Set slices using datetime object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(datetime(2009, 8, 9, 3, 4)) self.assertTrue(item.is_valid()) self.assertEqual(str(item.slices), '4 3 9 8 *') def test_20_slice_validation(self): """CronSlices class and objects can validate""" CronSlices = crontab.CronSlices self.assertTrue(CronSlices('* * * * *').is_valid()) self.assertTrue(CronSlices.is_valid('* * * * *')) self.assertTrue(CronSlices.is_valid('*/2 * * * *')) self.assertTrue(CronSlices.is_valid('* 1,2 * * *')) self.assertTrue(CronSlices.is_valid('* * 1-5 * *')) self.assertTrue(CronSlices.is_valid('* * * * MON-WED')) self.assertTrue(CronSlices.is_valid('@reboot')) sliced = CronSlices('* * * * *') sliced[0].parts = [300] self.assertEqual(str(sliced), '300 * * * *') self.assertFalse(sliced.is_valid()) self.assertFalse(CronSlices.is_valid('P')) self.assertFalse(CronSlices.is_valid('*/61 * * * *')) self.assertFalse(CronSlices.is_valid('* 1,300 * * *')) self.assertFalse(CronSlices.is_valid('* * 50-1 * *')) self.assertFalse(CronSlices.is_valid('* * * * FRO-TOO')) self.assertFalse(CronSlices.is_valid('@retool')) self.assertRaises(ValueError, CronSlices._parse_value, None) def test_21_slice_special(self): """Rendering can be done without specials""" cronitem = crontab.CronItem('true') cronitem.setall('0 0 * * *') self.assertEqual(cronitem.render(specials=True), '@daily true') self.assertEqual(cronitem.render(specials=None), '0 0 * * * true') cronitem.setall('@daily') self.assertEqual(cronitem.render(specials=None), '@daily true') self.assertEqual(cronitem.render(specials=False), '0 0 * * * true') def test_25_process(self): """Test opening pipes""" 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""" cron = crontab.CronTab(tab="02 3-5 2,4 */2 01 cmd") self.assertEqual(str(cron), '2 3-5 2,4 */2 1 cmd\n') with Attribute(crontab, 'ZERO_PAD', True): self.assertEqual(str(cron), '02 03-05 02,04 */2 01 cmd\n') def test_08_reset_after_daily(self): """Can jobs be rescheduled after midnight""" cron = crontab.CronTab(tab="@daily cmd") job = cron[0] self.assertEqual(str(job), '@daily cmd') job.minute.on(5) job.hour.on(1) self.assertEqual(str(job), '5 1 * * * cmd') def test_09_pre_comment(self): """Test use of pre_comments""" text = """ # Unattached comment 5 * * * 6 some_command # Attached comment */5 * * * * another_command */5 * * * * my_command # Other comment """ tab = crontab.CronTab(tab=text) self.assertEqual(tab[0].comment, "") self.assertEqual(tab[1].comment, "Attached comment") self.assertEqual(tab[2].comment, "Other comment") self.assertEqual(tab.render(), text) def tearDown(self): for filename in self.filenames: if os.path.exists(filename): os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( UseTestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697915817.0 python_crontab-3.2.0/tests/test_utf8.py0000664000175000017500000000712414515021651020512 0ustar00doctormodoctormo#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2014 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab use of UTF-8 filenames and strings """ import os import sys import locale import unittest from crontab import CronTab TEST_DIR = os.path.dirname(__file__) 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.assertEqual(locale.getpreferredencoding(), 'UTF-8') self.crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertTrue(crontab) with open(filename, "r") as fhl: self.assertEqual(content, fhl.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""" job = self.crontab.new(command=u"ůțƒ_command", comment=u'ůțƒ_comment') self.assertEqual(job.command, u"ůțƒ_command") self.assertEqual(job.comment, u"ůțƒ_comment") self.crontab.render() def test_07_utf8(self): """Write New via UTF-8""" job = self.crontab.new(command=b'\xc5\xaf\xc8\x9b\xc6\x92_command', comment=b'\xc5\xaf\xc8\x9b\xc6\x92_comment') self.assertEqual(self.crontab.render(), u""" */4 * * * * ůțƒ_command # ůțƒ_comment * * * * * ůțƒ_command # ůțƒ_comment """) self.assertEqual(type(job.command), str) self.assertEqual(type(job.comment), str) def test_08_utf8_str(self): """Test UTF8 (non unicode) strings""" self.crontab[0].command = '£12' self.crontab[0].comment = '𝗔𝗕𝗖𝗗' self.assertEqual(self.crontab.render(), u""" */4 * * * * £12 # 𝗔𝗕𝗖𝗗 """) def test_09_utf8_again(self): """Test Extra UTF8 input""" filename = os.path.join(TEST_DIR, 'data', 'utf_extra') with open(filename + '.in', 'w') as fhl: fhl.write('# 中文\n30 2 * * * source /etc/profile\n30 1 * * * source /etc/profile') try: cron = CronTab(tabfile=filename + '.in') finally: os.unlink(filename + '.in') cron.write(filename + '.out') if os.path.isfile(filename + '.out'): os.unlink(filename + '.out') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( Utf8TestCase, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1527260041.0 python_crontab-3.2.0/tests/utils.py0000644000175000017500000000541713302021611017713 0ustar00doctormodoctormo# # Copyright (C) 2018 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Provide some utilities to tests """ from logging import Handler, getLogger, root from collections import defaultdict # FLAG: do not report failures from here in tracebacks # pylint: disable=invalid-name __unittest = True class LoggingRecorder(Handler): """Record any logger output for testing""" def __init__(self, *args, **kwargs): self.logs = defaultdict(list) super(LoggingRecorder, self).__init__(*args, **kwargs) def __getitem__(self, name): return self.logs[name.upper()] def emit(self, record): """Save the log message to the right level""" # We have no idea why record's getMessage is prefixed msg = str(record.getMessage()) if msg.startswith('u"'): msg = msg[1:] if msg and (msg[0] == msg[-1] and msg[0] in '"\''): msg = msg[1:-1] self[record.levelname].append(msg) return True class LoggingMixin(object): """Provide logger capture""" log_name = None def setUp(self): """Make a fresh logger for each test function""" super(LoggingMixin, self).setUp() named = getLogger(self.log_name) for handler in root.handlers[:]: root.removeHandler(handler) for handler in named.handlers[:]: named.removeHandler(handler) self.log_handler = LoggingRecorder(level='DEBUG') named.addHandler(self.log_handler) def tearDown(self): """Warn about untested logs""" for level in self.log_handler.logs: for msg in self.log_handler[level]: raise ValueError("Uncaught log: {}: {}\n".format(level, msg)) def assertLog(self, level, msg): """Checks that the logger has emitted the given log""" logs = self.log_handler[level] self.assertTrue(logs, 'Logger hasn\'t emitted "{}"'.format(msg)) if len(logs) == 1: self.assertEqual(msg, logs[0]) else: self.assertIn(msg, logs) logs.remove(msg) def assertNoLog(self, level, msg): """Checks that the logger has NOT emitted the given log""" self.assertNotIn(msg, self.log_handler[level])