pax_global_header00006660000000000000000000000064152446107460014523gustar00rootroot0000000000000052 comment=040c496686a6dd8739efee67024caf646e33623d python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/000077500000000000000000000000001524461074600222055ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/.gitignore000066400000000000000000000001701524461074600241730ustar00rootroot00000000000000MANIFEST dist build __pycache__ debian output-* pythonenv .coverage *.egg-info .eggs *.pyc *.swp .mypy_cache CHANGES.md python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/.gitlab-ci.yml000066400000000000000000000046011524461074600246420ustar00rootroot00000000000000# This file is a template, and might need editing before it works on your project. # This is a sample GitLab CI/CD configuration file that should run without any modifications. # It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts, # it uses echo commands to simulate the pipeline execution. # # A pipeline is composed of independent jobs that run scripts, grouped into stages. # Stages run in sequential order, but jobs within stages run in parallel. # # For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages # # You can copy and paste this template into a new `.gitlab-ci.yml` file. # You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. # # To contribute improvements to CI/CD templates, please follow the Development guide at: # https://docs.gitlab.com/ee/development/cicd/templates.html # This specific template is located at: # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml stages: # List of stages for jobs, and their order of execution - lint - test ruff: stage: lint image: registry.gitlab.com/pipeline-components/ruff:latest script: - ruff check --output-format=gitlab . pytest: stage: test image: python:latest script: # All pytests fail with ModuleNotFoundError: No module named 'test' - pip install pytest - pip install --editable ".[cron-description,cron-schedule]" - pip list - python -m pytest setup.py_test: stage: test image: python:latest script: # All pytests fail with ModuleNotFoundError: No module named 'test' - pip install pytest setuptools - pip install --editable ".[cron-description,cron-schedule]" - pip list - python setup.py test setup.py_test_with_coverage: stage: test image: python:latest script: # With `&> /dev/null` in `./coverage.sh` everything appears green but... - pip install coverage pytest setuptools - pip install --editable ".[cron-description,cron-schedule]" - ./coverage.sh setup.py_test_with_coverage_no_hide: stage: test image: python:latest script: # Removing `&> /dev/null` from `./coverage.sh` reveal that pytest is failing - pip install coverage pytest - pip install --editable ".[cron-description,cron-schedule]" - coverage run --source crontab,crontabs,cronlog setup.py test - coverage report -m python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/.pylintrc000066400000000000000000000154771524461074600240700ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=CVS # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. disable=C0103,C0301,E1101,R0201,R0903,R0902,R0904 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html output-format=text # Include message's id in output include-ids=no # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=yes # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ [FORMAT] # Maximum number of characters on a single line. max-line-length=80 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. generated-members=REQUEST,acl_users,aq_parent [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/AUTHORS000066400000000000000000000000421524461074600232510ustar00rootroot00000000000000Martin Owens (doctormo@gmail.com) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/COPYING000066400000000000000000000167431524461074600232530ustar00rootroot00000000000000 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. python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/MANIFEST.in000066400000000000000000000005021524461074600237400ustar00rootroot00000000000000include 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/* python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/README.rst000066400000000000000000000404161524461074600237010ustar00rootroot00000000000000Python 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 * * * =========== ============ To control the output of specials see `SPECIALS_CONVERSION` below. 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') Randomly between two times, supported on some modern systems:: job.hour.random_during(1, 3) Each time restriction will clear the previous restriction:: job.hour.every(10) # Set to * */10 * * * job.hour.on(2) # Set to * 2 * * * Appending restrictions is explicit:: job.hour.every(10) # Set to * */10 * * * job.hour.also.on(2) # Set to * 2,*/10 * * * Setting all time slices at once:: job.setall(2, 10, '2-4', '*/2', None) job.setall('2 10 * * *') Setting the slice to a python date object:: job.setall(time(10, 2)) job.setall(date(2000, 4, 2)) job.setall(datetime(2000, 4, 2, 10, 2)) Run a jobs command. Running the job here will not effect it's existing schedule with another crontab process:: job_standard_output = job.run() Creating a job with a comment:: job = cron.new(command='/foo/bar', comment='SomeID') Creating a job in the middle of the crontab:: job = cron.new('/bin/a', before='someID') job = cron.new('/bin/b', before=jobItem) job = cron.new('/bin/c', before=re.compile('id*')) job = cron.new('/bin/d', before=cron.find_command('/usr/bin/existing')) Get the comment or command for a job:: command = job.command comment = job.comment Modify the comment or command on a job:: job.set_command("new_script.sh") job.set_comment("New ID or comment here") Disabled or Enable Job:: job.enable() job.enable(False) False is job.is_enabled() Validity Check:: True is job.is_valid() Use a special syntax:: job.every_reboot() Find an existing job by command sub-match or regular expression:: iter = cron.find_command('bar') # matches foobar1 iter = cron.find_command(re.compile(r'b[ab]r$')) Find an existing job by comment exact match or regular expression:: iter = cron.find_comment('ID or some text') iter = cron.find_comment(re.compile(' or \w')) Find an existing job by schedule:: iter = cron.find_time(2, 10, '2-4', '*/2', None) iter = cron.find_time("*/2 * * * *") Clean a job of all rules:: job.clear() Iterate through all jobs, this includes disabled (commented out) cron jobs:: for job in cron: print(job) Iterate through all lines, this includes all comments and empty lines:: for line in cron.lines: print(line) Remove Items:: cron.remove( job ) cron.remove_all('echo') cron.remove_all(comment='foo') cron.remove_all(time='*/2') Clear entire cron of all jobs:: cron.remove_all() Write CronTab back to system or filename:: cron.write() Write CronTab to new filename:: cron.write( 'output.tab' ) Write to this user's crontab (unix only):: cron.write_to_user( user=True ) Write to some other user's crontab:: cron.write_to_user( user='bob' ) Validate a cron time string:: from crontab import CronSlices bool = CronSlices.is_valid('0/2 * * * *') Compare list of cron objects against another and return the difference:: difference = set([CronItem1, CronItem2, CronItem3]) - set([CronItem2, CronItem3]) Compare two CronItems for equality:: CronItem1 = CronTab(tab="* * * * * COMMAND # Example Job") CronItem2 = CronTab(tab="10 * * * * COMMAND # Example Job 2") if CronItem1 != CronItem2: print("Cronjobs do not match") Environment Variables ===================== Some versions of vixie cron support variables outside of the command line. Sometimes just update the envronment when commands are run, the Cronie fork of vixie cron also supports CRON_TZ which looks like a regular variable but actually changes the times the jobs are run at. Very old vixie crons don't support per-job variables, but most do. Iterate through cron level environment variables:: for (name, value) in cron.env.items(): print(name) print(value) Create new or update cron level environment variables:: print(cron.env['SHELL']) cron.env['SHELL'] = '/bin/bash' print(cron.env) Each job can also have a list of environment variables:: for job in cron: job.env['NEW_VAR'] = 'A' print(job.env) Specials Mode ============= You may need to control the output of special schedules such as `@hourly`. This can be done with the `crontab.SPECIALS_CONVERSION` package variable. Enable conversion to a special on write (default):: crontab.SPECIALS_CONVERSION = True Enabled conversion to normal output on write:: crontab.SPECIALS_CONVERSION = False Do no conversion and write whatever was read in:: crontab.SPECIALS_CONVERSION = None Proceeding Unit Confusion ========================= It is sometimes logical to think that `job.hour.every(2)` will set all proceeding units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling only the hours units and the minute column is unaffected. The real result would be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs. There is a special 'every' method on a job to clear the job's existing schedule and replace it with a simple single unit:: job.every(4).hours() == '0 */4 * * *' job.every().dom() == '0 0 * * *' job.every().month() == '0 0 0 * *' job.every(2).dows() == '0 0 * * */2' This is a convenience method only, it does normal things with the existing api. Running the Scheduler ===================== The module is able to run a cron tab as a daemon as long as the optional croniter module is installed; each process will block and errors will be logged (new in 2.0). (note this functionality is new and not perfect, if you find bugs report them!) Running the scheduler:: tab = CronTab(tabfile='MyScripts.tab') for result in tab.run_scheduler(): print("Return code: {result.returncode}") print("Standard Out: {result.stdout}") print("Standard Err: {result.stderr}") Do not do this, it won't work because it returns generator function:: tab.run_scheduler() Timeout and cadence can be changed for testing or error management:: for result in tab.run_scheduler(timeout=600): print("Will run jobs every 1 minutes for ten minutes from now()") for result in tab.run_scheduler(cadence=1, warp=True): print("Will run jobs every 1 second, counting each second as 1 minute") The scheduler can be run with python 3.5+ async too:: async for result in tab.run_scheduler_async(): print("Will gather results in mainloop") Frequency Calculation ===================== Every job's schedule has a frequency. We can attempt to calculate the number of times a job would execute in a give amount of time. We have two variants `frequency_per_*` and `frequency_at_*` calculations. The `freqency_at_*` always returnes *times* a job would execute and is aware of leap years. `frequency_per_*` ----------------- For `frequency_per_*` We have three simple methods:: job.setall("1,2 1,2 * * *") job.frequency_per_day() == 4 The per year frequency method will tell you how many **days** a year the job would execute:: job.setall("* * 1,2 1,2 *") job.frequency_per_year(year=2010) == 4 These are combined to give the number of times a job will execute in any year:: job.setall("1,2 1,2 1,2 1,2 *") job.frequency(year=2010) == 16 Frequency can be quickly checked using python built-in operators:: job < "*/2 * * * *" job > job2 job.slices == "*/5" `frequency_at_*` ---------------- For `frequency_at_*` We have four simple methods. The at per hour frequency method will tell you how many times the job would execute at a given hour:: job.setall("*/2 0 * * *") job.frequency_at_hour() == 30 job.frequency_at_hour(year=2010, month=1, day=1, hour=0) == 30 # even hour job.frequency_at_hour(year=2010, month=1, day=1, hour=1) == 0 # odd hour The at day frequency method parameterized tells you how many times the job would execute at a given day:: job.setall("0 0 * * 1,2") job.frequency_at_day(year=2010, month=1, day=18) == 24 # Mon Jan 18th 2020 job.frequency_at_day(year=2010, month=1, day=21) == 0 # Thu Jan 21th 2020 The at month frequency method will tell you how many times the job would execute at a given month:: job.setall("0 0 * * *") job.frequency_at_month() == job.frequency_at_month(year=2010, month=1) == 31 job.frequency_at_month(year=2010, month=2) == 28 job.frequency_at_month(year=2012, month=2) == 29 # leap year The at year frequency method will tell you how many times a year the job would execute:: job.setall("* * 3,29 2 *") job.frequency_at_year(year=2021) == 24 job.frequency_at_year(year=2024) == 48 # leap year Log Functionality ================= The log functionality will read a cron log backwards to find you the last run instances of your crontab and cron jobs. The crontab will limit the returned entries to the user the crontab is for:: cron = CronTab(user='root') for d in cron.log: print(d['pid'] + " - " + d['date']) Each job can return a log iterator too, these are filtered so you can see when the last execution was:: for d in cron.find_command('echo')[0].log: print(d['pid'] + " - " + d['date']) All System CronTabs Functionality ================================= The crontabs (note the plural) module can attempt to find all crontabs on the system. This works well for Linux systems with known locations for cron files and user spolls. It will even extract anacron jobs so you can get a picture of all the jobs running on your system:: from crontabs import CronTabs for cron in CronTabs(): print(repr(cron)) All jobs can be brought together to run various searches, all jobs are added to a CronTab object which can be used as documented above:: jobs = CronTabs().all.find_command('foo') Schedule Functionality ====================== If you have the croniter python module installed, you will have access to a schedule on each job. For example if you want to know when a job will next run:: schedule = job.schedule(date_from=datetime.now()) This creates a schedule croniter based on the job from the time specified. The default date_from is the current date/time if not specified. Next we can get the datetime of the next job:: datetime = schedule.get_next() Or the previous:: datetime = schedule.get_prev() The get methods work in the same way as the default croniter, except that they will return datetime objects by default instead of floats. If you want the original functionality, pass float into the method when calling:: datetime = schedule.get_current(float) If you don't have the croniter module installed, you'll get an ImportError when you first try using the schedule function on your cron job object. Descriptor Functionality ======================== If you have the cron-descriptor module installed, you will be able to ask for a translated string which describes the frequency of the job in the current locale language. This should be mostly human readable. print(job.description(use_24hour_time_format=True)) See cron-descriptor for details of the supported languages and options. Extra Support ============= - Customise the location of the crontab command by setting the global CRON_COMMAND or the per-object cron_command attribute. - Support for vixie cron with username addition with user flag - Support for SunOS, AIX & HP with compatibility 'SystemV' mode. - Python 3 (3.7, 3.8, 3.10) tested, python 2.6, 2.7 removed from support. - Windows support works for non-system crontabs only. ( see mem_cron and file_cron examples above for usage ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/branding.svg000066400000000000000000000245031524461074600245160ustar00rootroot00000000000000 Python Crontab Logo image/svg+xml Python Crontab Logo 2012-10-13 Martin Owens Copyright Martin Owens 2012 logo snake clock time cron crontab python hourglass Logo for the python-crontab project python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/coverage.sh000077500000000000000000000001051524461074600243330ustar00rootroot00000000000000pytest --cov=crontab --cov=crontabs --cov=cronlog coverage report -m python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/cronlog.py000066400000000000000000000074771524461074600242410ustar00rootroot00000000000000# # 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 from datetime import datetime 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) def cron_date_to_datetime(cron_str): with_year = f"{cron_str} {datetime.now().year}" return datetime.strptime(with_year, "%b %d %H:%M:%S %Y") class CronLog(LogReader): """Use the LogReader to make a Cron specific log reader""" def __init__(self, filename='/var/log/syslog', user=None): LogReader.__init__(self, filename) self.user = user def for_program(self, command): """Return log entries for this specific command name""" return ProgramLog(self, command) def __iter__(self): for line in super(CronLog, self).__iter__(): match = re.match(MATCHER, str(line)) datum = match and match.groupdict() if datum and (not self.user or datum['user'] == self.user): datum['date'] = cron_date_to_datetime(datum['date']) yield datum class ProgramLog(object): """Specific log control for a single command/program""" def __init__(self, log, command): self.log = log self.command = command def __iter__(self): for entry in self.log: if entry['cmd'] == str(self.command): yield entry python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/crontab.py000066400000000000000000001466341524461074600242250ustar00rootroot00000000000000# # Copyright 2025, 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 asyncio import logging import tempfile import platform import subprocess as sp from calendar import monthrange from time import sleep from datetime import time, date, datetime, timedelta from collections import OrderedDict from shutil import which __pkgname__ = 'python-crontab' __version__ = '3.4.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_CONVERSION = True SPECIALS = {"reboot": '@reboot', "hourly": '0 * * * *', "daily": '0 0 * * *', "weekly": '0 0 * * 0', "monthly": '0 0 1 * *', "yearly": '0 0 1 1 *', "annually": '0 0 1 1 *', "midnight": '0 0 * * *'} SPECIAL_IGNORE = ['midnight', 'annually'] S_INFO = [ {'max': 59, 'min': 0, 'name': 'Minutes'}, {'max': 23, 'min': 0, 'name': 'Hours'}, {'max': 31, 'min': 1, 'name': 'Day of Month'}, {'max': 12, 'min': 1, 'name': 'Month', 'enum': MONTH_ENUM}, {'max': 6, 'min': 0, 'name': 'Day of Week', 'enum': WEEK_ENUM}, ] # Detect Python3 and which OS for temperments. WINOS = platform.system() == 'Windows' POSIX = os.name == 'posix' SYSTEMV = not WINOS and os.uname()[0] in ["SunOS", "AIX", "HP-UX"] SYSTEMV = not WINOS and ( os.uname()[0] in ["SunOS", "AIX", "HP-UX"] or os.uname()[4] in ["mips"] ) # Switch this on if you want your crontabs to have zero padding. ZERO_PAD = False LOG = logging.getLogger('crontab') CRON_COMMAND = which("crontab") or "/usr/bin/crontab" SHELL = os.environ.get('SHELL', '/bin/sh') # The shell won't actually work on windows here, but # it should be updated later in the below conditional. # pylint: disable=W0622,invalid-name,too-many-public-methods # pylint: disable=function-redefined,too-many-instance-attributes current_user = lambda: None if not WINOS: import pwd def current_user(): """Returns the username of the current user""" return pwd.getpwuid(os.getuid())[0] def _str(text): """Convert to the best string format for this python version""" if isinstance(text, bytes): return text.decode('utf-8') return text class Process: """Runs a program and orders the arguments for compatability. a. keyword args are flags and always appear /before/ arguments for bsd """ def __init__(self, cmd, *args, **flags): cmd_args = tuple(shlex.split(cmd, posix=flags.pop('posix', POSIX))) self.env = flags.pop('env', None) for (key, value) in flags.items(): if len(key) == 1: cmd_args += (f"-{key}",) if value is not None: cmd_args += (str(value),) else: cmd_args += (f"--{key}={value}",) self.args = tuple(arg for arg in (cmd_args + tuple(args)) if arg) self.has_run = False self.stdout = None self.stderr = None self.returncode = None def _run(self): """Run this process and return the popen process object""" return sp.Popen(self.args, stdout=sp.PIPE, stderr=sp.PIPE, env=self.env) def run(self): """Run this process and store whatever is returned""" process = self._run() (out, err) = process.communicate() self.returncode = process.returncode self.stdout = out.decode("utf-8") self.stderr = err.decode("utf-8") return self def __str__(self): return self.stdout.strip() def __repr__(self): return f"Process({self.args})" def __int__(self): return self.returncode def __eq__(self, other): return str(self) == other class CronTab: """ Crontab object which can access any time based cron using the standard. user - Set the user of the crontab (default: None) * 'user' = Load from $username's crontab (instead of tab or tabfile) * None = Don't load anything from any user crontab. * True = Load from current $USER's crontab (unix only) * False = This is a system crontab, each command has a username tab - Use a string variable as the crontab instead of installed crontab tabfile - Use a file for the crontab instead of installed crontab log - Filename for logfile instead of /var/log/syslog """ def __init__(self, user=None, tab=None, tabfile=None, log=None): self.lines = None self.crons = None self.filen = None self.cron_command = CRON_COMMAND self.env = None self._parked_env = OrderedDict() # Protect windows users self.root = not WINOS and os.getuid() == 0 # Storing user flag / username self._user = user # Load string or filename as inital crontab self.intab = tab self.tabfile = tabfile self.read(tabfile) self._log = log def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.write() @property def log(self): """Returns the CronLog object for this tab (user or root tab only)""" from cronlog import CronLog # pylint: disable=import-outside-toplevel if self._log is None or isinstance(self._log, str): self._log = CronLog(self._log, user=self.user or 'root') return self._log @property def user(self): """Return user's username of this crontab if applicable""" if self._user is True: return current_user() return self._user @property def user_opt(self): """Returns the user option for the crontab commandline""" # Fedora and Mac require the current user to not specify # But Ubuntu/Debian doesn't care. Be careful here. if self._user and self._user is not True: if self._user != current_user(): return {'u': self._user} return {} def __setattr__(self, name, value): """Catch setting crons and lines directly""" if name == 'lines' and value: for line in value: self.append(CronItem.from_line(line, cron=self), line, read=True) elif name == 'crons' and value: raise AttributeError("You can NOT set crons attribute directly") else: super().__setattr__(name, value) def read(self, filename=None): """ Read in the crontab from the system into the object, called automatically when listing or using the object. use for refresh. """ self.crons = [] self.lines = [] self.env = OrderedVariableList() lines = [] if self.intab is not None: lines = self.intab.split('\n') elif filename: self.filen = filename with codecs.open(filename, 'r', encoding='utf-8') as fhl: lines = fhl.readlines() elif self.user: try: process = Process(self.cron_command, l='', **self.user_opt).run() except FileNotFoundError: raise IOError(f"Can't read crontab; No crontab program '{self.cron_command}'") if process.stderr and 'no crontab for' not in process.stderr: raise IOError(f"Read crontab {self.user}: {process.stderr}") lines = process.stdout.split("\n") self.lines = lines def append(self, item, line='', read=False, before=None): """Append a CronItem object to this CronTab Keyword arguments: item - The CronItem object to append line - The textual line which this item is. read - Internal use only before - Append before this CronItem, comment regex or generator """ cron_id = len(self.crons) line_id = len(self.lines) if isinstance(before, (str, type(ITEMREX))): before = self.find_comment(before) try: if isinstance(before, (list, tuple, types.GeneratorType)): *_, before = before if before is not None: cron_id = self.crons.index(before) line_id = self.lines.index(before) except ValueError as err: raise ValueError("Can not find CronItem in crontab to insert before") from err if item.is_valid(): item.env.update(self._parked_env) self._parked_env = OrderedDict() if read and not item.comment and self.lines and \ self.lines[-1] and self.lines[-1][0] == '#': item.set_comment(self.lines.pop()[1:].strip(), True) self.crons.insert(cron_id, item) self.lines.insert(line_id, item) elif '=' in line: if ' ' not in line or line.index('=') < line.index(' '): (name, value) = line.split('=', 1) value = value.strip() for quot in "\"'": if value[0] == quot and value[-1] == quot: value = value.strip(quot) break self._parked_env[name.strip()] = value else: if not self.crons and self._parked_env: self.env.update(self._parked_env) self._parked_env = OrderedDict() self.lines.append(line.replace('\n', '')) def write(self, filename=None, user=None, errors=False): """Write the crontab to it's source or a given filename.""" if filename: self.filen = filename elif user is not None: self.filen = None self.intab = None self._user = user # Add to either the crontab or the internal tab. if self.intab is not None: self.intab = self.render() # And that's it if we never saved to a file if not self.filen: return if self.filen: fileh = open(self.filen, 'wb') # pylint: disable=consider-using-with else: filed, path = tempfile.mkstemp() fileh = os.fdopen(filed, 'wb') fileh.write(self.render(errors=errors).encode('utf-8')) fileh.close() if not self.filen: # Add the entire crontab back to the user crontab if not self.user: os.unlink(path) raise IOError("Please specify user or filename to write.") try: proc = Process(self.cron_command, path, **self.user_opt)._run() except FileNotFoundError: raise IOError(f"Can't write crontab, no crontab program '{self.cron_command}'") ret = proc.wait() if ret != 0: msg = proc.stderr.read() raise IOError(f"Program Error: {self.cron_command} returned {ret}: {msg}") proc.stdout.close() proc.stderr.close() os.unlink(path) def write_to_user(self, user=True): """Write the crontab to a user (or root) instead of a file.""" return self.write(user=user) def run_pending(self, **kwargs): """Run all commands in this crontab if pending (generator)""" for job in self: ret = job.run_pending(**kwargs) if ret not in [None, -1]: yield ret def run_scheduler(self, timeout=-1, cadence=60, warp=False): """Run the CronTab as an internal scheduler (generator)""" count = 0 while count != timeout: now = datetime.now() if warp: now += timedelta(seconds=count * 60) for value in self.run_pending(now=now): yield value sleep(cadence) count += 1 async def run_scheduler_async(self, timeout=-1, cadence=60, warp=False): """Run the CronTab as an internal scheduler (generator)""" count = 0 while count != timeout: now = datetime.now() if warp: now += timedelta(seconds=count * 60) for value in self.run_pending(now=now): yield value await asyncio.sleep(cadence) count += 1 def render(self, errors=False): """Render this crontab as it would be in the crontab. errors - Should we not comment out invalid entries and cause errors? """ crons = [] for line in self.lines: if isinstance(line, (str, str)): if line.strip().startswith('#') or not line.strip(): crons.append(line.strip()) elif not errors: crons.append('# DISABLED LINE\n# ' + line) else: raise ValueError(f"Invalid line: {line}") elif isinstance(line, CronItem): if not line.is_valid() and not errors: line.enabled = False crons.append(line.render().strip()) # Environment variables are attached to cron lines so order will # always work no matter how you add lines in the middle of the stack. result = str(self.env) + '\n'.join(crons) if result and result[-1] not in ('\n', '\r'): result += '\n' return result def new(self, command='', comment='', user=None, pre_comment=False, before=None): # pylint: disable=too-many-arguments """ Create a new CronItem and append it to the cron. Keyword arguments: command - The command that will be run. comment - The comment that should be associated with this command. user - For system cron tabs, the user this command should run as. pre_comment - If true the comment will apear just before the command line. before - Append this command before this item instead of at the end. Returns the new CronItem object. """ if not user and self.user is False: raise ValueError("User is required for system crontabs.") item = CronItem(command, comment, user=user, pre_comment=pre_comment) item.cron = self self.append(item, before=before) return item def find_command(self, command): """Return an iter of jobs matching any part of the command.""" for job in list(self.crons): if isinstance(command, type(ITEMREX)): if command.findall(job.command): yield job elif command in job.command: yield job def find_comment(self, comment): """Return an iter of jobs that match the comment field exactly.""" for job in list(self.crons): if isinstance(comment, type(ITEMREX)): if comment.findall(job.comment): yield job elif comment == job.comment: yield job def find_time(self, *args): """Return an iter of jobs that match this time pattern""" for job in list(self.crons): if job.slices == CronSlices(*args): yield job @property def commands(self): """Return a generator of all unqiue commands used in this crontab""" returned = [] for cron in self.crons: if cron.command not in returned: yield cron.command returned.append(cron.command) @property def comments(self): """Return a generator of all unique comments/Id used in this crontab""" returned = [] for cron in self.crons: if cron.comment and cron.comment not in returned: yield cron.comment returned.append(cron.comment) def remove_all(self, *args, **kwargs): """Removes all crons using the stated command OR that have the stated comment OR removes everything if no arguments specified. command - Remove all with this command comment - Remove all with this comment or ID time - Remove all with this time code """ if args: raise AttributeError("Invalid use: remove_all(command='cmd')") if 'command' in kwargs: return self.remove(*self.find_command(kwargs['command'])) if 'comment' in kwargs: return self.remove(*self.find_comment(kwargs['comment'])) if 'time' in kwargs: return self.remove(*self.find_time(kwargs['time'])) return self.remove(*self.crons[:]) def remove(self, *items): """Remove a selected cron from the crontab.""" result = 0 for item in items: if isinstance(item, (list, tuple, types.GeneratorType)): for subitem in item: result += self._remove(subitem) elif isinstance(item, CronItem): result += self._remove(item) else: raise TypeError("You may only remove CronItem objects, "\ "please use remove_all() to specify by name, id, etc.") return result def _remove(self, item): """Internal removal of an item""" # Manage siblings when items are deleted for sibling in self.lines[self.lines.index(item)+1:]: if isinstance(sibling, CronItem): env = sibling.env sibling.env = item.env sibling.env.update(env) sibling.env.job = sibling break if sibling != '': break self.lines.remove(sibling) self.crons.remove(item) self.lines.remove(item) return 1 def __repr__(self): kind = 'System ' if self._user is False else '' if self.filen: return f"<{kind}CronTab '{self.filen}'>" if self.user and not self.user_opt: return "" if self.user: return f"" return f"" def __iter__(self): """Return generator so we can track jobs after removal""" for job in list(self.crons.__iter__()): yield job def __getitem__(self, i): return self.crons[i] def __len__(self): return len(self.crons) def __str__(self): return self.render() class CronItem: """ An item which objectifies a single line of a crontab and May be considered to be a cron job object. """ def __init__(self, command='', comment='', user=None, pre_comment=False): self.cron = None self.user = user self.valid = False self.enabled = True self.special = False self.comment = None self.command = None self.last_run = None self.env = OrderedVariableList(job=self) # Marker labels Ansible jobs etc self.pre_comment = False self.marker = None self.stdin = None self._log = None # Initalise five cron slices using static info. self.slices = CronSlices() self.set_comment(comment, pre_comment) if command: self.set_command(command) def __hash__(self): return hash((self.command, self.comment, self.hour, self.minute, self.dow)) def __eq__(self, other): if not isinstance(other, CronItem): return False return self.__hash__() == other.__hash__() @classmethod def from_line(cls, line, user=None, cron=None): """Generate CronItem from a cron-line and parse out command and comment""" obj = cls(user=user) obj.cron = cron obj.parse(line.strip()) return obj def delete(self): """Delete this item and remove it from it's parent""" if not self.cron: raise UnboundLocalError("Cron item is not in a crontab!") self.cron.remove(self) def set_command(self, cmd, parse_stdin=False): """Set the command and filter as needed""" if parse_stdin: cmd = cmd.replace('%', '\n').replace('\\\n', '%') if '\n' in cmd: cmd, self.stdin = cmd.split('\n', 1) self.command = _str(cmd.strip()) self.valid = True def set_comment(self, cmt, pre_comment=False): """Set the comment and don't filter, pre_comment indicates comment appears before the cron, otherwise it appears ont he same line after the command. """ if cmt and cmt[:8] == 'Ansible:': self.marker = 'Ansible' cmt = cmt[8:].lstrip() pre_comment = True self.comment = cmt self.pre_comment = pre_comment def parse(self, line): """Parse a cron line string and save the info as the objects.""" line = _str(line) if not line or line[0] == '#': self.enabled = False line = line.strip().lstrip('#').strip() # We parse all lines so we can detect disabled entries. self._set_parse(ITEMREX.findall(line), line) self._set_parse(SPECREX.findall(line), line) def _set_parse(self, result, line=""): """Set all the parsed variables into the item""" if not result: return self.comment = result[0][-1] if self.cron.user is False: # Special flag to look for per-command user ret = result[0][-3].split(None, 1) self.set_command(ret[-1], True) if len(ret) == 2: self.user = ret[0] else: # Disabled jobs might be ordinary comments, so log as DEBUG level = logging.ERROR if self.enabled else logging.DEBUG self.valid = False self.enabled = False LOG.log(level, str("Missing user or command in system cron %s: %s"), '' if self.cron is None else (self.cron.tabfile or ''), line) else: self.set_command(result[0][-3], True) try: self.setall(*result[0][:-3]) except (ValueError, KeyError) as err: if self.enabled: LOG.error(str(err)) self.valid = False self.enabled = False def enable(self, enabled=True): """Set if this cron job is enabled or not""" if enabled in [True, False]: self.enabled = enabled return self.enabled def is_enabled(self): """Return true if this job is enabled (not commented out)""" return self.enabled def is_valid(self): """Return true if this job is valid""" return self.valid def render(self): """Render this set cron-job to a string""" if not self.is_valid() and self.enabled: raise ValueError('Refusing to render invalid crontab.' ' Disable to continue.') command = _str(self.command).replace('%', '\\%') user = '' if self.cron and self.cron.user is False: if not self.user: raise ValueError("Job to system-cron format, no user set!") user = self.user + ' ' rend = self.slices.render() result = f"{rend} {user}{command}" if self.stdin: result += ' %' + self.stdin.replace('\n', '%') if not self.enabled: result = "# " + result if self.comment: comment = self.comment = _str(self.comment) if self.marker: comment = f"#{self.marker}: {comment}" else: comment = "# " + comment if SYSTEMV or self.pre_comment or self.stdin: result = comment + "\n" + result else: result += ' ' + comment return str(self.env) + result def every_reboot(self): """Set to every reboot instead of a time pattern: @reboot""" self.clear() return self.slices.setall('@reboot') def every(self, unit=1): """ Replace existing time pattern with a single unit, setting all lower units to first value in valid range. For instance job.every(3).days() will be `0 0 */3 * *` while job.day().every(3) would be `* * */3 * *` Many of these patterns exist as special tokens on Linux, such as `@midnight` and `@hourly` """ return Every(self.slices, unit) def setall(self, *args): """Replace existing time pattern with these five values given as args: job.setall("1 2 * * *") job.setall(1, 2) == '1 2 * * *' job.setall(0, 0, None, '>', 'SUN') == '0 0 * 12 SUN' """ return self.slices.setall(*args) def clear(self): """Clear the special and set values""" return self.slices.clear() def frequency(self, year=None): """Returns the number of times this item will execute in a given year (defaults to this year) """ return self.slices.frequency(year=year) def frequency_at_hour(self, year=None, month=None, day=None, hour=None): """Returns the number of times this item will execute in a given hour (defaults to this hour) """ return self.slices.frequency_at_hour(year=year, month=month, day=day, hour=hour) def frequency_at_day(self, year=None, month=None, day=None): """Returns the number of times this item will execute in a given day (defaults to today) """ return self.slices.frequency_at_day(year=year, month=month, day=day) def frequency_at_month(self, year=None, month=None): """Returns the number of times this item will execute in a given month (defaults to this month) """ return self.slices.frequency_at_month(year=year, month=month) def frequency_at_year(self, year=None): """Returns the number of times this item will execute in a given year (defaults to this year) """ return self.slices.frequency_at_year(year=year) def frequency_per_year(self, year=None): """Returns the number of /days/ this item will execute on in a year (defaults to this year) """ return self.slices.frequency_per_year(year=year) def frequency_per_day(self): """Returns the number of time this item will execute in any day""" return self.slices.frequency_per_day() def frequency_per_hour(self): """Returns the number of times this item will execute in any hour""" return self.slices.frequency_per_hour() def run_pending(self, now=None): """Runs the command if scheduled""" now = now or datetime.now() if self.is_enabled(): if self.last_run is None: self.last_run = now next_time = self.schedule(self.last_run).get_next() if next_time < now: self.last_run = now return self.run() return -1 def run(self): """Runs the given command as a pipe""" env = os.environ.copy() env.update(self.env.all()) shell = self.env.get('SHELL', SHELL) process = Process(shell, '-c', self.command, env=env).run() if process.stderr: LOG.error(process.stderr) return process def schedule(self, date_from=None): """Return a croniter schedule if available.""" if not date_from: date_from = datetime.now() try: # Croniter is an optional import from croniter.croniter import croniter # pylint: disable=import-outside-toplevel except ImportError as err: raise ImportError("Croniter not available. Please install croniter" " python module via pip or your package manager") from err return croniter(self.slices.clean_render(), date_from, ret_type=datetime) def description(self, **kw): """ Returns a description of the crontab's schedule (if available) **kw - Keyword arguments to pass to cron_descriptor (see docs) """ try: from cron_descriptor import ExpressionDescriptor # pylint: disable=import-outside-toplevel except ImportError as err: raise ImportError("cron_descriptor not available. Please install"\ "cron_descriptor python module via pip or your package manager") from err exdesc = ExpressionDescriptor(self.slices.clean_render(), **kw) return exdesc.get_description() @property def log(self): """Return a cron log specific for this job only""" if not self._log and self.cron: self._log = self.cron.log.for_program(self.command) return self._log @property def minute(self): """Return the minute slice""" return self.slices[0] @property def minutes(self): """Same as minute""" return self.minute @property def hour(self): """Return the hour slice""" return self.slices[1] @property def hours(self): """Same as hour""" return self.hour @property def day(self): """Return the day slice""" return self.dom @property def dom(self): """Return the day-of-the month slice""" return self.slices[2] @property def month(self): """Return the month slice""" return self.slices[3] @property def months(self): """Same as month""" return self.month @property def dow(self): """Return the day of the week slice""" return self.slices[4] def __repr__(self): return f"" def __len__(self): return len(str(self)) def __getitem__(self, key): return self.slices[key] def __lt__(self, value): return self.frequency() < CronSlices(value).frequency() def __gt__(self, value): return self.frequency() > CronSlices(value).frequency() def __str__(self): return self.render() class Every: """Provide an interface to the job.every() method: Available Calls: minute, minutes, hour, hours, dom, doms, month, months, dow, dows Once run all units will be cleared (set to *) then proceeding units will be set to '0' and the target unit will be set as every x units. """ def __init__(self, item, units): self.slices = item self.unit = units for (key, name) in enumerate(['minute', 'hour', 'dom', 'month', 'dow', 'min', 'hour', 'day', 'moon', 'weekday']): setattr(self, name, self.set_attr(key % 5)) setattr(self, name+'s', self.set_attr(key % 5)) def set_attr(self, target): """Inner set target, returns function""" def innercall(): """Returned inner call for setting slice targets""" self.slices.clear() # Day-of-week is actually a level 2 set, not level 4. for key in range(target == 4 and 2 or target): self.slices[key].on('<') self.slices[target].every(self.unit) return innercall def year(self): """Special every year target""" if self.unit > 1: raise ValueError(f"Invalid value '{self.unit}', outside 1 year") self.slices.setall('@yearly') class CronSlices(list): """Controls a list of five time 'slices' which reprisent: minute frequency, hour frequency, day of month frequency, month requency and finally day of the week frequency. """ def __init__(self, *args): super().__init__([CronSlice(info) for info in S_INFO]) self.special = None self.setall(*args) self.is_valid = self.is_self_valid def is_self_valid(self, *args): """Object version of is_valid""" return CronSlices.is_valid(*(args or (self,))) @classmethod def is_valid(cls, *args): #pylint: disable=method-hidden """Returns true if the arguments are valid cron pattern""" try: return bool(cls(*args)) except (ValueError, KeyError): return False def setall(self, *slices): """Parses the various ways date/time frequency can be specified""" self.clear() if len(slices) == 1: (slices, self.special) = self._parse_value(slices[0]) if slices[0] == '@reboot': return if id(slices) == id(self): raise AssertionError("Can not set cron to itself!") for set_a, set_b in zip(self, slices): set_a.parse(set_b) @staticmethod def _parse_value(value): """Parse a single value into an array of slices""" if isinstance(value, str) and value: return CronSlices._parse_str(value) if isinstance(value, CronItem): return value.slices, None if isinstance(value, datetime): return [value.minute, value.hour, value.day, value.month, '*'], None if isinstance(value, time): return [value.minute, value.hour, '*', '*', '*'], None if isinstance(value, date): return [0, 0, value.day, value.month, '*'], None # It might be possible to later understand timedelta objects # but there's no convincing mathematics to do the conversion yet. if not isinstance(value, (list, tuple)): typ = type(value).__name__ raise ValueError(f"Unknown type: {typ}") return value, None @staticmethod def _parse_str(value): """Parse a string which contains slice information""" key = value.lstrip('@').lower() if value.count(' ') == 4: return value.strip().split(' '), None if key in SPECIALS: return SPECIALS[key].split(' '), '@' + key if value.startswith('@'): raise ValueError(f"Unknown special '{value}'") return [value], None def clean_render(self): """Return just numbered parts of this crontab""" return ' '.join([str(s) for s in self]) def render(self): "Return just the first part of a cron job (the numbers or special)" slices = self.clean_render() if self.special and SPECIALS_CONVERSION is not False: if self.special == '@reboot' or \ SPECIALS[self.special.strip('@')] == slices: return self.special if not SYSTEMV and SPECIALS_CONVERSION is True: for (name, value) in SPECIALS.items(): if value == slices and name not in SPECIAL_IGNORE: return f"@{name}" return slices def clear(self): """Clear the special and set values""" self.special = None for item in self: item.clear() def frequency(self, year=None): """Return frequence per year times frequency per day""" return self.frequency_per_year(year=year) * self.frequency_per_day() def frequency_per_year(self, year=None): """Returns the number of times this item will execute in a given year (default is this year)""" result = 0 if not year: year = date.today().year weekdays = list(self[4]) for month in self[3]: for day in self[2]: try: if (date(year, month, day).weekday() + 1) % 7 in weekdays: result += 1 except ValueError: continue return result def frequency_per_day(self): """Returns the number of times this item will execute in any day""" return len(self[0]) * len(self[1]) def frequency_per_hour(self): """Returns the number of times this item will execute in any hour""" return len(self[0]) def frequency_at_year(self, year=None): """Returns the number of /days/ this item will execute in a given year (default is this year)""" if not year: year = date.today().year total = 0 for month in range(1, 13): total += self.frequency_at_month(year, month) return total def frequency_at_month(self, year=None, month=None): """Returns the number of times this item will execute in given month (default: current month) """ if year is None and month is None: year = date.today().year month = date.today().month elif year is None or month is None: raise ValueError( f"One of more arguments undefined: year={year}, month={month}") total = 0 if month in self[3]: # Calculate amount of days of specific month days = monthrange(year, month)[1] for day in range(1, days + 1): total += self.frequency_at_day(year, month, day) return total def frequency_at_day(self, year=None, month=None, day=None): """Returns the number of times this item will execute in a day (default: any executed day) """ # If arguments provided, all needs to be provided test_none = [x is None for x in [year, month, day]] if all(test_none): return len(self[0]) * len(self[1]) if any(test_none): raise ValueError( f"One of more arguments undefined: year={year}, month={month}, day={day}") total = 0 if day in self[2]: for hour in range(24): total += self.frequency_at_hour(year, month, day, hour) return total def frequency_at_hour(self, year=None, month=None, day=None, hour=None): """Returns the number of times this item will execute in a hour (default: any executed hour) """ # If arguments provided, all needs to be provided test_none = [x is None for x in [year, month, day, hour]] if all(test_none): return len(self[0]) if any(test_none): raise ValueError( f"One of more arguments undefined: year={year}, month={month}, day={day}, hour={hour}") result = 0 weekday = date(year, month, day).weekday() # Check if scheduled for execution at defined moment if hour in self[1] and \ day in self[2] and \ month in self[3] and \ ((weekday + 1) % 7) in self[4]: result = len(self[0]) return result def __str__(self): return self.render() def __eq__(self, arg): return self.render() == CronSlices(arg).render() class SundayError(KeyError): """Sunday was specified as 7 instead of 0""" class Also: """Link range values together (appending instead of replacing)""" def __init__(self, obj): self.obj = obj def every(self, *a): """Also every one of these""" return self.obj.every(*a, also=True) def on(self, *a): """Also on these""" return self.obj.on(*a, also=True) def during(self, *a): """Also during these""" return self.obj.during(*a, also=True) class CronSlice: """Cron slice object which shows a time pattern""" def __init__(self, info, value=None): if isinstance(info, int): info = S_INFO[info] self.min = info.get('min', None) self.max = info.get('max', None) self.name = info.get('name', None) self.enum = info.get('enum', None) self.parts = [] if value: self.parse(value) def __hash__(self): return hash(str(self)) def parse(self, value): """Set values into the slice.""" self.clear() import sys if value is not None: for part in str(value).split(','): if part.find("/") > 0 or part.find("-") > 0 or "~" in part or part == '*': self.parts += self.get_range(part) else: self.parts.append(self.parse_value(part, sunday=0)) def render(self, resolve=False): """Return the slice rendered as a crontab. resolve - return integer values instead of enums (default False) """ if not self.parts: return '*' return _render_values(self.parts, ',', resolve) def __repr__(self): return f"" def __eq__(self, value): return str(self) == str(value) def __str__(self): return self.render() def every(self, n_value, also=False, random=False): """Set the every X units value""" n_value = self.test_value(n_value) if not also: self.clear() self.parts += self.get_range(int(n_value), random=random) 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, random=False): """Set the During value, which sets a range""" if not also: self.clear() self.parts += self.get_range(str(vfrom) + '-' + str(vto), random=random) return self.parts[-1] def random_during(self, vfrom, vto): """Set command to run at one random time""" return self.during(vfrom, vto, random=True) @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, random=False): """Return a cron range for this slice""" ret = CronRange(self, *vrange, random=random) if ret.dangling is not None: return [ret.dangling, ret] return [ret] def __iter__(self): """Return the entire element as an iterable""" ret = {} # An empty part means '*' which is every(1) if not self.parts: self.every(1) for part in self.parts: if isinstance(part, CronRange): for bit in part.range(): ret[bit] = 1 else: ret[int(part)] = 1 for val in ret: yield val def __len__(self): """Returns the number of times this slice happens in it's range""" return len(list(self.__iter__())) def parse_value(self, val, sunday=None): """Parse the value of the cron slice and raise any errors needed""" if val == '>': val = self.max elif val == '<': val = self.min try: out = get_cronvalue(val, self.enum) except ValueError as err: raise ValueError(f"Unrecognised {self.name}: '{val}'") from err except KeyError as err: raise KeyError(f"No enumeration for {self.name}: '{val}'") from err return self.test_value(out, sunday=sunday) def test_value(self, value, sunday=None): """Test the value is within range for this slice""" if self.max == 6 and int(value) == 7: if sunday is not None: return sunday raise SundayError("Detected Sunday as 7 instead of 0!") if int(value) < self.min or int(value) > self.max: raise ValueError(f"'{value}', not in {self.min}-{self.max} for {self.name}") return value def get_cronvalue(value, enums): """Returns a value as int (pass-through) or a special enum value""" if isinstance(value, int): return value if str(value).isdigit(): return int(str(value)) if not enums: raise KeyError("No enumeration allowed") return CronValue(str(value), enums) class CronValue: # pylint: disable=too-few-public-methods """Represent a special value in the cron line""" def __init__(self, value, enums): self.text = value self.value = enums.index(value.lower()) def __lt__(self, value): return self.value < int(value) def __repr__(self): return str(self) def __str__(self): return self.text def __int__(self): return self.value def _render_values(values, sep=',', resolve=False): """Returns a rendered list, sorted and optionally resolved""" if len(values) > 1: values.sort() return sep.join([_render(val, resolve) for val in values]) def _render(value, resolve=False): """Return a single value rendered""" if isinstance(value, CronRange): return value.render(resolve) if resolve: return str(int(value)) return str(f'{value:02d}' if ZERO_PAD else value) class CronRange: """A range between one value and another for a time range.""" def __init__(self, vslice, *vrange, random=False): # holds an extra dangling entry, for example sundays. self.dangling = None self.slice = vslice self.cron = None self.seq = 1 self.is_random_range = random if not vrange: self.all() elif isinstance(vrange[0], str): self.parse(vrange[0]) elif isinstance(vrange[0], (int, CronValue)): if len(vrange) == 2: (self.vfrom, self.vto) = vrange else: self.seq = vrange[0] self.all() def parse(self, value): """Parse a ranged value in a cronjob""" if value.count('/') == 1: value, seq = value.split('/') try: self.seq = self.slice.parse_value(seq) except SundayError: self.seq = 1 value = "0-0" if self.seq < 1 or self.seq > self.slice.max: raise ValueError("Sequence can not be divided by zero or max") if value.count('~') == 1 and value.count('-') == 0 and len(value) > 1: self.is_random_range = True value = value.replace('~', '-') if value.count('-') == 1: vfrom, vto = value.split('-') self.vfrom = self.slice.parse_value(vfrom, sunday=0) try: self.vto = self.slice.parse_value(vto) except SundayError: if self.vfrom == 1: self.vfrom = 0 else: self.dangling = 0 self.vto = self.slice.parse_value(vto, sunday=6) if self.vto < self.vfrom: raise ValueError(f"Bad range '{self.vfrom}-{self.vto}'") elif value in '~*': self.all() self.is_random_range = value == '~' else: raise ValueError(f'Unknown cron range value "{value}"') def all(self): """Set this slice to all units between the miniumum and maximum""" self.vfrom = self.slice.min self.vto = self.slice.max def render(self, resolve=False): """Render the ranged value for a cronjob""" value = '*~'[self.is_random_range] if int(self.vfrom) > self.slice.min or int(self.vto) < self.slice.max: if self.vfrom == self.vto: value = str(self.vfrom) else: value = _render_values([self.vfrom, self.vto], "-~"[self.is_random_range], resolve) if self.seq != 1: value += f"/{self.seq:d}" if value != '*' and SYSTEMV: value = ','.join([str(val) for val in self.range()]) return value def range(self): """Returns the range of this cron slice as a iterable list""" return range(int(self.vfrom), int(self.vto)+1, self.seq) def every(self, value): """Set the sequence value for this range.""" self.seq = int(value) def __lt__(self, value): return int(self.vfrom) < int(value) def __gt__(self, value): return int(self.vto) > int(value) def __int__(self): return int(self.vfrom) def __str__(self): return self.render() class OrderedVariableList(OrderedDict): """An ordered dictionary with a linked list containing the previous OrderedVariableList which this list depends. Duplicates in this list are weeded out in favour of the previous list in the chain. This is all in aid of the ENV variables list which must exist one per job in the chain. """ def __init__(self, *args, **kw): self.job = kw.pop('job', None) super().__init__(*args, **kw) @property def previous(self): """Returns the previous env in the list of jobs in the cron""" if self.job is not None and self.job.cron is not None: index = self.job.cron.crons.index(self.job) if index == 0: return self.job.cron.env return self.job.cron[index-1].env return None def all(self): """ Returns the full dictionary, everything from this dictionary plus all those in the chain above us. """ if self.job is not None: ret = self.previous.all().copy() ret.update(self) return ret return self.copy() def __getitem__(self, key): previous = self.previous if key in self: return super().__getitem__(key) if previous is not None: return previous.all()[key] raise KeyError(f"Environment Variable '{key}' not found.") def __str__(self): """Constructs to variable list output used in cron jobs""" ret = [] for key, value in self.items(): if self.previous: if self.previous.all().get(key, None) == value: continue if ' ' in str(value) or value == '': value = f'"{value}"' ret.append(f"{key}={value}") ret.append('') return "\n".join(ret) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/crontabs.py000066400000000000000000000107321524461074600243750ustar00rootroot00000000000000# # Copyright 2016, Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ The crontabs manager will list all available crontabs on the system. """ import os import sys import itertools from os import stat, access, X_OK from crontab import CronTab, WINOS getpwuid = None if not WINOS: from pwd import getpwuid class UserSpool(list): """Generates all user crontabs, yields both owned and abandoned tabs""" def __init__(self, loc, tabs=None): for username in self.listdir(loc): tab = self.generate(loc, username) if tab: self.append(tab) if not self: tab = CronTab(user=True) if tab: self.append(tab) def listdir(self, loc): try: return os.listdir(loc) except OSError: return [] def get_owner(self, path): """Returns user file at path""" if not getpwuid: raise OSError("This functionality is not available on Windows") try: return getpwuid(stat(path).st_uid).pw_name except KeyError: return def generate(self, loc, username): path = os.path.join(loc, username) if username != self.get_owner(path): # Abandoned crontab pool entry! return CronTab(tabfile=path) return CronTab(user=username) class SystemTab(list): """Generates all system tabs""" def __init__(self, loc, tabs=None): if os.path.isdir(loc): for item in os.listdir(loc): if item[0] == '.': continue path = os.path.join(loc, item) self.append(CronTab(user=False, tabfile=path)) elif os.path.isfile(loc): self.append(CronTab(user=False, tabfile=loc)) class AnaCronTab(list): """Attempts to digest anacron entries (if possible)""" def __init__(self, loc, tabs=None): if tabs and os.path.isdir(loc): self.append(CronTab(user=False)) jobs = list(tabs.all.find_command(loc)) if jobs: for item in os.listdir(loc): self.add(loc, item, jobs[0]) jobs[0].delete() def add(self, loc, item, anajob): path = os.path.join(loc, item) if item in ['0anacron'] or item[0] == '.' or not access(path, X_OK): return job = self[0].new(command=path, user=anajob.user) job.set_comment('Anacron %s' % loc.split('.')[-1]) job.setall(anajob) return job # Files are direct, directories are listed (recursively) KNOWN_LOCATIONS = [ # Known linux locations (Debian, RedHat, etc) (UserSpool, '/var/spool/cron/crontabs/'), (SystemTab, '/etc/crontab'), (SystemTab, '/etc/cron.d/'), # Anacron digestion (we want to know more) (AnaCronTab, '/etc/cron.hourly'), (AnaCronTab, '/etc/cron.daily'), (AnaCronTab, '/etc/cron.weekly'), (AnaCronTab, '/etc/cron.monthly'), # Known MacOSX locations # None # Other (windows, bsd) # None ] class CronTabs(list): """Singleton dictionary of all detectable crontabs""" _all = None _self = None def __new__(cls, *args, **kw): if not cls._self: cls._self = super(CronTabs, cls).__new__(cls, *args, **kw) return cls._self def __init__(self): if not self: for loc in KNOWN_LOCATIONS: self.add(*loc) def add(self, cls, *args): for tab in cls(*args, tabs=self): self.append(tab) self._all = None @property def all(self): """Return a CronTab object with all jobs (read-only)""" if self._all is None: self._all = CronTab(user=False) for tab in self: for job in tab: if job.user is None: job.user = tab.user or 'unknown' self._all.append(job) return self._all python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/pyproject.toml000066400000000000000000000001721524461074600251210ustar00rootroot00000000000000[tool.ruff] line-length = 128 [tool.ruff.lint] ignore = [ "E402", "E731", "F401", "F821", "F841", ] python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/setup.py000077500000000000000000000053071524461074600237270ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2008-2025 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 # 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 = 'python-crontab', version = '3.3.0', release = RELEASE, description = 'Python Crontab API', long_description = description, long_description_content_type = "text/x-rst", author = 'Martin Owens', url = 'https://gitlab.com/doctormo/python-crontab/', author_email = 'doctormo@gmail.com', test_suite = 'tests', platforms = 'linux', license = 'LGPLv3', py_modules = ['crontab', 'crontabs', 'cronlog'], provides = ['crontab', 'crontabs', 'cronlog'], 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', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: SunOS/Solaris', 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ], options = { 'bdist_rpm': { 'build_requires': [ 'python', 'python-setuptools', ], 'release': RELEASE, }, }, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/000077500000000000000000000000001524461074600233475ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/__init__.py000066400000000000000000000000011524461074600254470ustar00rootroot00000000000000 python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/000077500000000000000000000000001524461074600242605ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/anacron/000077500000000000000000000000001524461074600257015ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/anacron/an_command.sh000077500000000000000000000000001524461074600303220ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/anacron/not_command.txt000066400000000000000000000000001524461074600307260ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/bash\win.exe000077500000000000000000000001341524461074600265530ustar00rootroot00000000000000#!/usr/bin/env python import sys print("Double Glazing Installed:{}".format(sys.argv[1])) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/basic.log000066400000000000000000000001541524461074600260440ustar00rootroot00000000000000A really long line which can test log lines ability to put two bits together First Line 9 2 Sickem The End python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontab000066400000000000000000000000611524461074600256300ustar00rootroot00000000000000 * * * * * peter parker * 2 * * * driver parker python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontabs/000077500000000000000000000000001524461074600260735ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontabs/.empty000066400000000000000000000000001524461074600272200ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontabs/system_one000066400000000000000000000000621524461074600302010ustar00rootroot00000000000000 * * * * * bilbo baggins * 2 * * * frodo baggins python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontabs/system_two000066400000000000000000000000341524461074600302300ustar00rootroot00000000000000 1 2 3 * * plastic baggins python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/crontest000077500000000000000000000025261524461074600260540ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys def main(): """Run this""" args = sys.argv[1:] loc = os.path.dirname(__file__) if '-h' in args: print('|'.join(sorted(args))) return elif '-e' in args: sys.stderr.write('|'.join(sorted(args)) + "\n") return elif '-ev' in args: print(os.environ.get('CR_VAR', 'FAILED') + '\n') return user = 'user' if '-u' in args: user = args[args.index('-u')+1] if user == 'john': sys.stderr.write("Program Error") sys.exit(2) if '-l' in args: if user == 'error': raise ValueError("Delibrate IO Error") if not os.path.exists(os.path.join(loc, 'spool', user)): sys.stderr.write("no crontab for %s\n" % user) sys.exit(1) fhl = open(os.path.join(loc, 'spool', user), 'r') print(fhl.read()) return for filename in args: if filename[0] == '-' or filename == user: continue new_name = os.path.join(loc, 'spool', user) if not os.path.exists(filename): raise KeyError("Can't find file: {}".format(filename)) with open(filename, 'r') as source: with open(new_name, 'w') as output: output.write(source.read()) if __name__ == '__main__': main() sys.exit(0) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/specials.tab000066400000000000000000000000621524461074600265510ustar00rootroot000000000000000 * * * * hourly 0 0 * * * daily 0 0 * * 0 weekly python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/specials_enc.tab000066400000000000000000000001151524461074600273750ustar00rootroot00000000000000@hourly hourly @daily daily @midnight midnight @weekly weekly @reboot reboot python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/spool/000077500000000000000000000000001524461074600254145ustar00rootroot00000000000000python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/spool/basic000066400000000000000000000000271524461074600264170ustar00rootroot000000000000000 * * * * firstcommand python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/spool/hgreen000066400000000000000000000001221524461074600266020ustar00rootroot00000000000000 # Hank Green's crontab 0 0 5 */2 * start_company 0 0 * * 6 do_vlog_brothers python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/spool/jgreen000066400000000000000000000001171524461074600266100ustar00rootroot00000000000000 # John Green's crontab 0 */2 * * * write_book 0 0 * * 2 do_vlog_brothers python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/spool/user000066400000000000000000000000521524461074600263120ustar00rootroot00000000000000 */4 * * * * user_command # user_comment python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/test.log000066400000000000000000000045511524461074600257470ustar00rootroot00000000000000Apr 4 21:24:01 servername CRON[16490]: (user) CMD (userscript &> /dev/null) Apr 4 21:24:01 servername CRON[16489]: (root) CMD (rootscript &> /dev/null) Apr 4 21:24:01 servername CRON[16487]: (CRON) info (Cron information item) Apr 4 21:25:01 servername CRON[16496]: (user) CMD (userscript &> /dev/null) Apr 4 21:25:01 servername CRON[16497]: (root) CMD (shadowscript &> /dev/null) Apr 4 21:25:01 servername CRON[16494]: (CRON) info (Cron information item) Apr 4 21:26:01 servername CRON[16513]: (user) CMD (userscript &> /dev/null) Apr 4 21:26:01 servername CRON[16511]: (CRON) info (Cron information item) Apr 4 21:26:01 servername CRON[16514]: (root) CMD (rootscript &> /dev/null) Apr 4 21:27:01 servername CRON[16519]: (user) CMD (userscript &> /dev/null) Apr 4 21:27:01 servername CRON[16518]: (CRON) info (Cron information item) Apr 4 21:28:01 servername CRON[16523]: (user) CMD (userscript &> /dev/null) Apr 4 21:28:01 servername CRON[16522]: (root) CMD (rootscript &> /dev/null) Apr 4 21:28:01 servername CRON[16520]: (CRON) info (Cron information item) Apr 4 21:28:30 servername NOTCRON: Log entry that isn't a cron Apr 4 21:28:30 servername NOTCRON: entry in order to test Apr 4 21:28:31 servername NOTCRON: that these are ignored Apr 4 21:29:01 servername CRON[16539]: (user) CMD (userscript &> /dev/null) Apr 4 21:29:01 servername CRON[16538]: (CRON) info (Cron information item) Apr 4 21:30:01 servername CRON[16551]: (root) CMD (shadowscript &> /dev/null) Apr 4 21:30:01 servername CRON[16552]: (root) CMD (rootscript &> /dev/null) Apr 4 21:30:01 servername CRON[16554]: (user) CMD (userscript &> /dev/null) Apr 4 21:30:01 servername CRON[16548]: (CRON) info (Cron information item) Apr 4 21:31:01 servername CRON[16569]: (user) CMD (userscript &> /dev/null) Apr 4 21:31:01 servername CRON[16568]: (CRON) info (Cron information item) Apr 4 21:32:01 servername CRON[16573]: (user) CMD (userscript &> /dev/null) Apr 4 21:32:01 servername CRON[16574]: (root) CMD (rootscript &> /dev/null) Apr 4 21:32:01 servername CRON[16571]: (CRON) info (Cron information item) Apr 4 21:33:02 servername CRON[16588]: (user) CMD (userscript &> /dev/null) Apr 4 21:33:02 servername CRON[16587]: (CRON) info (Cron information item) Apr 4 21:34:01 servername CRON[16591]: (user) CMD (userscript &> /dev/null) Apr 4 21:34:01 servername CRON[16592]: (root) CMD (rootscript &> /dev/null) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/data/test.tab000066400000000000000000000007271524461074600257350ustar00rootroot00000000000000# 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 python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_compatibility.py000066400000000000000000000076341524461074600276430ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2012 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab interaction. """ import os import sys import unittest import crontab TEST_DIR = os.path.dirname(__file__) INITAL_TAB = """ # First Comment 0,30 * * * * firstcommand """ class CompatTestCase(unittest.TestCase): """Test basic functionality of crontab.""" @classmethod def setUpClass(cls): crontab.SYSTEMV = True @classmethod def tearDownClass(cls): crontab.SYSTEMV = False def setUp(self): self.crontab = crontab.CronTab(tab=INITAL_TAB) def test_00_enabled(self): """Test Compatability Mode""" self.assertTrue(crontab.SYSTEMV) def test_01_addition(self): """New Job Rendering""" job = self.crontab.new('addition1') job.minute.during(0, 3) job.hour.during(21, 23).every(1) job.dom.every(1) self.assertEqual(job.render(), '0,1,2,3 21,22,23 * * * addition1') def test_02_addition(self): """New Job Rendering""" job = self.crontab.new(command='addition2') job.minute.during(4, 9) job.hour.during(2, 10).every(2) job.dom.every(10) self.assertNotEqual(job.render(), '4-9 2-10/2 */3 * * addition2') self.assertEqual(job.render(), '4,5,6,7,8,9 2,4,6,8,10 1,11,21,31 * * addition2') def test_03_specials(self): """Ignore Special Symbols""" tab = crontab.CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) self.assertEqual(tab.render(), """0 * * * * hourly 0 0 * * * daily 0 0 * * 0 weekly """) def test_04_comments(self): """Comments should be on their own lines""" self.assertEqual(self.crontab[0].comment, 'First Comment') self.assertEqual(self.crontab.render(), INITAL_TAB) job = self.crontab.new('command', comment="Test comment") self.assertEqual(job.render(), "# Test comment\n* * * * * command") def test_05_ansible(self): """Crontab shouldn't break ansible cronjobs""" cron = crontab.CronTab(tab=""" #Ansible: {job_name} * * * * * {command} """) self.assertEqual(cron[0].comment, '{job_name}') self.assertEqual(cron[0].command, '{command}') self.assertEqual(str(cron[0]), '#Ansible: {job_name}\n* * * * * {command}') def test_06_escaped_chars(self): """Do escaped chars parse correctly when read in""" cron = crontab.CronTab(tab=""" * * * * * cmd arg_with_\\#_character # comment """) self.assertEqual(cron[0].command, 'cmd arg_with_\\#_character') self.assertEqual(cron[0].comment, 'comment') def test_07_non_posix_shell(self): """Shell in windows environments is split correctly""" from crontab import Process winfile = os.path.join(TEST_DIR, 'data', "bash\\win.exe") pipe = Process("{sys.executable} {winfile}".format(winfile=winfile, sys=sys), 'SLASHED', posix=False)._run() self.assertEqual(pipe.wait(), 0, 'Windows shell command not found!') (out, err) = pipe.communicate() self.assertEqual(out, b'Double Glazing Installed:SLASHED\n') self.assertEqual(err, b'') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( CompatTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_context.py000066400000000000000000000040341524461074600264450ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2011 Martin Owens, 2019 Jordan Miller # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab context. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab INITAL_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,FRI,WED multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ RESULT_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,WED,FRI multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ class ContextTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB) def test_01_context(self): """context matches non-context""" self.crontab.write() results = RESULT_TAB.split('\n') for line_no, line in enumerate(self.crontab.intab.split('\n')): self.assertEqual(str(line), results[line_no]) with CronTab(tab=INITAL_TAB) as crontab_context: self.crontab_context = crontab_context for line_no, line in enumerate(self.crontab_context.intab.split('\n')): self.assertEqual(str(line), results[line_no]) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( ContextTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_croniter.py000066400000000000000000000055531524461074600266150ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test croniter extention to find out when items will next happen. """ import os import sys from datetime import datetime sys.path.insert(0, '../') import unittest import crontab INITAL_TAB = """ # Basic Comment 20 * * * * execute # comment """ class CroniterTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = crontab.CronTab(tab=INITAL_TAB) self.job = list(self.crontab.find_command('execute'))[0] try: import croniter except ImportError: self.skipTest("Croniter not installed") def test_00_nocroniter(self): """No Croniter""" # Remove croniter if imported before. for i in range(1, 4): name = '.'.join((['croniter'] * i)) if name in sys.modules: del sys.modules[name] old, sys.path = sys.path, [] with self.assertRaises(ImportError): self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) sys.path = old def test_01_schedule(self): """Get Scheduler""" ct = self.job.schedule(datetime(2009, 10, 11, 5, 12, 10)) self.assertTrue(ct) def test_02_next(self): """Get Next Scheduled Items""" ct = self.job.schedule(datetime(2000, 10, 11, 5, 12, 10)) self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 5, 20, 0)) self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 6, 20, 0)) def test_03_prev(self): """Get Prev Scheduled Items""" ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_prev(), datetime(2001, 10, 11, 0, 20, 0)) self.assertEqual(ct.get_prev(), datetime(2001, 10, 10, 23, 20, 0)) def test_04_current(self): """Get Current Item""" ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10)) self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10)) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( CroniterTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_crontabs.py000066400000000000000000000073671524461074600266100ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontabs listing all available crontabs. """ import os import sys import unittest import crontab import crontabs TEST_DIR = os.path.dirname(__file__) class fake_getpwuid(object): def __init__(self, st): self.pw_name = st if st == 'error': raise KeyError("Expected Error") class fake_stat(object): def __init__(self, path): self.st_uid = os.path.basename(path).split('_')[0] crontabs.getpwuid = fake_getpwuid crontabs.stat = fake_stat class AnacronSourceTab(list): """This enables the anacron detection""" slices = "56 4 * * 4" def __init__(self, tabs=None): self.append(crontab.CronTab(user=False, tab="%s root anacron_cmd %s" \ % (self.slices, os.path.join(TEST_DIR, 'data', 'anacron')))) crontabs.KNOWN_LOCATIONS = [ (AnacronSourceTab,), (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'spool')), (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'bad_spool')), (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontab')), (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontabs')), (crontabs.AnaCronTab, os.path.join(TEST_DIR, 'data', 'anacron')), ] crontab.CRON_COMMAND = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest')) class CronTabsTestCase(unittest.TestCase): """Test use documentation in crontab.""" def setUp(self): self.tabs = crontabs.CronTabs() def assertInTabs(self, command, *users): jobs = list(self.tabs.all.find_command(command)) self.assertEqual(len(jobs), len(users)) users = sorted([job.user for job in jobs]) self.assertEqual(users, sorted(users)) return jobs def assertNotInTabs(self, command): return self.assertInTabs(command) def test_05_spool(self): """Test a user spool""" self.assertInTabs('do_vlog_brothers', 'hgreen', 'jgreen') def test_06_bad_spool(self): """Test no access to spool (non-root)""" # IMPORTANT! This is testing the fact that the bad-spool will load # the user's own crontab, in this instance this is 'user' from the # crontest script. This tab is already loaded by the previous User # spool and so we expect to find two of them. self.assertInTabs('user_command', 'user', 'user') def test_10_crontab_dir(self): """Test crontabs loaded from system directory""" self.assertInTabs('baggins', 'bilbo', 'frodo', 'plastic') def test_11_crontab_file(self): """Test a single crontab file loaded from system tab""" self.assertInTabs('parker', 'driver', 'peter') def test_20_anacron(self): """Anacron digested""" self.assertNotInTabs('anacron_cmd') jobs = self.assertInTabs('an_command.sh', 'root') self.assertEqual(str(jobs[0].slices), AnacronSourceTab.slices) self.assertNotInTabs('not_command.txt') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(CronTabsTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_description.py000066400000000000000000000040121524461074600273000ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test optional crontab description intergration. """ import sys sys.path.insert(0, '../') import unittest import crontab INITAL_TAB = """ # Basic Comment 20 10-20 */2 * * execute # At 20 minutes past the hour, between 10:00 AM and 08:59 PM, every 2 days """ class DescriptorTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = crontab.CronTab(tab=INITAL_TAB) self.job = list(self.crontab.find_command('execute'))[0] try: import cron_descriptor except ImportError: self.skipTest("Cron-descriptor module not installed") def test_00_no_module(self): """No module found""" # Remove module if imported already for i in range(1, 4): name = '.'.join((['cron_descriptor'] * i)) if name in sys.modules: del sys.modules[name] old, sys.path = sys.path, [] with self.assertRaises(ImportError): self.job.description() sys.path = old def test_01_description(self): """Get Job Description""" self.assertEqual(self.job.description(), self.job.comment) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(DescriptorTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_enums.py000066400000000000000000000070411524461074600261110ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2011 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab enumerations. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab INITAL_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,FRI,WED multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ COMMANDS = [ 'enums', 'ranges', 'multiples', 'com1', 'com2', ] RESULT_TAB = """ * * * JAN SAT enums * * * MAR-APR * ranges * * * * MON,WED,FRI multiples * * * * * com1 # Comment One * * * * * com2 # Comment One """ class EnumTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=INITAL_TAB) def test_01_presevation(self): """All Entries Re-Rendered Correctly""" self.crontab.write() results = RESULT_TAB.split('\n') line_no = 0 for line in self.crontab.intab.split('\n'): self.assertEqual(str(line), results[line_no]) line_no += 1 def test_02_simple_enum(self): """Simple Enumerations""" e = list(self.crontab.find_command('enums'))[0] self.assertEqual(e.month, 'JAN') self.assertEqual(e.month.render(True), '1') self.assertEqual(e.dow, 'SAT') self.assertEqual(e.dow.render(True), '6') def test_03_enum_range(self): """Enumeration Ranges""" e = list(self.crontab.find_command('ranges'))[0] self.assertEqual(e.month, 'MAR-APR') self.assertEqual(e.month.render(True), '3-4' ) def test_04_sets(self): """Enumeration Sets""" e = list(self.crontab.find_command('multiples'))[0] self.assertEqual(e.dow, 'MON,WED,FRI') self.assertEqual(e.dow.render(True), '1,3,5' ) def test_05_create(self): """Create by Enumeration""" job = self.crontab.new(command='new') job.month.on('JAN') job.dow.on('SUN') self.assertEqual(str(job), '* * * JAN SUN new') def test_06_create_range(self): """Created Enum Range""" job = self.crontab.new(command='new2') job.month.during('APR', 'NOV').every(2) self.assertEqual(str(job), '* * * APR-NOV/2 * new2') def test_07_create_set(self): """Created Enum Set""" job = self.crontab.new(command='new3') job.month.on('APR') job.month.also.on('NOV','JAN') self.assertEqual(str(job), '* * * JAN,APR,NOV * new3') def test_08_find_comment(self): """Comment Set""" jobs = list(self.crontab.find_comment('Comment One')) self.assertEqual(len(jobs), 2) for job in jobs: self.assertEqual(job.comment, 'Comment One') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( EnumTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_env.py000066400000000000000000000140711524461074600255530ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2017 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test the creation, reading and writing on environment variables. """ import os import sys sys.path.insert(0, '../') from .test_usage import TEST_DIR import unittest from collections import OrderedDict from crontab import CronTab INITIAL_TAB = """PERSONAL_VAR=bar CRON_VAR=fork 34 12 * * * eat_soup CRON_VAR=spoon 35 12 * * * eat_salad CRON_VAR=knife 36 12 * * * eat_icecream SECONDARY=fork 38 12 * * * eat_steak """ class EnvTestCase(unittest.TestCase): """Test vixie cron user addition.""" def setUp(self): self.crontab = CronTab(tab=INITIAL_TAB) def test_01_consistancy(self): """Read in crontab and write out the same""" self.assertEqual(INITIAL_TAB, str(self.crontab)) def test_02_top_vars(self): """Whole file env variables""" crontab = CronTab(tab="SHELL=dash\n") self.assertEqual(str(crontab), "SHELL=dash\n") self.assertEqual(crontab.env['SHELL'], 'dash') crontab.env['SHELL'] = 'bash' self.assertEqual(str(crontab), "SHELL=bash\n") self.assertEqual(crontab.env['SHELL'], 'bash') def test_03_get_job_var(self): """Test each of the job env structures""" for job, expected in zip(self.crontab, [ {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'fork'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'spoon'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife'}, {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife', 'SECONDARY': 'fork'}, ]): self.assertEqual(OrderedDict(job.env.all()), expected) def test_04_set_job_var(self): """Test that variables set are applied correctly""" self.crontab[1].env['CRON_VAR'] = 'javlin' self.assertEqual(self.crontab[1].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'}) self.crontab[2].env['CRON_VAR'] = 'javlin' self.assertEqual(self.crontab[2].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'}) self.assertEqual(self.crontab[3].env['PERSONAL_VAR'], 'bar') self.assertEqual(self.crontab[3].env.all(), {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin', 'SECONDARY': 'fork'}) self.crontab.env['PERSONAL_VAR'] = 'foo' self.assertEqual(self.crontab[2].env.all(), {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'javlin'}) self.crontab.env['CRON_VAR'] = 'fork' self.assertEqual(self.crontab[0].env.all(), {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'fork'}) self.assertEqual(str(self.crontab), """PERSONAL_VAR=foo CRON_VAR=fork 34 12 * * * eat_soup CRON_VAR=javlin 35 12 * * * eat_salad 36 12 * * * eat_icecream SECONDARY=fork 38 12 * * * eat_steak """) def test_05_no_env(self): """Test that we get an error asking for no var""" with self.assertRaises(KeyError): self.crontab.env['BLUE_BOTTLE'] with self.assertRaises(KeyError): self.crontab[0].env['RED_BOTTLE'] def test_06_env_access(self): cron = CronTab(tab=""" MYNAME='Random' * * * * * echo "first: $MYNAME" * * * * * echo "second: $MYNAME" * * * * * echo "third: $MYNAME" """) for job in cron: self.assertEqual(job.env['MYNAME'], "Random") def test_07_mutated_dict(self): """Test when the ordered dict is changed during loop""" cron = CronTab(tab=""" ALL='all' ABCD='first' * * * * * echo "first" """) def test_08_space_quotes(self): """Test that spaces and quotes are handled correctly""" cron = CronTab(tab=""" A= 123 B=" 123 " C=' 123 ' D= " 123 " E= 1 2 3 """) self.assertEqual(cron.env['A'], '123') self.assertEqual(cron.env['B'], ' 123 ') self.assertEqual(cron.env['C'], ' 123 ') self.assertEqual(cron.env['D'], ' 123 ') self.assertEqual(cron.env['E'], '1 2 3') self.assertEqual(str(cron), """A=123 B=" 123 " C=" 123 " D=" 123 " E="1 2 3" """) def test_09_delete_middle(self): """Test that a delete doesn't remove vars""" self.crontab.remove_all(command='eat_icecream') self.crontab.remove_all(command='eat_soup') self.assertEqual(str(self.crontab), """PERSONAL_VAR=bar CRON_VAR=spoon 35 12 * * * eat_salad CRON_VAR=knife SECONDARY=fork 38 12 * * * eat_steak """) def test_10_empty_env(self): """Test when an env is an empty string it should have quotes""" tab='MAILTO=""\n' self.assertEqual(str(CronTab(tab=tab)), tab) def test_11_empty_flow(self): """Test what happends when an env is involved in flow""" tab = """ # A # B MAILTO="" # C */10 * * * * /home/pi/job.py # any job """ cron = CronTab(tab=tab) job = cron.new('update.py', 'update') job.setall('1 12 * * 3') self.assertTrue(job.is_valid()) self.assertEqual(str(cron), """MAILTO="" # A # B # C */10 * * * * /home/pi/job.py # any job 1 12 * * 3 update.py # update """) cron.remove_all(comment='update') cron.write_to_user("bob") filename = os.path.join(TEST_DIR, 'data', 'spool', 'bob') with open(filename, 'r') as fhl: self.assertEqual(fhl.read(), """MAILTO="" # A # B # C */10 * * * * /home/pi/job.py # any job """) os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(EnvTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_equality.py000077500000000000000000000040561524461074600266250ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2019 Kyle Bakker # Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test equality between CronItems """ import unittest from crontab import CronTab CRONTAB1 = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 """ CRONTAB2 = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 4 * * * * command5 # CommentID K """ class EqualityTestCase(unittest.TestCase): """ Testing equality against CronItems""" def setUp(self): self.crontab1 = CronTab(tab=CRONTAB1) self.crontab2 = CronTab(tab=CRONTAB2) def test_equality(self): """Test equality""" self.assertEqual(self.crontab1[0], self.crontab2[0]) def test_inequality(self): """Test inequality""" self.assertNotEqual(self.crontab1[0], self.crontab2[1]) def test_listdifference(self): """Test diference between lists""" self.assertEqual(set(self.crontab2) - set(self.crontab1), {self.crontab2[3]}) def test_hash(self): """Test object hashing""" self.assertEqual(self.crontab1[0].__hash__(), hash((self.crontab1[0]))) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(EqualityTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_every.py000066400000000000000000000065331524461074600261210ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test simple 'every' api. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab TEST_DIR = os.path.dirname(__file__) class EveryTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab')) def test_00_minutes(self): """Every Minutes""" for job in self.crontab: job.every(3).minutes() self.assertEqual(job.slices.clean_render(), '*/3 * * * *') job.minutes.every(5) self.assertEqual(job.slices.clean_render(), '*/5 * * * *') def test_01_hours(self): """Every Hours""" for job in self.crontab: job.every(3).hours() self.assertEqual(job.slices.clean_render(), '0 */3 * * *') def test_02_dom(self): """Every Day of the Month""" for job in self.crontab: job.every(3).dom() self.assertEqual(job.slices.clean_render(), '0 0 */3 * *') def test_03_single(self): """Every Single Hour""" for job in self.crontab: job.every().hour() self.assertEqual(job.slices.clean_render(), '0 * * * *') def test_04_month(self): """Every Month""" for job in self.crontab: job.every(3).months() self.assertEqual(job.slices.clean_render(), '0 0 1 */3 *') def test_05_dow(self): """Every Day of the Week""" for job in self.crontab: job.every(3).dow() self.assertEqual(job.slices.clean_render(), '0 0 * * */3') def test_06_year(self): """Every Year""" for job in self.crontab: job.every().year() self.assertEqual(job.slices.render(), '@yearly') self.assertEqual(job.slices.clean_render(), '0 0 1 1 *') self.assertRaises(ValueError, job.every(2).year) def test_07_reboot(self): """Every Reboot""" for job in self.crontab: job.every_reboot() self.assertEqual(job.slices.render(), '@reboot') self.assertEqual(job.slices.clean_render(), '* * * * *') def test_08_newitem(self): """Every on New Item""" job = self.crontab.new(command='hourly') job.every().hour() self.assertEqual(job.slices.render(), '@hourly') job = self.crontab.new(command='firstly') job.hours.every(2) self.assertEqual(job.slices.render(), '* */2 * * *') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( EveryTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_frequency.py000066400000000000000000000162651524461074600267730ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test frequency calculations """ import os import sys from calendar import isleap from datetime import date sys.path.insert(0, '../') import unittest from crontab import CronTab START_TAB = """ """ class FrequencyTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=START_TAB.strip()) self.job = self.crontab.new(command='freq') def test_00_slice_frequency(self): """Each Slice Frequency""" # Make sure some of these overlap self.job.setall("13-16,14-15 6,*/3 2,3,4 * *") self.assertEqual(len(self.job[0]), 4) self.assertEqual(len(self.job[1]), 8) self.assertEqual(len(self.job[2]), 3) self.assertEqual(len(self.job[3]), 12) self.assertEqual(len(self.job[4]), 7) def test_01_per_day(self): """Frequency per Day""" self.job.setall("21-29 10-14 * * *") self.assertEqual(len(self.job[0]), 9) self.assertEqual(len(self.job[1]), 5) self.assertEqual(self.job.frequency_per_day(), 45) def test_02_days_per_year(self): """Frequency in Days per Year""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency_per_year(year=2012), 366) self.job.setall("1 1 11-20 1,4,6,8 0,6") self.assertEqual(len(self.job[2]), 10) self.assertEqual(len(self.job[3]), 4) self.assertEqual(len(self.job[4]), 2) self.assertEqual(self.job.frequency_per_year(year=2010), 11) self.assertEqual(self.job.frequency_per_year(year=2013), 12) self.assertEqual(self.job.frequency_per_year(year=2020), 13) def test_03_job(self): """Once Yearly""" self.job.setall("0 0 1 1 *") self.assertEqual(self.job.frequency(year=2010), 1) def test_04_twice(self): """Twice Yearly""" self.job.setall("0 0 1 1,6 *") self.assertEqual(self.job.frequency(year=2010), 2) def test_05_thrice(self): """Thrice Yearly""" self.job.setall("0 0 1 1,3,6 *") self.assertEqual(self.job.frequency(year=2010), 3) def test_06_quart(self): """Four Yearly""" self.job.setall("0 0 1 */3 *") self.assertEqual(self.job.frequency(year=2010), 4) def test_07_monthly(self): """Once a month""" self.job.setall("0 0 1 * *") self.assertEqual(self.job.frequency(year=2010), 12) def test_08_six_monthly(self): """Six a month""" self.job.setall("0 0 1,2,3,4,5,6 * *") self.assertEqual(self.job.frequency(year=2010), 72) def test_09_every_day(self): """Every Day""" self.job.setall("0 0 * * *") self.assertEqual(self.job.frequency(year=2010), 365) def test_10_every_hour(self): """Every Hour""" self.job.setall("0 * * * *") self.assertEqual(self.job.frequency(year=2010), 8760) def test_11_every_other_hour(self): """Every Other Hour""" self.job.setall("0 */2 * * *") self.assertEqual(self.job.frequency(year=2010), 4380) def test_12_every_minute(self): """Every Minute""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency(year=2010), 525600) def test_13_enum(self): """Enumerations""" self.job.setall("0 0 * * MON-WED") self.assertEqual(self.job.frequency(year=2010), 156) self.job.setall("0 0 * JAN-MAR *") self.assertEqual(self.job.frequency(year=2010), 90) def test_14_all(self): """Test Maximum""" self.job.setall("* * * * *") self.assertEqual(self.job.frequency(2010), 525600) self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency_per_day(), 1440) self.job.setall("*") self.assertEqual(self.job.frequency_per_day(), 1440) self.assertEqual(self.job.frequency_per_year(year=2010), 365) self.assertEqual(self.job.frequency(2010), 525600) def test_15_compare(self): """Compare Times""" job = self.crontab.new(command='match') job.setall("*/2 * * * *") self.assertEqual(job.slices, "*/2 * * * *") self.assertEqual(job.slices, ["*/2"]) self.assertLess(job, ["*"]) self.assertGreater(job, "*/3") def test_16_frequency_per_hour(self): """Count per hour""" job = self.crontab.new(command='per_hour') job.setall("*/2 * * * *") self.assertEqual(job.frequency_per_hour(), 30) def test_17_frequency_at_hour(self): """Frequency at hour at given moment""" job = self.crontab.new(command='at_hour') job.setall("*/2 10 * * *") self.assertEqual(job.frequency_at_hour(2021, 7, 9, 10), 30) self.assertEqual(job.frequency_at_hour(2021, 7, 9, 11), 0) self.assertEqual(job.frequency_at_hour(), 30) self.assertRaises(ValueError, job.frequency_at_hour, 2021) def test_18_frequency_at_day(self): """Frequency per day at given moment""" job = self.crontab.new(command='at_day') job.setall("2,4 7 9,14 * *") self.assertEqual(job.frequency_at_day(2021, 7, 9), 2) self.assertEqual(job.frequency_at_day(2021, 7, 10), 0) self.assertEqual(job.frequency_at_day(), 2) self.assertRaises(ValueError, job.frequency_at_day, 2021) def test_19_frequency_at_month(self): """Frequency per month at moment""" job = self.crontab.new(command='at_month') job.setall("2,4 9 7,14 10,11 *") self.assertEqual(job.frequency_at_month(2021, 10), 4) self.assertEqual(job.frequency_at_month(2021, 12), 0) self.assertIn(job.frequency_at_month(), [0, 4]) self.assertRaises(ValueError, job.frequency_at_month, 2021) def test_20_frequency_at_year(self): """Frequency at leap year day""" job = self.crontab.new(command='at_year') job.setall("0 * 3,29 2 *") self.assertEqual(job.frequency_at_year(2021), 24) self.assertEqual(job.frequency_at_year(2024), 48) self.assertEqual(job.frequency_at_year(), [24, 48][isleap(date.today().year)]) def test_21_bad_frequency(self): """Frequency must be within range""" job = self.crontab.new(command='at_year') self.assertRaises(ValueError, job.hour.every, 72) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( FrequencyTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_interaction.py000066400000000000000000000331061524461074600273020ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab interaction. """ import re import os import unittest from crontab import CronTab, CronSlices, CronSlice from .utils import LoggingMixin TEST_DIR = os.path.dirname(__file__) COMMANDS = [ 'firstcommand', 'range', 'byweek', 'disabled', 'spaced', 'python /example_app/testing.py', 'rebooted', 'echo % escaped', 'cat %%', ] RESULT_TAB = """# First Comment # Edit this line to test for mistaken checks # m h dom mon dow user command */30 * * * * firstcommand * 10-20/3 * * * range # Middle Comment * * * 10 * byweek # Comment One # * * * * * disabled 0 5 * * * spaced # Comment Two # monitorCron\\n*/59 * * * * python /example_app/testing.py @reboot rebooted # re-id # Last Comment @has this # extra # percent_one\\n* * * * * echo \\% escaped # percent_two\\n* * * * * cat \\%\\% % stdin%with%lines """ class InteractionTestCase(LoggingMixin, unittest.TestCase): """Test basic functionality of crontab.""" log_name = 'crontab' def setUp(self): super(InteractionTestCase, self).setUp() self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab')) def test_01_presevation(self): """All Entries Re-Rendered Correctly""" results = RESULT_TAB.split('\n') for line_no, line in enumerate(self.crontab.lines): self.assertEqual(str(line), results[line_no].replace('\\n', '\n')) def test_02_access(self): """All Entries Are Accessable""" for line_no, job in enumerate(self.crontab): self.assertEqual(str(job.command), COMMANDS[line_no]) else: # pylint: disable=useless-else-on-loop self.assertEqual(line_no + 1, 9) def test_03_blank(self): """Render Blank""" job = self.crontab.new(command='blank') self.assertEqual(repr(job), '') self.assertEqual(job.render(), '* * * * * blank') def test_04_number(self): """Render Number""" job = self.crontab.new(command='number') job.minute.on(4) self.assertEqual(job.render(), '4 * * * * number') def test_05_fields(self): """Render Hours Days and Weeks""" job = self.crontab.new(command='fields') job.hour.on(4) self.assertEqual(job.render(), '* 4 * * * fields') job.dom.on(5) self.assertEqual(job.render(), '* 4 5 * * fields') job.month.on(6) self.assertEqual(job.render(), '* 4 5 6 * fields') job.dow.on(7) self.assertEqual(job.render(), '* 4 5 6 0 fields') def test_05_multiple_fields(self): """Test mutliple named fields""" job = self.crontab.new(command='fields') job.hour.on(4, 6, 7) self.assertEqual(job.render(), '* 4,6,7 * * * fields') job.dow.on('SUN', 'FRI') self.assertEqual(job.render(), '* 4,6,7 * * SUN,FRI fields') def test_06_clear(self): """Render Hours Days and Weeks""" job = self.crontab.new(command='clear') job.minute.on(3) job.hour.on(4) job.dom.on(5) job.month.on(6) job.dow.on(7) self.assertEqual(job.render(), '3 4 5 6 0 clear') job.clear() self.assertEqual(job.render(), '* * * * * clear') def test_07_range(self): """Render Time Ranges""" job = self.crontab.new(command='range') job.minute.during(4, 10) self.assertEqual(job.render(), '4-10 * * * * range') job.minute.during(15, 19) self.assertEqual(job.render(), '15-19 * * * * range') job.minute.clear() self.assertEqual(job.render(), '* * * * * range') job.minute.during(15, 19) self.assertEqual(job.render(), '15-19 * * * * range') job.minute.also.during(4, 10) self.assertEqual(job.render(), '4-10,15-19 * * * * range') def test_08_sequence(self): """Render Time Sequences""" job = self.crontab.new(command='seq') job.hour.every(4) self.assertEqual(job.render(), '* */4 * * * seq') job.hour.during(2, 10) self.assertEqual(job.render(), '* 2-10 * * * seq') job.hour.clear() self.assertEqual(job.render(), '* * * * * seq') job.hour.during(2, 10).every(4) self.assertEqual(job.render(), '* 2-10/4 * * * seq') job.hour.also.during(1, 4) self.assertEqual(job.render(), '* 1-4,2-10/4 * * * seq') job.hour.also.every(4) self.assertEqual(job.render(), '* */4,1-4,2-10/4 * * * seq') def test_09_new(self): """Adding new Items at any order""" thing1 = self.crontab.new('red', comment='thing1') self.assertEqual(self.crontab[-1], thing1) thing2 = self.crontab.new('blue', before=thing1) self.assertEqual(self.crontab[-1], thing1) self.assertEqual(self.crontab[-2], thing2) thing3 = self.crontab.new('green', before='thing1') self.assertEqual(self.crontab[-1], thing1) self.assertEqual(self.crontab[-2], thing3) self.assertEqual(self.crontab[-3], thing2) thing4 = self.crontab.new('purple', before=re.compile('^thing')) self.assertEqual(self.crontab[-2], thing4) thing5 = self.crontab.new('black', before=self.crontab.find_command('blue')) self.assertEqual(self.crontab[-5], thing5) self.assertRaises(ValueError, self.crontab.new, 'fail', before='fail') def test_10_comment(self): """Render cron Comments""" job = self.crontab.new(command='com', comment='I love this') self.assertEqual(str(job), '* * * * * com # I love this') def test_11_disabled(self): """Disabled Job""" jobs = list(self.crontab.find_command('firstcommand')) self.assertTrue(jobs[0].enabled) jobs = list(self.crontab.find_command('disabled')) self.assertFalse(jobs[0].enabled) def test_12_disable(self): """Disable and Enable Job""" job = self.crontab.new(command='dis') job.enable(False) self.assertEqual(str(job), '# * * * * * dis') job.enable() self.assertEqual(str(job), '* * * * * dis') def test_13_plural(self): """Plural API""" job = self.crontab.new(command='plural') job.minutes.every(4) job.hours.on(5, 6) job.day.on(4) job.months.on(2) self.assertEqual(str(job), '*/4 5,6 4 2 * plural') def test_14_valid(self): """Valid and Invalid""" job = self.crontab.new(command='valid') job.minute.every(2) job.valid = False with self.assertRaises(ValueError): str(job) # Disabled jobs still work job.enabled = False str(job) job.enabled = True with self.assertRaises(ValueError): self.crontab.render(errors=True) self.crontab.render(errors=False) self.assertFalse(job.enabled) job = self.crontab.new() self.assertFalse(job.is_valid()) job.set_command("now_valid") self.assertTrue(job.is_valid()) def test_15_slices(self): """Invalid Slices""" mon = CronSlices('* * * * *') with self.assertRaises(ValueError): CronSlices('* * * */15 *') with self.assertRaises(AssertionError): mon.setall(mon) def test_16_slice(self): """Single Slice""" dow = CronSlice({'name': 'M', 'max': 7, 'min': 0, 'enum': ['a']}, '*/6') self.assertEqual(repr(dow), '') self.assertEqual(repr(dow.parse_value('a')), 'a') with self.assertRaises(ValueError): dow.parse_value('b') self.assertEqual(dow.get_range()[0].render(), '*') with self.assertRaises(ValueError): dow.get_range('%') def test_17_slice_id(self): """Single slice by Id""" self.assertEqual(CronSlice(1).max, 23) def test_18_range_cmp(self): """Compare ranges""" dow = CronSlice({'max': 5, 'min': 0}) three = dow.get_range(2, 4)[0] self.assertGreater(three, 2) self.assertLess(three, 4) self.assertEqual(str(three), '2-4') def test_18_find(self): """Find a command and comments by name""" cmds = list(self.crontab.find_command('byweek')) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].comment, 'Comment One') cmds = list(self.crontab.find_command(re.compile(r'stc\w'))) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].command, 'firstcommand') cmds = list(self.crontab.find_command('Comment One')) self.assertEqual(len(cmds), 0) cmds = list(self.crontab.find_comment('Comment One')) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].command, 'byweek') cmds = list(self.crontab.find_comment(re.compile(r'om+en\w O'))) self.assertEqual(len(cmds), 1) self.assertEqual(cmds[0].comment, 'Comment One') cmds = list(self.crontab.find_comment(re.compile(r'stc\w'))) self.assertEqual(len(cmds), 0) def test_20_write(self): """Write CronTab to file""" self.crontab.write('output.tab') self.assertTrue(os.path.exists('output.tab')) os.unlink('output.tab') def test_21_multiuse(self): """Multiple Renderings""" cron = '# start of tab\n' for i in range(10): crontab = CronTab(tab=cron) p = list(crontab.new(command='multi%d' % i)) cron = str(crontab) crontab = CronTab(tab=cron) list(crontab.find_command('multi%d' % i))[0].delete() cron = str(crontab) self.assertEqual(str(crontab), '# start of tab\n') def test_22_min(self): """Minimum Field Values""" job = self.crontab.new(command='min') job.minute.on('<') job.hour.on('<') job.dom.on('<') job.month.on('<') self.assertEqual(str(job), '@yearly min') def test_23_max(self): """Maximum Field Values""" job = self.crontab.new(command='max') job.minute.on('>') job.hour.on('>') job.dom.on('>') job.month.on('>') self.assertEqual(str(job), '59 23 31 12 * max') def test_24_special_r(self): """Special formats are read and written""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials_enc.tab')) self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@midnight midnight\n@weekly weekly\n@reboot reboot\n""") self.assertEqual(len(list(tab)), 5) def test_24_special_d(self): """Removal All Specials""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) tab.remove_all() self.assertEqual(len(list(tab)), 0) def test_24_special_w(self): """Write Specials""" tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab')) self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@weekly weekly\n""") self.assertEqual(len(list(tab)), 3) def test_25_setall(self): """Set all values at once""" job = self.crontab.new(command='all') job.setall(1, '*/2', '2-4', '>', 'SUN') self.assertEqual(str(job), '1 */2 2-4 12 SUN all') job.setall('*/2') self.assertEqual(str(job), '*/2 * * * * all') job.setall('1 */2 2-4 12 SUN') self.assertEqual(str(job), '1 */2 2-4 12 SUN all') job.setall(['*']) self.assertEqual(str(job), '* * * * * all') def test_26_setall_obj(self): """Copy all values""" job = self.crontab.new(command='all') job2 = self.crontab.new(command='ignore') job2.setall("1 */2 2-4 12 SUN") job.setall(job2) self.assertEqual(str(job), '1 */2 2-4 12 SUN all') job2.setall("2 */3 4-8 10 MON") job.setall(job2.slices) self.assertEqual(str(job), '2 */3 4-8 10 MON all') def test_27_commands(self): """Get all commands""" self.assertEqual(list(self.crontab.commands), [u'firstcommand', u'range', u'byweek', u'disabled', u'spaced', u'python /example_app/testing.py', u'rebooted', u'echo % escaped', u'cat %%' ]) def test_28_comments(self): """Get all comments""" self.assertEqual(list(self.crontab.comments), ['Comment One', 'Comment Two', 'monitorCron', 're-id', 'percent_one', 'percent_two']) def test_29_set_lines(self): """Set crontab lines directly""" cron = CronTab(tab='') cron.lines = [ str(self.crontab.lines[5]), ] self.assertEqual(str(cron), '\n*/30 * * * * firstcommand\n') with self.assertRaises(AttributeError): cron.crons = ['A'] if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(InteractionTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_log.py000066400000000000000000000127641524461074600255530ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) YEAR Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test the cron log extention with a test syslog example data file. """ import os import sys from datetime import datetime, date import unittest from crontab import CronTab from cronlog import CronLog, LogReader TEST_DIR = os.path.dirname(__file__) INITAL_TAB = """ * * * * * userscript &> /dev/null * * * * * rootscript &> /dev/null * * * * * shadowscript &> /dev/null """ ROOT_PIDS = ['16592', '16574', '16552', '16522', '16514', '16489'] SHAD_PIDS = ['16551', '16497'] USER_PIDS = ['16591', '16588', '16573', '16569', '16554', '16539', '16523',\ '16519','16513','16496','16490'] YEAR = date.today().year ROOT_DATES = [ datetime(YEAR, 4, 4, 21, 34, 1), datetime(YEAR, 4, 4, 21, 32, 1), datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 28, 1), datetime(YEAR, 4, 4, 21, 26, 1), datetime(YEAR, 4, 4, 21, 24, 1), ] SHAD_DATES = [ datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 25, 1), ] USER_DATES = [ datetime(YEAR, 4, 4, 21, 34, 1), datetime(YEAR, 4, 4, 21, 33, 2), datetime(YEAR, 4, 4, 21, 32, 1), datetime(YEAR, 4, 4, 21, 31, 1), datetime(YEAR, 4, 4, 21, 30, 1), datetime(YEAR, 4, 4, 21, 29, 1), datetime(YEAR, 4, 4, 21, 28, 1), datetime(YEAR, 4, 4, 21, 27, 1), datetime(YEAR, 4, 4, 21, 26, 1), datetime(YEAR, 4, 4, 21, 25, 1), datetime(YEAR, 4, 4, 21, 24, 1), ] LONG_LINE = 'A really long line which can test log lines ability to put two bits together' READ_LINE = [ 'The End', 'Sickem', '2', '9', 'First Line', LONG_LINE ] class BasicTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.log = os.path.join(TEST_DIR, 'data', 'test.log') self.crontab = CronTab(tab=INITAL_TAB, log=self.log) def test_00_logreader(self): """Log Reader""" lines = READ_LINE[:] reader = LogReader(os.path.join(TEST_DIR, 'data', 'basic.log'), mass=50) for line in reader: self.assertEqual(line.strip(), lines.pop(0)) def test_01_cronreader(self): """Cron Log Lines""" with self.crontab.log as log: lines = list(log.readlines()) self.assertEqual(len(lines), 32) self.assertEqual(lines[0][1], "Apr 4 21:34:01 servername CRON[16592]" ": (root) CMD (rootscript &> /dev/null)") self.assertEqual(lines[15][1], "Apr 4 21:28:31 servername NOTCRON" ": that these are ignored") self.assertEqual(lines[-1][1], "Apr 4 21:24:01 servername CRON[16490]" ": (user) CMD (userscript &> /dev/null)") def test_01_iter(self): """Iterate directly over log""" with self.crontab.log as log: self.assertEqual(len(list(log)), 8) def test_02_cronlog(self): """Cron Log Items""" entries = list(CronLog(os.path.join(TEST_DIR, 'data', 'test.log'))) self.assertEqual(len(entries), 19) self.assertEqual(entries[0]['pid'], "16592") self.assertEqual(entries[3]['pid'], "16574") self.assertEqual(entries[-1]['pid'], "16490") def test_03_crontab(self): """Cron Tab Items""" entries = list(self.crontab.log) self.assertEqual(len(entries), 8) self.assertEqual(entries[0]['pid'], "16592") self.assertEqual(entries[3]['pid'], "16551") self.assertEqual(entries[-1]['pid'], "16489") def test_04_root(self): """Cron Job Items""" pids, dates = ROOT_PIDS[:], ROOT_DATES[:] job = list(self.crontab.find_command('rootscript'))[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def tst_05_shadow(self): """Seperate Job Items""" pids, dates = SHAD_PIDS[:], SHAD_DATES[:] job = self.crontab.find_command('shadowscript')[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def tst_06_user(self): """Seperate User Crontab""" pids, dates = USER_PIDS[:], User_DATES[:] self.crontab.user = 'user' job = self.crontab.find_command('userscript')[0] for log in job.log: self.assertEqual(log['pid'], pids.pop(0)) self.assertEqual(log['date'], dates.pop(0)) self.assertEqual(pids, []) def test_08_readerror(self): """Cron Log Error""" self.crontab.log.pipe = False with self.assertRaises(IOError): list(self.crontab.log.readlines()) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( BasicTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_range.py000066400000000000000000000120451524461074600260560ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab ranges. """ import unittest from crontab import CronTab, CronSlice from .utils import LoggingMixin class RangeTestCase(LoggingMixin, unittest.TestCase): """Test basic functionality of crontab.""" log_name = 'crontab' def setUp(self): super(RangeTestCase, self).setUp() self.crontab = CronTab(tab="") def test_01_atevery(self): """At Every""" tab = CronTab(tab=""" * * * * * command 61 * * * * command * 25 * * * command * * 32 * * command * * * 13 * command * * * * 8 command """) self.assertEqual(len(tab), 1) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") def test_02_withinevery(self): """Within Every""" tab = CronTab(tab=""" * * * * * command 1-61 * * * * command * 1-25 * * * command * * 1-32 * * command * * * 1-13 * command * * * * 1-8 command """) self.assertEqual(len(tab), 1) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") def test_03_outevery(self): """Out of Every""" tab = CronTab(tab=""" * * * * * command */61 * * * * command * */25 * * * command * * */32 * * command * * * */13 * command * * * * */8 command """) self.assertEqual(len(tab), 1) self.assertLog("error", "'61', not in 0-59 for Minutes") self.assertLog("error", "'25', not in 0-23 for Hours") self.assertLog("error", "'32', not in 1-31 for Day of Month") self.assertLog("error", "'13', not in 1-12 for Month") self.assertLog("error", "'8', not in 0-6 for Day of Week") def test_03_inevery(self): """Inside of Every""" tab = CronTab(tab=""" * * * * * command */59 * * * * command * */23 * * * command * * */30 * * command * * * */11 * command * * * * */7 command """) self.assertEqual(len(tab), 6, str(tab)) def test_04_zero_seq(self): """Zero divisor in range""" tab = CronTab(tab=""" */0 * * * * command """) self.assertEqual(len(tab), 0) self.assertLog("error", "Sequence can not be divided by zero or max") def test_14_invalid_range(self): """No numerator in range""" tab = CronTab(tab="/10 * * * * command") self.assertEqual(len(tab), 0) with self.assertRaises(ValueError): tab.render(errors=True) self.assertEqual(str(tab), "# DISABLED LINE\n# /10 * * * * command\n") self.assertLog('error', u"No enumeration for Minutes: '/10'") def test_05_sunday(self): """Test all possible day of week combinations""" for (a, b) in (\ ("7", "0"), ("5-7", "0,5-6"), ("1-7", "*"), ("*/7", "0"),\ ("0-6", "*"), ("2-7", "0,2-6"), ("1-5", "1-5"), ("0-5", "0-5")): v = str(CronSlice(4, a)) self.assertEqual(v, b, "%s != %s, from %s" % (v, b, a)) def test_06_backwards(self): """Test backwards ranges for error""" tab = CronTab(tab="* * * * 3-1 command") self.assertEqual(str(tab), "# DISABLED LINE\n# * * * * 3-1 command\n") self.assertLog("error", "Bad range '3-1'") def test_07_random_range(self): """Test random ranges""" tab = CronTab(tab="~ 4~5 ~/2 1~1,2~3 * command") self.assertEqual(str(tab), "~ 4~5 ~/2 1,2~3 * command\n") job = tab[0] job.hour.random_during(1, 4) self.assertEqual(str(tab), "~ 1~4 ~/2 1,2~3 * command\n") job.hour.random_during(0, 23) self.assertEqual(str(tab), "~ ~ ~/2 1,2~3 * command\n") if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(RangeTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_removal.py000066400000000000000000000120001524461074600264160ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2013 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test cron item removal """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab START_TAB = """ 3 * * * * command1 # CommentID C 2 * * * * command2 # CommentID AAB 1 * * * * command3 # CommentID B3 """ class RemovalTestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.filenames = [] self.crontab = CronTab(tab=START_TAB.strip()) def test_01_remove(self): """Remove Item""" self.assertEqual(len(self.crontab), 3) self.crontab.remove(self.crontab.crons[0]) self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 69) def test_02_remove_all(self): """Remove All""" self.crontab.remove_all() self.assertEqual(len(self.crontab), 0) self.assertEqual(str(self.crontab), '') def test_03_remove_cmd(self): """Remove all with Command""" self.crontab.remove_all(command='command2') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) self.crontab.remove_all(command='command3') self.assertEqual(len(self.crontab), 1) self.assertEqual(len(self.crontab.render()), 33) def test_04_remove_id(self): """Remove all with Comment/ID""" self.crontab.remove_all(comment='CommentID B3') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 68) def test_05_remove_date(self): """Remove all with Time Code""" self.crontab.remove_all(time='2 * * * *') self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) def test_05_remove_all_error(self): """Remove all with old arg""" with self.assertRaises(AttributeError): self.crontab.remove_all('command') def test_06_removal_of_none(self): """Remove all respects None as a possible value""" self.crontab[1].set_comment(None) self.crontab.remove_all(comment=None) self.assertEqual(len(self.crontab), 2) self.assertEqual(len(self.crontab.render()), 67) def test_07_removal_in_loop(self): """Remove items in a loop""" for job in self.crontab: self.crontab.remove(job) self.assertEqual(len(self.crontab), 0) def test_08_removal_write_loop(self): """Remove items in a loop and save each time""" filename = self.get_new_file('removal') crontab = CronTab() for i in range(10): crontab.new('test', comment=str(i)) crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertEqual(len(crontab), 1) self.assertNotEqual(str(crontab), '') crontab.remove_all(comment=str(i)) crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertEqual(len(crontab), 0) self.assertEqual(str(crontab), '') def test_09_removal_during_iter(self): crontab = CronTab() for x in range(0, 5, 1): job = crontab.new(command="cmd", comment="SAME_ID") job.setall("%d * * * *" % (x + 1)) for item in crontab.find_comment("SAME_ID"): crontab.remove(item) self.assertEqual(len(crontab), 0) def test_11_remove_generator(self): """Remove jobs from the find generator""" tabs = self.crontab.find_command('command2') self.crontab.remove(tabs) self.assertEqual(len(self.crontab), 2) def test_12_remove_nonsense(self): """Fail to remove bad type""" self.assertRaises(TypeError, self.crontab.remove, 5) self.assertRaises(TypeError, self.crontab.remove, "foo") def get_new_file(self, name): """Gets a filename and records it for deletion""" this_dir = os.path.dirname(__file__) filename = os.path.join(this_dir, 'data', 'spool', name) self.filenames.append(filename) return filename def tearDown(self): for filename in self.filenames: if os.path.isfile(filename): os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( RemovalTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_scheduler.py000066400000000000000000000067171524461074600267510ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2016 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test any internal scheduling """ import os import sys import datetime import unittest import crontab import logging import string import random try: from StringIO import StringIO except ImportError: from io import StringIO logger = logging.getLogger() logger.level = logging.WARNING TEST_DIR = os.path.dirname(__file__) COMMAND = os.path.join(TEST_DIR, 'data', 'crontest ') class SchedulerTestCase(unittest.TestCase): """Test scheduling functions of CronTab.""" def setUp(self): self.tab = crontab.CronTab() try: import croniter except ImportError: self.skipTest("Croniter not installed") self.handlers = [] self.log = crontab.LOG self.log.setLevel(logging.DEBUG) self.log.propagate = False for handler in self.log.handlers: self.handlers.append(handler) self.log.removeHandler(handler) self.err = StringIO() self.handler = logging.StreamHandler(self.err) self.log.addHandler(self.handler) def tearDown(self): logger.removeHandler(self.handler) for handler in self.handlers: logger.addHandler(handler) #self.err.close() def assertLog(self, text): self.handler.flush() self.assertEqual(self.err.getvalue().strip(), text) def assertSchedule(self, slices, count, result): uid = random.choice(string.ascii_letters) self.tab.new(command=COMMAND + '-h ' + uid).setall(slices) ret = list(self.tab.run_scheduler(count, cadence=0.01, warp=True)) self.assertEqual(len(ret), result) if count > 0: self.assertEqual(ret[0], '-h|' + uid) def test_01_run(self): """Run the command""" self.tab.env['SHELL'] = crontab.SHELL ret = self.tab.new(command=COMMAND+'-h A').run() self.assertEqual(ret, '-h|A') def test_02_run_error(self): """Run with errors""" ret = self.tab.new(command=COMMAND+'-e B').run() self.assertEqual(ret, '') self.assertLog('-e|B') def test_03_schedule(self): """Simple Schedule""" self.assertSchedule("* * * * *", 5, 4) def test_04_schedule_ten(self): """Every Ten Minutes""" # If on the 10 minute mark, two runs are expected exact = (datetime.datetime.now().minute % 10) == 0 self.assertSchedule("*/10 * * * *", 10, 1 + exact) def test_05_env_passed(self): """Environment is passed in""" self.tab.env['CR_VAR'] = 'BABARIAN' ret = self.tab.new(command=COMMAND+' -ev').run() self.assertEqual(ret, 'BABARIAN') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(SchedulerTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_scheduler_async.py000066400000000000000000000031001524461074600301250ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2025 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 async scheduling """ import os import sys import asyncio import datetime import crontab import string import random import pytest TEST_DIR = os.path.dirname(__file__) COMMAND = os.path.join(TEST_DIR, 'data', 'crontest ') try: import pytest_asyncio @pytest.mark.asyncio async def test_run_async(): exact = (datetime.datetime.now().minute % 10) == 0 slices = "*/10 * * * *" count = 10 result = 1 + exact tab = crontab.CronTab() uid = random.choice(string.ascii_letters) tab.new(command=COMMAND + '-h ' + uid).setall(slices) proc = 0 async for payload in tab.run_scheduler_async(count, cadence=0.01, warp=True): proc += 1 assert payload == '-h|' + uid assert proc == result except ImportError: def test_run_async(): # skipTest("pytest-asyncio not installed") pass python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_system_cron.py000066400000000000000000000100541524461074600273250ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2015 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ System cron is prefixed with the username the process should run under. """ import os import sys sys.path.insert(0, '../') import unittest from crontab import CronTab TEST_FILE = os.path.join(os.path.dirname(__file__), 'data', 'crontab') INITIAL_TAB = """ */30 * * * * palin one_cross_each */30 * * * * matias\ttab_user_error """ class SystemCronTestCase(unittest.TestCase): """Test vixie cron user addition.""" def setUp(self): self.crontab = CronTab(tab=INITIAL_TAB, user=False) def test_00_repr(self): """System crontab repr""" self.assertEqual(repr(self.crontab), "") def test_01_read(self): """Read existing command""" job = self.crontab[0] self.assertEqual(job.user, 'palin') self.assertEqual(job.command, 'one_cross_each') job = self.crontab[1] self.assertEqual(job.user, 'matias') self.assertEqual(job.command, 'tab_user_error') self.assertEqual(len(self.crontab), 2) def test_02_new(self): """Create a new job""" job = self.crontab.new(command='release_brian', user='pontus') self.assertEqual(job.user, 'pontus') self.assertEqual(job.command, 'release_brian') self.assertEqual(str(self.crontab), INITIAL_TAB.replace('\t', ' ') + """ * * * * * pontus release_brian """) def test_03_new_tab(self): """Create a new system crontab""" tab = CronTab(user=False) tab.env['SHELL'] = '/usr/bin/roman' job = tab.new(command='release_bwian', user='pontus') self.assertEqual(str(tab), """SHELL=/usr/bin/roman * * * * * pontus release_bwian """) def test_04_failure(self): """Fail when no user""" with self.assertRaises(ValueError): self.crontab.new(command='im_brian') cron = self.crontab.new(user='user', command='no_im_brian') cron.user = None with self.assertRaises(ValueError): cron.render() def test_05_remove(self): """Remove the user flag""" self.crontab._user = None self.assertEqual(str(self.crontab), """ */30 * * * * one_cross_each */30 * * * * tab_user_error """) self.crontab.new(command='now_go_away') def test_06_comments(self): """Comment with six parts parses successfully""" crontab = CronTab(user=False, tab=""" #a system_comment that has six parts_will_fail_to_parse """) def test_07_recreation(self): """Input doesn't change on save""" crontab = CronTab(user=False, tab="* * * * * user command") self.assertEqual(str(crontab), "* * * * * user command\n") crontab = CronTab(user=False, tab="* * * * * user command\n") self.assertEqual(str(crontab), "* * * * * user command\n") def test_09_resaving(self): """Cycle rendering to show no changes""" for i in range(10): self.crontab = CronTab(tab=str(self.crontab)) self.assertEqual(INITIAL_TAB.replace('\t', ' '), str(self.crontab)) def test_10_system_file(self): """Load system crontab from a file""" crontab = CronTab(user=False, tabfile=TEST_FILE) self.assertEqual(repr(crontab), "" % TEST_FILE) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest(SystemCronTestCase) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_usage.py000066400000000000000000000240271524461074600260710ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2012 Jay Sigbrandt # Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab usage. """ import os import sys import unittest import crontab from datetime import date, time, datetime, timedelta crontab.LOG.setLevel(crontab.logging.ERROR) TEST_DIR = os.path.dirname(__file__) class DummyStdout(object): def write(self, text): pass BASIC = '@hourly firstcommand\n\n' USER = '\n*/4 * * * * user_command # user_comment\n\n\n' crontab.CRON_COMMAND = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest')) def flush(): pass class Attribute(object): def __init__(self, obj, attr, value): self.obj = obj self.attr = attr self.value = value def __enter__(self, *args, **kw): if hasattr(self.obj, self.attr): self.previous = getattr(self.obj, self.attr) setattr(self.obj, self.attr, self.value) def __exit__(self, *args, **kw): if hasattr(self, 'previous'): setattr(self.obj, self.attr, self.previous) else: delattr(self.obj, self.attr) class UseTestCase(unittest.TestCase): """Test use documentation in crontab.""" def setUp(self): self.filenames = [] def test_01_empty(self): """Open system crontab""" cron = crontab.CronTab() self.assertEqual(cron.render(), "") self.assertEqual(cron.__str__(), "") self.assertEqual(repr(cron), "") def test_02_user(self): """Open a user's crontab""" cron = crontab.CronTab(user='basic') self.assertEqual(cron.render(), BASIC) self.assertEqual(repr(cron), "") def test_02_user_bad_command(self): # Test bad cron command cron = crontab.CronTab(user='basic') cron.cron_command = 'wibble/no/no/no/yes' with self.assertRaises(IOError): cron.read() with self.assertRaises(IOError): cron.write() def test_02_user_self(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, {}) def test_03_usage(self): """Dont modify crontab""" cron = crontab.CronTab(tab='') sys.stdout = DummyStdout() sys.stdout.flush = flush try: exec(crontab.__doc__) except ImportError: pass sys.stdout = sys.__stdout__ self.assertEqual(cron.render(), '') def test_04_username(self): """Username is True""" cron = crontab.CronTab(user=True) self.assertNotEqual(cron.user, True) self.assertEqual(cron.render(), USER) self.assertEqual(repr(cron), "") def test_05_nouser(self): """Username doesn't exist""" cron = crontab.CronTab(user='nouser') self.assertEqual(cron.render(), '') def test_06_touser(self): """Write to user API""" cron = crontab.CronTab(tab=USER) self.assertEqual(repr(cron), "") cron.write_to_user('bob') filename = os.path.join(TEST_DIR, 'data', 'spool', 'bob') self.filenames.append(filename) self.assertTrue(os.path.exists(filename)) self.assertEqual(repr(cron), "") def test_06_touser_fail(self): """Write to user failed""" cron = crontab.CronTab(tab=USER) self.assertRaises(IOError, cron.write_to_user, 'john') filename = os.path.join(TEST_DIR, 'data', 'spool', 'john') if os.path.exists(filename): os.unlink(filename) self.assertTrue(filename) def test_07_ioerror_read(self): """No filename ioerror""" with self.assertRaises(IOError): cron = crontab.CronTab(user='error') cron.read() def test_07_ioerror_write(self): """User not specified, nowhere to write to""" cron = crontab.CronTab() with self.assertRaises(IOError): cron.write() def test_08_cronitem(self): """CronItem Standalone""" item = crontab.CronItem.from_line('noline') self.assertTrue(item.is_enabled()) with self.assertRaises(UnboundLocalError): item.delete() item.set_command('nothing') self.assertEqual(item.render(), '* * * * * nothing') def test_10_time_object(self): """Set slices using time object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(time(1, 2)) self.assertEqual(str(item.slices), '2 1 * * *') self.assertTrue(item.is_valid()) item.setall(time(0, 30, 0, 0)) self.assertEqual(str(item.slices), '30 0 * * *') self.assertTrue(item.is_valid()) self.assertEqual(str(item), '30 0 * * * cmd') def test_11_date_object(self): """Set slices using date object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(date(2010, 6, 7)) self.assertEqual(str(item.slices), '0 0 7 6 *') self.assertTrue(item.is_valid()) def test_12_datetime_object(self): """Set slices using datetime object""" item = crontab.CronItem(command='cmd') self.assertEqual(str(item.slices), '* * * * *') item.setall(datetime(2009, 8, 9, 3, 4)) self.assertTrue(item.is_valid()) self.assertEqual(str(item.slices), '4 3 9 8 *') def test_20_slice_validation(self): """CronSlices class and objects can validate""" CronSlices = crontab.CronSlices self.assertTrue(CronSlices('* * * * *').is_valid()) self.assertTrue(CronSlices.is_valid('* * * * *')) self.assertTrue(CronSlices.is_valid('*/2 * * * *')) self.assertTrue(CronSlices.is_valid('* 1,2 * * *')) self.assertTrue(CronSlices.is_valid('* * 1-5 * *')) self.assertTrue(CronSlices.is_valid('* * * * MON-WED')) self.assertTrue(CronSlices.is_valid('@reboot')) sliced = CronSlices('* * * * *') sliced[0].parts = [300] self.assertEqual(str(sliced), '300 * * * *') self.assertFalse(sliced.is_valid()) self.assertFalse(CronSlices.is_valid('P')) self.assertFalse(CronSlices.is_valid('*/61 * * * *')) self.assertFalse(CronSlices.is_valid('* 1,300 * * *')) self.assertFalse(CronSlices.is_valid('* * 50-1 * *')) self.assertFalse(CronSlices.is_valid('* * * * FRO-TOO')) self.assertFalse(CronSlices.is_valid('@retool')) self.assertRaises(ValueError, CronSlices._parse_value, None) def test_21_slice_special(self): """Rendering can be done without specials""" cronitem = crontab.CronItem('true') cronitem.setall('0 0 * * *') crontab.SPECIALS_CONVERSION = True self.assertEqual(cronitem.render(), '@daily true') crontab.SPECIALS_CONVERSION = False self.assertEqual(cronitem.render(), '0 0 * * * true') crontab.SPECIALS_CONVERSION = None self.assertEqual(cronitem.render(), '0 0 * * * true') def test_22_slice_special_convert(self): """Render of specials global control""" cronitem = crontab.CronItem('true') cronitem.setall('@daily') crontab.SPECIALS_CONVERSION = True self.assertEqual(cronitem.render(), '@daily true') crontab.SPECIALS_CONVERSION = False self.assertEqual(cronitem.render(), '0 0 * * * true') crontab.SPECIALS_CONVERSION = None self.assertEqual(cronitem.render(), '@daily true') def test_25_process(self): """Test opening pipes""" from crontab import Process, CRON_COMMAND process = Process(CRON_COMMAND, h=None, a='one', abc='two').run() self.assertEqual(int(process), 0) self.assertEqual(repr(process)[:8], "Process(") self.assertEqual(process.stderr, '') self.assertEqual(process.stdout, '--abc=two|-a|-h|one\n') def test_07_zero_padding(self): """Can we get zero padded output""" cron = crontab.CronTab(tab="02 3-5 2,4 */2 01 cmd") self.assertEqual(str(cron), '2 3-5 2,4 */2 1 cmd\n') with Attribute(crontab, 'ZERO_PAD', True): self.assertEqual(str(cron), '02 03-05 02,04 */2 01 cmd\n') def test_08_reset_after_daily(self): """Can jobs be rescheduled after midnight""" cron = crontab.CronTab(tab="@daily cmd") job = cron[0] self.assertEqual(str(job), '@daily cmd') job.minute.on(5) job.hour.on(1) self.assertEqual(str(job), '5 1 * * * cmd') def test_09_pre_comment(self): """Test use of pre_comments""" text = """ # Unattached comment 5 * * * 6 some_command # Attached comment */5 * * * * another_command */5 * * * * my_command # Other comment """ tab = crontab.CronTab(tab=text) self.assertEqual(tab[0].comment, "") self.assertEqual(tab[1].comment, "Attached comment") self.assertEqual(tab[2].comment, "Other comment") self.assertEqual(tab.render(), text) def tearDown(self): for filename in self.filenames: if os.path.exists(filename): os.unlink(filename) if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( UseTestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/test_utf8.py000066400000000000000000000071341524461074600256530ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2014 Martin Owens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. # """ Test crontab use of UTF-8 filenames and strings """ import os import sys import locale import unittest from crontab import CronTab TEST_DIR = os.path.dirname(__file__) content = """ */4 * * * * ůțƒ_command # ůțƒ_comment """ filename = os.path.join(TEST_DIR, 'data', 'output-ůțƒ-8.tab') class Utf8TestCase(unittest.TestCase): """Test basic functionality of crontab.""" def setUp(self): self.crontab = CronTab(tab=content) def test_01_input(self): """Read UTF-8 contents""" self.assertTrue(self.crontab) def test_02_write(self): """Write/Read UTF-8 Filename""" self.assertEqual(locale.getpreferredencoding().lower(), 'utf-8') self.crontab.write(filename) crontab = CronTab(tabfile=filename) self.assertTrue(crontab) with open(filename, "r") as fhl: self.assertEqual(content, fhl.read()) os.unlink(filename) def test_04_command(self): """Read Command String""" self.assertEqual(self.crontab[0].command, u"ůțƒ_command") def test_05_comment(self): """Read Comment String""" self.assertEqual(self.crontab[0].comment, u'ůțƒ_comment') def test_06_unicode(self): """Write New via Unicode""" job = self.crontab.new(command=u"ůțƒ_command", comment=u'ůțƒ_comment') self.assertEqual(job.command, u"ůțƒ_command") self.assertEqual(job.comment, u"ůțƒ_comment") self.crontab.render() def test_07_utf8(self): """Write New via UTF-8""" job = self.crontab.new(command=b'\xc5\xaf\xc8\x9b\xc6\x92_command', comment=b'\xc5\xaf\xc8\x9b\xc6\x92_comment') self.assertEqual(self.crontab.render(), u""" */4 * * * * ůțƒ_command # ůțƒ_comment * * * * * ůțƒ_command # ůțƒ_comment """) self.assertEqual(type(job.command), str) self.assertEqual(type(job.comment), str) def test_08_utf8_str(self): """Test UTF8 (non unicode) strings""" self.crontab[0].command = '£12' self.crontab[0].comment = '𝗔𝗕𝗖𝗗' self.assertEqual(self.crontab.render(), u""" */4 * * * * £12 # 𝗔𝗕𝗖𝗗 """) def test_09_utf8_again(self): """Test Extra UTF8 input""" filename = os.path.join(TEST_DIR, 'data', 'utf_extra') with open(filename + '.in', 'w') as fhl: fhl.write('# 中文\n30 2 * * * source /etc/profile\n30 1 * * * source /etc/profile') try: cron = CronTab(tabfile=filename + '.in') finally: os.unlink(filename + '.in') cron.write(filename + '.out') if os.path.isfile(filename + '.out'): os.unlink(filename + '.out') if __name__ == '__main__': try: from test import test_support except ImportError: from test import support as test_support test_support.run_unittest( Utf8TestCase, ) python-crontab-v3.4.0-040c496686a6dd8739efee67024caf646e33623d/tests/utils.py000066400000000000000000000054171524461074600250700ustar00rootroot00000000000000# # 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])