././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3833036
python-crontab-2.6.0/ 0000775 0001750 0001750 00000000000 00000000000 016167 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/AUTHORS 0000664 0001750 0001750 00000000042 00000000000 017233 0 ustar 00doctormo doctormo 0000000 0000000 Martin Owens (doctormo@gmail.com)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1568657783.0
python-crontab-2.6.0/CHANGES.md 0000644 0001750 0001750 00000034352 00000000000 017566 0 ustar 00doctormo doctormo 0000000 0000000 # Release notes
This is a generated CHANGES file, based on the Git history.
## Version 3.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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/COPYING 0000664 0001750 0001750 00000016743 00000000000 017235 0 ustar 00doctormo doctormo 0000000 0000000 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.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584981178.0
python-crontab-2.6.0/MANIFEST.in 0000644 0001750 0001750 00000000502 00000000000 017720 0 ustar 00doctormo doctormo 0000000 0000000 include 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/*
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3833036
python-crontab-2.6.0/PKG-INFO 0000664 0001750 0001750 00000044222 00000000000 017270 0 ustar 00doctormo doctormo 0000000 0000000 Metadata-Version: 2.1
Name: python-crontab
Version: 2.6.0
Summary: Python Crontab API
Home-page: https://gitlab.com/doctormo/python-crontab/
Author: Martin Owens
Author-email: doctormo@gmail.com
License: LGPLv3
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')
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("This was printed to stdout by the process.")
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 three simple
methods::
job.setall("1,2 1,2 * * *")
job.frequency_per_day() == 4
The per year frequency method will tell you how many days a year the
job would execute::
job.setall("* * 1,2 1,2 *")
job.frequency_per_year(year=2010) == 4
These are combined to give the number of times a job will execute in any year::
job.setall("1,2 1,2 1,2 1,2 *")
job.frequency(year=2010) == 16
Frequency can be quickly checked using python built-in operators::
job < "*/2 * * * *"
job > job2
job.slices == "*/5"
Log Functionality
=================
The log functionality will read a cron log backwards to find you the last run
instances of your crontab and cron jobs.
The crontab will limit the returned entries to the user the crontab is for::
cron = CronTab(user='root')
for d in cron.log:
print(d['pid'] + " - " + d['date'])
Each job can return a log iterator too, these are filtered so you can see when
the last execution was::
for d in cron.find_command('echo')[0].log:
print(d['pid'] + " - " + d['date'])
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.5, 3.6, 3.7) and Python 2.7 tested, python 2.6 removed from support.
- Windows support works for non-system crontabs only.
( see mem_cron and file_cron examples above for usage )
Platform: linux
Classifier: Development Status :: 5 - Production/Stable
Classifier: Development Status :: 6 - Mature
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: GNU 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 :: 2.7
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Provides: crontab
Provides: crontabs
Provides: cronlog
Provides-Extra: cron-description
Provides-Extra: cron-schedule
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634675004.0
python-crontab-2.6.0/README.rst 0000644 0001750 0001750 00000033275 00000000000 017666 0 ustar 00doctormo doctormo 0000000 0000000 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')
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("This was printed to stdout by the process.")
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 three simple
methods::
job.setall("1,2 1,2 * * *")
job.frequency_per_day() == 4
The per year frequency method will tell you how many days a year the
job would execute::
job.setall("* * 1,2 1,2 *")
job.frequency_per_year(year=2010) == 4
These are combined to give the number of times a job will execute in any year::
job.setall("1,2 1,2 1,2 1,2 *")
job.frequency(year=2010) == 16
Frequency can be quickly checked using python built-in operators::
job < "*/2 * * * *"
job > job2
job.slices == "*/5"
Log Functionality
=================
The log functionality will read a cron log backwards to find you the last run
instances of your crontab and cron jobs.
The crontab will limit the returned entries to the user the crontab is for::
cron = CronTab(user='root')
for d in cron.log:
print(d['pid'] + " - " + d['date'])
Each job can return a log iterator too, these are filtered so you can see when
the last execution was::
for d in cron.find_command('echo')[0].log:
print(d['pid'] + " - " + d['date'])
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.5, 3.6, 3.7) and Python 2.7 tested, python 2.6 removed from support.
- Windows support works for non-system crontabs only.
( see mem_cron and file_cron examples above for usage )
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451462986.0
python-crontab-2.6.0/cronlog.py 0000664 0001750 0001750 00000007500 00000000000 020206 0 ustar 00doctormo doctormo 0000000 0000000 #
# 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
PY3 = platform.python_version()[0] == '3'
if PY3:
# pylint: disable=W0622
unicode = str
basestring = str
from dateutil import parser as dateparse
MATCHER = r'(?P\w+ +\d+ +\d\d:\d\d:\d\d) (?P\w+) ' + \
r'CRON\[(?P\d+)\]: \((?P\w+)\) CMD \((?P.*)\)'
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, unicode(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'] == unicode(self.command):
yield entry
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676262.0
python-crontab-2.6.0/crontab.py 0000664 0001750 0001750 00000132050 00000000000 020172 0 ustar 00doctormo doctormo 0000000 0000000 #
# 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 time import sleep
from datetime import time, date, datetime, timedelta
try:
from collections import OrderedDict
except ImportError:
# python 2.6 and below causes this error
try:
from ordereddict import OrderedDict
except ImportError:
raise ImportError("OrderedDict is required for python-crontab, you can"
" install ordereddict 1.1 from pypi for python2.6")
__pkgname__ = 'python-crontab'
__version__ = '2.6.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.
PY3 = platform.python_version()[0] == '3'
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 = "/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]
if PY3:
unicode = str
basestring = str
def open_pipe(cmd, *args, **flags):
"""Runs a program and orders the arguments for compatability.
a. keyword args are flags and always appear /before/ arguments for bsd
"""
cmd_args = tuple(shlex.split(cmd, posix=flags.pop('posix', POSIX)))
env = flags.pop('env', None)
for (key, value) in flags.items():
if len(key) == 1:
cmd_args += (("-%s" % key),)
if value is not None:
cmd_args += (unicode(value),)
else:
cmd_args += (("--%s=%s" % (key, value)),)
args = tuple(arg for arg in (cmd_args + tuple(args)) if arg)
return sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE, env=env)
def _unicode(text):
"""Convert to the best string format for this python version"""
if isinstance(text, str) and not PY3:
return unicode(text, 'utf-8')
if isinstance(text, bytes) and PY3:
return text.decode('utf-8')
return text
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.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, basestring):
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(CronTab, self).__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:
(out, err) = open_pipe(self.cron_command, l='', **self.user_opt).communicate()
if err and 'no crontab for' in unicode(err):
pass
elif err:
raise IOError("Read crontab %s: %s" % (self.user, err))
lines = out.decode('utf-8').split("\n")
self.lines = lines
def append(self, item, line='', read=False):
"""Append a CronItem object to this CronTab"""
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.append(item)
self.lines.append(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')
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.")
proc = open_pipe(self.cron_command, path, **self.user_opt)
ret = proc.wait()
if ret != 0:
raise IOError("Program Error: {} returned {}: {}".format(
self.cron_command, ret, proc.stderr.read()))
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, **kwargs):
"""Run the CronTab as an internal scheduler (generator)"""
count = 0
while count != timeout:
now = datetime.now()
if 'warp' in kwargs:
now += timedelta(seconds=count * 60)
for value in self.run_pending(now=now):
yield value
sleep(kwargs.get('cadence', 60))
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, (unicode, 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("Invalid line: %s" % 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 = unicode(self.env) + u'\n'.join(crons)
if result and result[-1] not in (u'\n', u'\r'):
result += u'\n'
return result
def new(self, command='', comment='', user=None, pre_comment=False):
"""
Create a new cron with a command and comment.
Returns the new CronItem object.
"""
if not user and self.user is False:
raise ValueError("User is required for system crontabs.")
item = CronItem(command, comment, user=user, pre_comment=pre_comment)
item.cron = self
self.append(item)
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 "<%sCronTab '%s'>" % (kind, self.filen)
if self.user and not self.user_opt:
return ""
if self.user:
return "" % self.user
return "" % kind
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 __unicode__(self):
return self.render()
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 = _unicode(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 = _unicode(line)
if not line or line[0] == '#':
self.enabled = False
line = line[1:].strip()
# We parse all lines so we can detect disabled entries.
self._set_parse(ITEMREX.findall(line))
self._set_parse(SPECREX.findall(line))
def _set_parse(self, result):
"""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:
self.valid = False
self.enabled = False
LOG.error(str("Missing user or command in system cron 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 = _unicode(self.command).replace(u'%', u'\\%')
user = ''
if self.cron and self.cron.user is False:
if not self.user:
raise ValueError("Job to system-cron format, no user set!")
user = self.user + ' '
result = u"%s %s%s" % (self.slices.render(specials=specials), user, command)
if self.stdin:
result += ' %' + self.stdin.replace('\n', '%')
if not self.enabled:
result = u"# " + result
if self.comment:
comment = self.comment = _unicode(self.comment)
if self.marker:
comment = u"#%s: %s" % (self.marker, comment)
else:
comment = u"# " + comment
if SYSTEMV or self.pre_comment or self.stdin:
result = comment + "\n" + result
else:
result += ' ' + comment
return unicode(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_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)
(out, err) = open_pipe(shell, '-c', self.command, env=env).communicate()
if err:
LOG.error(err.decode("utf-8"))
return out.decode("utf-8").strip()
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:
raise ImportError("Croniter not available. Please install croniter"
" python module via pip or your package manager")
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:
raise ImportError("cron_descriptor not available. Please install"\
"cron_descriptor python module via pip or your package manager")
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 "" % unicode(self)
def __len__(self):
return len(unicode(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.__unicode__()
def __unicode__(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("Invalid value '%s', outside 1 year" % self.unit)
self.slices.setall('@yearly')
class CronSlices(list):
"""Controls a list of five time 'slices' which reprisent:
minute frequency, hour frequency, day of month frequency,
month requency and finally day of the week frequency.
"""
def __init__(self, *args):
super(CronSlices, self).__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, basestring) 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)):
raise ValueError("Unknown type: {}".format(type(value).__name__))
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.keys():
return SPECIALS[key].split(' '), '@' + key
if value.startswith('@'):
raise ValueError("Unknown special '{}'".format(value))
return [value], None
def clean_render(self):
"""Return just numbered parts of this crontab"""
return ' '.join([unicode(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 "@%s" % 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 __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 unicode(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, specials=True):
"""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 "" % unicode(self)
def __eq__(self, value):
return unicode(self) == unicode(value)
def __str__(self):
return self.__unicode__()
def __unicode__(self):
return self.render()
def every(self, n_value, also=False):
"""Set the every X units value"""
if not also:
self.clear()
self.parts += 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(unicode(vfrom) + '-' + unicode(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:
raise ValueError("Unrecognised %s: '%s'" % (self.name, val))
except KeyError:
raise KeyError("No enumeration for %s: '%s'" % (self.name, val))
if self.max == 6 and int(out) == 7:
if sunday is not None:
return sunday
raise SundayError("Detected Sunday as 7 instead of 0!")
if int(out) < self.min or int(out) > self.max:
raise ValueError("'{1}', not in {0.min}-{0.max} for {0.name}".format(self, val))
return out
def get_cronvalue(value, enums):
"""Returns a value as int (pass-through) or a special enum value"""
if isinstance(value, int):
return value
if unicode(value).isdigit():
return int(str(value))
if not enums:
raise KeyError("No enumeration allowed")
return CronValue(unicode(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 unicode(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 unicode(u'{:02d}'.format(value) 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], basestring):
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("Bad range '{0.vfrom}-{0.vto}'".format(self))
elif value == '*':
self.all()
else:
raise ValueError('Unknown cron range value "%s"' % value)
def all(self):
"""Set this slice to all units between the miniumum and maximum"""
self.vfrom = self.slice.min
self.vto = self.slice.max
def render(self, resolve=False):
"""Render the ranged value for a cronjob"""
value = '*'
if int(self.vfrom) > self.slice.min or int(self.vto) < self.slice.max:
if self.vfrom == self.vto:
value = unicode(self.vfrom)
else:
value = _render_values([self.vfrom, self.vto], '-', resolve)
if self.seq != 1:
value += "/%d" % self.seq
if value != '*' and SYSTEMV:
value = ','.join([unicode(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.__unicode__()
def __unicode__(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(OrderedVariableList, self).__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(OrderedVariableList, self).__getitem__(key)
if previous is not None:
return previous.all()[key]
raise KeyError("Environment Variable '%s' not found." % key)
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 unicode(value) or value == '':
value = '"%s"' % value
ret.append("%s=%s" % (key, unicode(value)))
ret.append('')
return "\n".join(ret)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451341380.0
python-crontab-2.6.0/crontabs.py 0000664 0001750 0001750 00000010526 00000000000 020360 0 ustar 00doctormo doctormo 0000000 0000000 #
# 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 pwd
import itertools
from os import stat, access, X_OK
from pwd import getpwuid
from crontab import CronTab
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"""
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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3673038
python-crontab-2.6.0/python_crontab.egg-info/ 0000775 0001750 0001750 00000000000 00000000000 022712 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676602.0
python-crontab-2.6.0/python_crontab.egg-info/PKG-INFO 0000664 0001750 0001750 00000044222 00000000000 024013 0 ustar 00doctormo doctormo 0000000 0000000 Metadata-Version: 2.1
Name: python-crontab
Version: 2.6.0
Summary: Python Crontab API
Home-page: https://gitlab.com/doctormo/python-crontab/
Author: Martin Owens
Author-email: doctormo@gmail.com
License: LGPLv3
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')
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("This was printed to stdout by the process.")
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 three simple
methods::
job.setall("1,2 1,2 * * *")
job.frequency_per_day() == 4
The per year frequency method will tell you how many days a year the
job would execute::
job.setall("* * 1,2 1,2 *")
job.frequency_per_year(year=2010) == 4
These are combined to give the number of times a job will execute in any year::
job.setall("1,2 1,2 1,2 1,2 *")
job.frequency(year=2010) == 16
Frequency can be quickly checked using python built-in operators::
job < "*/2 * * * *"
job > job2
job.slices == "*/5"
Log Functionality
=================
The log functionality will read a cron log backwards to find you the last run
instances of your crontab and cron jobs.
The crontab will limit the returned entries to the user the crontab is for::
cron = CronTab(user='root')
for d in cron.log:
print(d['pid'] + " - " + d['date'])
Each job can return a log iterator too, these are filtered so you can see when
the last execution was::
for d in cron.find_command('echo')[0].log:
print(d['pid'] + " - " + d['date'])
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.5, 3.6, 3.7) and Python 2.7 tested, python 2.6 removed from support.
- Windows support works for non-system crontabs only.
( see mem_cron and file_cron examples above for usage )
Platform: linux
Classifier: Development Status :: 5 - Production/Stable
Classifier: Development Status :: 6 - Mature
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: GNU 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 :: 2.7
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Provides: crontab
Provides: crontabs
Provides: cronlog
Provides-Extra: cron-description
Provides-Extra: cron-schedule
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676602.0
python-crontab-2.6.0/python_crontab.egg-info/SOURCES.txt 0000664 0001750 0001750 00000002136 00000000000 024600 0 ustar 00doctormo doctormo 0000000 0000000 AUTHORS
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 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676602.0
python-crontab-2.6.0/python_crontab.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 00000000000 026760 0 ustar 00doctormo doctormo 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676602.0
python-crontab-2.6.0/python_crontab.egg-info/requires.txt 0000664 0001750 0001750 00000000116 00000000000 025310 0 ustar 00doctormo doctormo 0000000 0000000 python-dateutil
[cron-description]
cron-descriptor
[cron-schedule]
croniter
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676602.0
python-crontab-2.6.0/python_crontab.egg-info/top_level.txt 0000664 0001750 0001750 00000000031 00000000000 025436 0 ustar 00doctormo doctormo 0000000 0000000 cronlog
crontab
crontabs
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3833036
python-crontab-2.6.0/setup.cfg 0000664 0001750 0001750 00000000046 00000000000 020010 0 ustar 00doctormo doctormo 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1569326124.0
python-crontab-2.6.0/setup.py 0000775 0001750 0001750 00000005350 00000000000 017707 0 ustar 00doctormo doctormo 0000000 0000000 #!/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,
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 :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
options = {
'bdist_rpm': {
'build_requires': [
'python',
'python-setuptools',
],
'release': RELEASE,
},
},
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3753037
python-crontab-2.6.0/tests/ 0000775 0001750 0001750 00000000000 00000000000 017331 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451423358.0
python-crontab-2.6.0/tests/__init__.py 0000664 0001750 0001750 00000000001 00000000000 021431 0 ustar 00doctormo doctormo 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3793037
python-crontab-2.6.0/tests/data/ 0000775 0001750 0001750 00000000000 00000000000 020242 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3793037
python-crontab-2.6.0/tests/data/anacron/ 0000775 0001750 0001750 00000000000 00000000000 021663 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451339331.0
python-crontab-2.6.0/tests/data/anacron/an_command.sh 0000755 0001750 0001750 00000000000 00000000000 024302 0 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451339346.0
python-crontab-2.6.0/tests/data/anacron/not_command.txt 0000664 0001750 0001750 00000000000 00000000000 024710 0 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/basic.log 0000664 0001750 0001750 00000000154 00000000000 022026 0 ustar 00doctormo doctormo 0000000 0000000 A really long line which can test log lines ability to put two bits together
First Line
9
2
Sickem
The End
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451329044.0
python-crontab-2.6.0/tests/data/crontab 0000664 0001750 0001750 00000000061 00000000000 021612 0 ustar 00doctormo doctormo 0000000 0000000
* * * * * peter parker
* 2 * * * driver parker
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3793037
python-crontab-2.6.0/tests/data/crontabs/ 0000775 0001750 0001750 00000000000 00000000000 022055 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451339223.0
python-crontab-2.6.0/tests/data/crontabs/.empty 0000664 0001750 0001750 00000000000 00000000000 023202 0 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451326648.0
python-crontab-2.6.0/tests/data/crontabs/system_one 0000664 0001750 0001750 00000000062 00000000000 024163 0 ustar 00doctormo doctormo 0000000 0000000
* * * * * bilbo baggins
* 2 * * * frodo baggins
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451328298.0
python-crontab-2.6.0/tests/data/crontabs/system_two 0000664 0001750 0001750 00000000034 00000000000 024212 0 ustar 00doctormo doctormo 0000000 0000000
1 2 3 * * plastic baggins
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1569496990.0
python-crontab-2.6.0/tests/data/crontest 0000775 0001750 0001750 00000002525 00000000000 022035 0 ustar 00doctormo doctormo 0000000 0000000 #!/usr/bin/env python
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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/specials.tab 0000664 0001750 0001750 00000000062 00000000000 022533 0 ustar 00doctormo doctormo 0000000 0000000 0 * * * * hourly
0 0 * * * daily
0 0 * * 0 weekly
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/specials_enc.tab 0000664 0001750 0001750 00000000115 00000000000 023357 0 ustar 00doctormo doctormo 0000000 0000000 @hourly hourly
@daily daily
@midnight midnight
@weekly weekly
@reboot reboot
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1634676603.3833036
python-crontab-2.6.0/tests/data/spool/ 0000775 0001750 0001750 00000000000 00000000000 021376 5 ustar 00doctormo doctormo 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/spool/basic 0000664 0001750 0001750 00000000027 00000000000 022401 0 ustar 00doctormo doctormo 0000000 0000000 0 * * * * firstcommand
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451329564.0
python-crontab-2.6.0/tests/data/spool/hgreen 0000664 0001750 0001750 00000000122 00000000000 022564 0 ustar 00doctormo doctormo 0000000 0000000
# Hank Green's crontab
0 0 5 */2 * start_company
0 0 * * 6 do_vlog_brothers
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451329395.0
python-crontab-2.6.0/tests/data/spool/jgreen 0000664 0001750 0001750 00000000117 00000000000 022572 0 ustar 00doctormo doctormo 0000000 0000000
# John Green's crontab
0 */2 * * * write_book
0 0 * * 2 do_vlog_brothers
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/spool/user 0000664 0001750 0001750 00000000052 00000000000 022274 0 ustar 00doctormo doctormo 0000000 0000000
*/4 * * * * user_command # user_comment
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/data/test.log 0000664 0001750 0001750 00000004551 00000000000 021731 0 ustar 00doctormo doctormo 0000000 0000000 Apr 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1589586195.0
python-crontab-2.6.0/tests/data/test.tab 0000664 0001750 0001750 00000000726 00000000000 021716 0 ustar 00doctormo doctormo 0000000 0000000 # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1589735628.0
python-crontab-2.6.0/tests/test_compatibility.py 0000644 0001750 0001750 00000010226 00000000000 023612 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
TEST_DIR = os.path.dirname(__file__)
INITAL_TAB = """
# First Comment
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 open_pipe
winfile = os.path.join(TEST_DIR, 'data', "bash\\win.exe")
pipe = open_pipe("{sys.executable} {winfile}".format(winfile=winfile, sys=sys), 'SLASHED', posix=False)
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__':
test_support.run_unittest(
CompatTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1580107188.0
python-crontab-2.6.0/tests/test_context.py 0000644 0001750 0001750 00000004013 00000000000 022422 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(
ContextTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451284570.0
python-crontab-2.6.0/tests/test_croniter.py 0000664 0001750 0001750 00000005532 00000000000 022574 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
INITAL_TAB = """
# Basic Comment
20 * * * * execute # comment
"""
class CroniterTestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.crontab = crontab.CronTab(tab=INITAL_TAB)
self.job = list(self.crontab.find_command('execute'))[0]
try:
import croniter
except ImportError:
self.skipTest("Croniter not installed")
def test_00_nocroniter(self):
"""No Croniter"""
# Remove croniter if 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__':
test_support.run_unittest(
CroniterTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634674493.0
python-crontab-2.6.0/tests/test_crontabs.py 0000664 0001750 0001750 00000007347 00000000000 022570 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(CronTabsTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1465677916.0
python-crontab-2.6.0/tests/test_description.py 0000664 0001750 0001750 00000003771 00000000000 023275 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(DescriptorTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1481559999.0
python-crontab-2.6.0/tests/test_enums.py 0000664 0001750 0001750 00000007020 00000000000 022070 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(
EnumTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1525809048.0
python-crontab-2.6.0/tests/test_env.py 0000664 0001750 0001750 00000014050 00000000000 021532 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(EnvTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1580109915.0
python-crontab-2.6.0/tests/test_equality.py 0000755 0001750 0001750 00000004036 00000000000 022603 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(EqualityTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1525783691.0
python-crontab-2.6.0/tests/test_every.py 0000664 0001750 0001750 00000006552 00000000000 022104 0 ustar 00doctormo doctormo 0000000 0000000 #!/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, PY3
try:
from test import test_support
except ImportError:
from test import support as test_support
TEST_DIR = os.path.dirname(__file__)
if PY3:
unicode = str
class EveryTestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab'))
def test_00_minutes(self):
"""Every Minutes"""
for job in self.crontab:
job.every(3).minutes()
self.assertEqual(job.slices.clean_render(), '*/3 * * * *')
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__':
test_support.run_unittest(
EveryTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634673274.0
python-crontab-2.6.0/tests/test_frequency.py 0000664 0001750 0001750 00000012537 00000000000 022753 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
sys.path.insert(0, '../')
import unittest
from crontab import CronTab, PY3
try:
from test import test_support
except ImportError:
from test import support as test_support
if PY3:
unicode = str
START_TAB = """
"""
class FrequencyTestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.crontab = CronTab(tab=START_TAB.strip())
self.job = self.crontab.new(command='freq')
def test_00_slice_frequency(self):
"""Each Slice Frequency"""
# Make sure some of these overlap
self.job.setall("13-16,14-15 6,*/3 2,3,4 * *")
self.assertEqual(len(self.job[0]), 4)
self.assertEqual(len(self.job[1]), 8)
self.assertEqual(len(self.job[2]), 3)
self.assertEqual(len(self.job[3]), 12)
self.assertEqual(len(self.job[4]), 7)
def test_01_per_day(self):
"""Frequency per Day"""
self.job.setall("21-29 10-14 * * *")
self.assertEqual(len(self.job[0]), 9)
self.assertEqual(len(self.job[1]), 5)
self.assertEqual(self.job.frequency_per_day(), 45)
def test_02_days_per_year(self):
"""Frequency in Days per Year"""
self.job.setall("* * * * *")
self.assertEqual(self.job.frequency_per_year(year=2010), 365)
self.assertEqual(self.job.frequency_per_year(year=2012), 366)
self.job.setall("1 1 11-20 1,4,6,8 0,6")
self.assertEqual(len(self.job[2]), 10)
self.assertEqual(len(self.job[3]), 4)
self.assertEqual(len(self.job[4]), 2)
self.assertEqual(self.job.frequency_per_year(year=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)
if __name__ == '__main__':
test_support.run_unittest(
FrequencyTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676140.0
python-crontab-2.6.0/tests/test_interaction.py 0000664 0001750 0001750 00000031465 00000000000 023272 0 ustar 00doctormo doctormo 0000000 0000000 #!/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, PY3
from .utils import LoggingMixin
try:
from test import test_support
except ImportError:
from test import support as test_support
TEST_DIR = os.path.dirname(__file__)
if PY3:
unicode = str #pylint: disable=redefined-builtin
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_10_comment(self):
"""Render cron Comments"""
job = self.crontab.new(command='com', comment='I love this')
self.assertEqual(unicode(job), '* * * * * com # I love this')
def test_11_disabled(self):
"""Disabled Job"""
jobs = list(self.crontab.find_command('firstcommand'))
self.assertTrue(jobs[0].enabled)
jobs = list(self.crontab.find_command('disabled'))
self.assertFalse(jobs[0].enabled)
def test_12_disable(self):
"""Disable and Enable Job"""
job = self.crontab.new(command='dis')
job.enable(False)
self.assertEqual(unicode(job), '# * * * * * dis')
job.enable()
self.assertEqual(unicode(job), '* * * * * dis')
def test_13_plural(self):
"""Plural API"""
job = self.crontab.new(command='plural')
job.minutes.every(4)
job.hours.on(5, 6)
job.day.on(4)
job.months.on(2)
self.assertEqual(unicode(job), '*/4 5,6 4 2 * plural')
def test_14_valid(self):
"""Valid and Invalid"""
job = self.crontab.new(command='valid')
job.minute.every(2)
job.valid = False
with self.assertRaises(ValueError):
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 = unicode(crontab)
crontab = CronTab(tab=cron)
list(crontab.find_command('multi%d' % i))[0].delete()
cron = unicode(crontab)
self.assertEqual(unicode(crontab), '# start of tab\n')
def test_22_min(self):
"""Minimum Field Values"""
job = self.crontab.new(command='min')
job.minute.on('<')
job.hour.on('<')
job.dom.on('<')
job.month.on('<')
self.assertEqual(unicode(job), '@yearly min')
def test_23_max(self):
"""Maximum Field Values"""
job = self.crontab.new(command='max')
job.minute.on('>')
job.hour.on('>')
job.dom.on('>')
job.month.on('>')
self.assertEqual(unicode(job), '59 23 31 12 * max')
def test_24_special_r(self):
"""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(unicode(job), '1 */2 2-4 12 SUN all')
job.setall('*/2')
self.assertEqual(unicode(job), '*/2 * * * * all')
job.setall('1 */2 2-4 12 SUN')
self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all')
job.setall(['*'])
self.assertEqual(unicode(job), '* * * * * all')
def test_26_setall_obj(self):
"""Copy all values"""
job = self.crontab.new(command='all')
job2 = self.crontab.new(command='ignore')
job2.setall("1 */2 2-4 12 SUN")
job.setall(job2)
self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all')
job2.setall("2 */3 4-8 10 MON")
job.setall(job2.slices)
self.assertEqual(unicode(job), '2 */3 4-8 10 MON all')
def test_27_commands(self):
"""Get all commands"""
self.assertEqual(list(self.crontab.commands),
[u'firstcommand', u'range', u'byweek', u'disabled',
u'spaced', u'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__':
test_support.run_unittest(InteractionTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1451541187.0
python-crontab-2.6.0/tests/test_log.py 0000664 0001750 0001750 00000012743 00000000000 021532 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
TEST_DIR = os.path.dirname(__file__)
INITAL_TAB = """
* * * * * userscript &> /dev/null
* * * * * rootscript &> /dev/null
* * * * * shadowscript &> /dev/null
"""
ROOT_PIDS = ['16592', '16574', '16552', '16522', '16514', '16489']
SHAD_PIDS = ['16551', '16497']
USER_PIDS = ['16591', '16588', '16573', '16569', '16554', '16539', '16523',\
'16519','16513','16496','16490']
YEAR = date.today().year
ROOT_DATES = [
datetime(YEAR, 4, 4, 21, 34, 1),
datetime(YEAR, 4, 4, 21, 32, 1),
datetime(YEAR, 4, 4, 21, 30, 1),
datetime(YEAR, 4, 4, 21, 28, 1),
datetime(YEAR, 4, 4, 21, 26, 1),
datetime(YEAR, 4, 4, 21, 24, 1),
]
SHAD_DATES = [
datetime(YEAR, 4, 4, 21, 30, 1),
datetime(YEAR, 4, 4, 21, 25, 1),
]
USER_DATES = [
datetime(YEAR, 4, 4, 21, 34, 1),
datetime(YEAR, 4, 4, 21, 33, 2),
datetime(YEAR, 4, 4, 21, 32, 1),
datetime(YEAR, 4, 4, 21, 31, 1),
datetime(YEAR, 4, 4, 21, 30, 1),
datetime(YEAR, 4, 4, 21, 29, 1),
datetime(YEAR, 4, 4, 21, 28, 1),
datetime(YEAR, 4, 4, 21, 27, 1),
datetime(YEAR, 4, 4, 21, 26, 1),
datetime(YEAR, 4, 4, 21, 25, 1),
datetime(YEAR, 4, 4, 21, 24, 1),
]
LONG_LINE = 'A really long line which can test log lines ability to put two bits together'
READ_LINE = [ 'The End', 'Sickem', '2', '9', 'First Line', LONG_LINE ]
class BasicTestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.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__':
test_support.run_unittest(
BasicTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1568657699.0
python-crontab-2.6.0/tests/test_range.py 0000664 0001750 0001750 00000011270 00000000000 022037 0 ustar 00doctormo doctormo 0000000 0000000 #!/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, PY3
try:
from test import test_support
except ImportError:
from test import support as test_support
from .utils import LoggingMixin
if PY3:
unicode = str #pylint: disable=redefined-builtin
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(unicode(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__':
test_support.run_unittest(RangeTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1527266657.0
python-crontab-2.6.0/tests/test_removal.py 0000664 0001750 0001750 00000012023 00000000000 022405 0 ustar 00doctormo doctormo 0000000 0000000 #!/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, PY3
try:
from test import test_support
except ImportError:
from test import support as test_support
if PY3:
unicode = str
START_TAB = """
3 * * * * command1 # CommentID C
2 * * * * command2 # CommentID AAB
1 * * * * command3 # CommentID B3
"""
class RemovalTestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.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(unicode(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__':
test_support.run_unittest(
RemovalTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1525811803.0
python-crontab-2.6.0/tests/test_scheduler.py 0000664 0001750 0001750 00000006462 00000000000 022730 0 ustar 00doctormo doctormo 0000000 0000000 #!/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 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
try:
from test import test_support
except ImportError:
from test import support as test_support
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"""
self.assertSchedule("*/10 * * * *", 12, 1)
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__':
test_support.run_unittest(SchedulerTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1546532365.0
python-crontab-2.6.0/tests/test_system_cron.py 0000664 0001750 0001750 00000010033 00000000000 023304 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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__':
test_support.run_unittest(SystemCronTestCase)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1634676365.0
python-crontab-2.6.0/tests/test_usage.py 0000664 0001750 0001750 00000021620 00000000000 022047 0 ustar 00doctormo doctormo 0000000 0000000 #!/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
try:
from test import test_support
except ImportError:
from test import support as test_support
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.__unicode__(), "")
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'))
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_open_pipe(self):
"""Test opening pipes"""
from crontab import open_pipe, CRON_COMMAND
pipe = open_pipe(CRON_COMMAND, h=None, a='one', abc='two')
(out, err) = pipe.communicate()
self.assertEqual(err, b'')
self.assertEqual(out, b'--abc=two|-a|-h|one\n')
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__':
test_support.run_unittest(
UseTestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1467862809.0
python-crontab-2.6.0/tests/test_utf8.py 0000664 0001750 0001750 00000007153 00000000000 021636 0 ustar 00doctormo doctormo 0000000 0000000 #!/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, PY3
try:
from test import test_support
except ImportError:
from test import support as test_support
TEST_DIR = os.path.dirname(__file__)
if PY3:
unicode = str
content = """
*/4 * * * * ůțƒ_command # ůțƒ_comment
"""
filename = os.path.join(TEST_DIR, 'data', 'output-ůțƒ-8.tab')
class Utf8TestCase(unittest.TestCase):
"""Test basic functionality of crontab."""
def setUp(self):
self.crontab = CronTab(tab=content)
def test_01_input(self):
"""Read UTF-8 contents"""
self.assertTrue(self.crontab)
def test_02_write(self):
"""Write/Read UTF-8 Filename"""
self.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), unicode)
self.assertEqual(type(job.comment), unicode)
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__':
test_support.run_unittest(
Utf8TestCase,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1527260041.0
python-crontab-2.6.0/tests/utils.py 0000644 0001750 0001750 00000005417 00000000000 021050 0 ustar 00doctormo doctormo 0000000 0000000 #
# 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])