소스 검색

HUE-8725 [core] Adding python-crontab lib for Celery Beat

Romain Rigaux 6 년 전
부모
커밋
c0f91d9e2b
35개의 변경된 파일5288개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      desktop/core/ext-py/python-crontab-2.3.6/AUTHORS
  2. 165 0
      desktop/core/ext-py/python-crontab-2.3.6/COPYING
  3. 10 0
      desktop/core/ext-py/python-crontab-2.3.6/MANIFEST.in
  4. 430 0
      desktop/core/ext-py/python-crontab-2.3.6/PKG-INFO
  5. 403 0
      desktop/core/ext-py/python-crontab-2.3.6/README.rst
  6. 123 0
      desktop/core/ext-py/python-crontab-2.3.6/cronlog.py
  7. 1346 0
      desktop/core/ext-py/python-crontab-2.3.6/crontab.py
  8. 145 0
      desktop/core/ext-py/python-crontab-2.3.6/crontabs.py
  9. 4 0
      desktop/core/ext-py/python-crontab-2.3.6/setup.cfg
  10. 79 0
      desktop/core/ext-py/python-crontab-2.3.6/setup.py
  11. 1 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/__init__.py
  12. 7 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/basic.log
  13. 4 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/crontab
  14. 50 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/crontest
  15. 3 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/specials.tab
  16. 5 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/specials_enc.tab
  17. 32 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/test.log
  18. 17 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/data/test.tab
  19. 120 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_compatibility.py
  20. 89 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_croniter.py
  21. 112 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_crontabs.py
  22. 66 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_description.py
  23. 121 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_enums.py
  24. 220 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_env.py
  25. 109 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_every.py
  26. 161 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_frequency.py
  27. 367 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_interaction.py
  28. 161 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_log.py
  29. 136 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_range.py
  30. 154 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_removal.py
  31. 113 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_scheduler.py
  32. 122 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_system_cron.py
  33. 215 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_usage.py
  34. 116 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/test_utf8.py
  35. 81 0
      desktop/core/ext-py/python-crontab-2.3.6/tests/utils.py

+ 1 - 0
desktop/core/ext-py/python-crontab-2.3.6/AUTHORS

@@ -0,0 +1 @@
+Martin Owens (doctormo@gmail.com)

+ 165 - 0
desktop/core/ext-py/python-crontab-2.3.6/COPYING

@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ 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.

+ 10 - 0
desktop/core/ext-py/python-crontab-2.3.6/MANIFEST.in

@@ -0,0 +1,10 @@
+include AUTHORS
+include COPYING
+include README.rst
+include MANIFEST.in
+include ChangeLog
+include setup.py
+include crontab.py
+include cronlog.py
+include tests/*.py
+include tests/data/*

+ 430 - 0
desktop/core/ext-py/python-crontab-2.3.6/PKG-INFO

@@ -0,0 +1,430 @@
+Metadata-Version: 2.1
+Name: python-crontab
+Version: 2.3.6
+Summary: Python Crontab API
+Home-page: https://gitlab.com/doctormo/python-crontab/
+Author: Martin Owens
+Author-email: doctormo@gmail.com
+License: LGPLv3
+Description: Python Crontab
+        --------------
+        
+        .. image:: https://gitlab.com/doctormo/python-crontab/raw/master/branding.svg
+        
+        .. image:: https://badge.fury.io/py/python-crontab.svg
+            :target: https://badge.fury.io/py/python-crontab
+        .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg
+            :target: https://gitlab.com/doctormo/python-crontab/raw/master/COPYING
+        
+        Bug Reports and Development
+        ===========================
+        
+        Please report any problems to the `GitLab issues tracker <https://gitlab.com/doctormo/python-crontab/issues>`_. Please use Git and push patches to the `GitLab project code hosting <https://gitlab.com/doctormo/python-crontab>`_.
+        
+        **Note:** If you get the error ``TypeError: __init__() takes exactly 2 arguments`` 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 <http://en.wikipedia.org/wiki/Cron#CRON_expression>`_
+        you will note that W, L, # and ? symbols are not supported as they are not
+        standard Linux or SystemV crontab format.
+        
+        +-------------+-----------+-----------------+-------------------+-------------+
+        |Field Name   |Mandatory  |Allowed Values   |Special Characters |Extra Values |
+        +=============+===========+=================+===================+=============+
+        |Minutes      |Yes        |0-59             |\* / , -           | < >         |
+        +-------------+-----------+-----------------+-------------------+-------------+
+        |Hours        |Yes        |0-23             |\* / , -           | < >         |
+        +-------------+-----------+-----------------+-------------------+-------------+
+        |Day of month |Yes        |1-31             |\* / , -           | < >         |
+        +-------------+-----------+-----------------+-------------------+-------------+
+        |Month        |Yes        |1-12 or JAN-DEC  |\* / , -           | < >         |
+        +-------------+-----------+-----------------+-------------------+-------------+
+        |Day of week  |Yes        |0-6 or SUN-SAT   |\* / , -           | < >         |
+        +-------------+-----------+-----------------+-------------------+-------------+
+        
+        Extra Values are '<' for minimum value, such as 0 for minutes or 1 for months.
+        And '>' for maximum value, such as 23 for hours or 12 for months.
+        
+        Supported special cases allow crontab lines to not use fields.
+        These are the supported aliases which are not available in SystemV mode:
+        
+        =========== ============
+        Case        Meaning    
+        =========== ============
+        @reboot     Every boot 
+        @hourly     0 * * * *  
+        @daily      0 0 * * *  
+        @weekly     0 0 * * 0  
+        @monthly    0 0 1 * *  
+        @yearly     0 0 1 1 *  
+        @annually   0 0 1 1 *  
+        @midnight   0 0 * * *  
+        =========== ============
+        
+        How to Use the Module
+        =====================
+        
+        **Note:** Several users have reported their new crontabs not saving automatically or that the module doesn't do anything. You **MUST** use write() if you want your edits to be saved out. See below for full details on the use of the write function.
+        
+        Getting access to a crontab can happen in five ways, three system methods that
+        will work only on Unix and require you to have the right permissions::
+        
+            from crontab import CronTab
+        
+            empty_cron    = CronTab()
+            my_user_cron  = CronTab(user=True)
+            users_cron    = CronTab(user='username')
+        
+        And two ways from non-system sources that will work on Windows too::
+        
+            file_cron = CronTab(tabfile='filename.tab')
+            mem_cron = CronTab(tab="""
+              * * * * * command
+            """)
+        
+        Special per-command user flag for vixie cron format (new in 1.9)::
+        
+            system_cron = CronTab(tabfile='/etc/crontab', user=False)
+            job = system_cron[0]
+            job.user != None
+            system_cron.new(command='new_command', user='root')
+        
+        Creating a new job is as simple as::
+        
+            job  = cron.new(command='/usr/bin/echo')
+        
+        And setting the job's time restrictions::
+        
+            job.minute.during(5,50).every(5)
+            job.hour.every(4)
+            job.day.on(4, 5, 6)
+        
+            job.dow.on('SUN')
+            job.dow.on('SUN', 'FRI')
+            job.month.during('APR', 'NOV')
+        
+        Each time restriction will clear the previous restriction::
+        
+            job.hour.every(10) # Set to * */10 * * *
+            job.hour.on(2)     # Set to * 2 * * *
+        
+        Appending restrictions is explicit::
+        
+            job.hour.every(10)  # Set to * */10 * * *
+            job.hour.also.on(2) # Set to * 2,*/10 * * *
+        
+        Setting all time slices at once::
+        
+            job.setall(2, 10, '2-4', '*/2', None)
+            job.setall('2 10 * * *')
+        
+        Setting the slice to a python date object::
+        
+            job.setall(time(10, 2))
+            job.setall(date(2000, 4, 2))
+            job.setall(datetime(2000, 4, 2, 10, 2))
+        
+        Run a jobs command. Running the job here will not effect it's
+        existing schedule with another crontab process::
+        
+            job_standard_output = job.run()
+        
+        Creating a job with a comment::
+        
+            job = cron.new(command='/foo/bar', comment='SomeID')
+        
+        Get the comment or command for a job::
+        
+            command = job.command
+            comment = job.comment
+        
+        Modify the comment or command on a job::
+        
+            job.set_command("new_script.sh")
+            job.set_comment("New ID or comment here")
+        
+        Disabled or Enable Job::
+        
+            job.enable()
+            job.enable(False)
+            False == job.is_enabled()
+        
+        Validity Check::
+        
+            True == 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 * * * *')
+        
+        
+        Environment Variables
+        =====================
+        
+        Some versions of vixie cron support variables outside of the command line.
+        Sometimes just update the envronment when commands are run, the Cronie fork
+        of vixie cron also supports CRON_TZ which looks like a regular variable but
+        actually changes the times the jobs are run at.
+        
+        Very old vixie crons don't support per-job variables, but most do.
+        
+        Iterate through cron level environment variables::
+        
+            for (name, value) in cron.env.items():
+                print name
+                print value
+        
+        Create new or update cron level environment variables::
+        
+            print cron.env['SHELL']
+            cron.env['SHELL'] = '/bin/bash'
+            print cron.env
+        
+        Each job can also have a list of environment variables::
+        
+            for job in cron:
+                job.env['NEW_VAR'] = 'A'
+                print job.env
+        
+        
+        Proceeding Unit Confusion
+        =========================
+        
+        It is sometimes logical to think that job.hour.every(2) will set all proceeding
+        units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling
+        only the hours units and the minute column is unaffected. The real result would
+        be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs.
+        
+        There is a special 'every' method on a job to clear the job's existing schedule
+        and replace it with a simple single unit::
+        
+            job.every(4).hours()  == '0 */4 * * *'
+            job.every().dom()     == '0 0 * * *'
+            job.every().month()   == '0 0 0 * *'
+            job.every(2).dows()   == '0 0 * * */2'
+        
+        This is a convenience method only, it does normal things with the existing api.
+        
+        Running the Scheduler
+        =====================
+        
+        The module is able to run a cron tab as a daemon as long as the optional
+        croniter module is installed; each process will block and errors will
+        be logged (new in 2.0).
+        
+        (note this functionality is new and not perfect, if you find bugs report them!)
+        
+        Running the scheduler::
+        
+            tab = CronTab(tabfile='MyScripts.tab')
+            for result in tab.run_scheduler():
+                print "This was printed to stdout by the process."
+        
+        Do not do this, it won't work because it returns generator function::
+        
+            tab.run_scheduler()
+        
+        Timeout and cadence can be changed for testing or error management::
+        
+            for result in tab.run_scheduler(timeout=600):
+                print "Will run jobs every 1 minutes for ten minutes from now()"
+        
+            for result in tab.run_scheduler(cadence=1, warp=True):
+                print "Will run jobs every 1 second, counting each second as 1 minute"
+        
+        Frequency Calculation
+        =====================
+        
+        Every job's schedule has a frequency. We can attempt to calculate the number
+        of times a job would execute in a give amount of time. We have three simple
+        methods::
+        
+            job.setall("1,2 1,2 * * *")
+            job.frequency_per_day() == 4
+        
+        The per year frequency method will tell you how many days a year the
+        job would execute::
+        
+            job.setall("* * 1,2 1,2 *")
+            job.frequency_per_year(year=2010) == 4
+        
+        These are combined to give the number of times a job will execute in any year::
+        
+            job.setall("1,2 1,2 1,2 1,2 *")
+            job.frequency(year=2010) == 16
+        
+        Frequency can be quickly checked using python built-in operators::
+        
+            job < "*/2 * * * *"
+            job > job2
+            job.slices == "*/5"
+        
+        Log Functionality
+        =================
+        
+        The log functionality will read a cron log backwards to find you the last run
+        instances of your crontab and cron jobs.
+        
+        The crontab will limit the returned entries to the user the crontab is for::
+        
+            cron = CronTab(user='root')
+        
+            for d in cron.log:
+                print d['pid'] + " - " + d['date']
+        
+        Each job can return a log iterator too, these are filtered so you can see when
+        the last execution was::
+        
+            for d in cron.find_command('echo')[0].log:
+                print d['pid'] + " - " + d['date']
+        
+        All System CronTabs Functionality
+        =================================
+        
+        The crontabs (note the plural) module can attempt to find all crontabs on the
+        system. This works well for Linux systems with known locations for cron files
+        and user spolls. It will even extract anacron jobs so you can get a picture
+        of all the jobs running on your system::
+        
+            from crontabs import CronTabs
+            
+            for cron in CronTabs():
+                print repr(cron)
+        
+        All jobs can be brought together to run various searches, all jobs are added
+        to a CronTab object which can be used as documented above::
+        
+           jobs = CronTabs().all.find_command('foo')
+        
+        Schedule Functionality
+        ======================
+        
+        If you have the croniter python module installed, you will have access to a
+        schedule on each job. For example if you want to know when a job will next run::
+        
+            schedule = job.schedule(date_from=datetime.now())
+        
+        This creates a schedule croniter based on the job from the time specified. The
+        default date_from is the current date/time if not specified. Next we can get
+        the datetime of the next job::
+        
+            datetime = schedule.get_next()
+        
+        Or the previous::
+        
+            datetime = schedule.get_prev()
+        
+        The get methods work in the same way as the default croniter, except that they
+        will return datetime objects by default instead of floats. If you want the
+        original functionality, pass float into the method when calling::
+        
+            datetime = schedule.get_current(float)
+        
+        If you don't have the croniter module installed, you'll get an ImportError when
+        you first try using the schedule function on your cron job object.
+        
+        Descriptor Functionality
+        ========================
+        
+        If you have the cron-descriptor module installed, you will be able to ask for a
+        translated string which describes the frequency of the job in the current
+        locale language. This should be mostly human readable.
+        
+        
+            print(job.description(use_24hour_time_format=True))
+        
+        See cron-descriptor for details of the supported languages and options.
+        
+        Extra Support
+        =============
+        
+         - Support for vixie cron with username addition with user flag
+         - Support for SunOS, AIX & HP with compatibility 'SystemV' mode.
+         - Python 3.5.2 and Python 2.7 tested, python 2.6 removed from support.
+         - Windows support works for non-system crontabs only.
+           ( see mem_cron and file_cron examples above for usage )
+        
+Platform: linux
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Development Status :: 6 - Mature
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Operating System :: POSIX :: SunOS/Solaris
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.5
+Provides: crontab
+Provides: crontabs
+Provides: cronlog
+Provides-Extra: cron-description
+Provides-Extra: cron-schedule

+ 403 - 0
desktop/core/ext-py/python-crontab-2.3.6/README.rst

@@ -0,0 +1,403 @@
+Python Crontab
+--------------
+
+.. image:: https://gitlab.com/doctormo/python-crontab/raw/master/branding.svg
+
+.. image:: https://badge.fury.io/py/python-crontab.svg
+    :target: https://badge.fury.io/py/python-crontab
+.. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg
+    :target: https://gitlab.com/doctormo/python-crontab/raw/master/COPYING
+
+Bug Reports and Development
+===========================
+
+Please report any problems to the `GitLab issues tracker <https://gitlab.com/doctormo/python-crontab/issues>`_. Please use Git and push patches to the `GitLab project code hosting <https://gitlab.com/doctormo/python-crontab>`_.
+
+**Note:** If you get the error ``TypeError: __init__() takes exactly 2 arguments`` 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 <http://en.wikipedia.org/wiki/Cron#CRON_expression>`_
+you will note that W, L, # and ? symbols are not supported as they are not
+standard Linux or SystemV crontab format.
+
++-------------+-----------+-----------------+-------------------+-------------+
+|Field Name   |Mandatory  |Allowed Values   |Special Characters |Extra Values |
++=============+===========+=================+===================+=============+
+|Minutes      |Yes        |0-59             |\* / , -           | < >         |
++-------------+-----------+-----------------+-------------------+-------------+
+|Hours        |Yes        |0-23             |\* / , -           | < >         |
++-------------+-----------+-----------------+-------------------+-------------+
+|Day of month |Yes        |1-31             |\* / , -           | < >         |
++-------------+-----------+-----------------+-------------------+-------------+
+|Month        |Yes        |1-12 or JAN-DEC  |\* / , -           | < >         |
++-------------+-----------+-----------------+-------------------+-------------+
+|Day of week  |Yes        |0-6 or SUN-SAT   |\* / , -           | < >         |
++-------------+-----------+-----------------+-------------------+-------------+
+
+Extra Values are '<' for minimum value, such as 0 for minutes or 1 for months.
+And '>' for maximum value, such as 23 for hours or 12 for months.
+
+Supported special cases allow crontab lines to not use fields.
+These are the supported aliases which are not available in SystemV mode:
+
+=========== ============
+Case        Meaning    
+=========== ============
+@reboot     Every boot 
+@hourly     0 * * * *  
+@daily      0 0 * * *  
+@weekly     0 0 * * 0  
+@monthly    0 0 1 * *  
+@yearly     0 0 1 1 *  
+@annually   0 0 1 1 *  
+@midnight   0 0 * * *  
+=========== ============
+
+How to Use the Module
+=====================
+
+**Note:** Several users have reported their new crontabs not saving automatically or that the module doesn't do anything. You **MUST** use write() if you want your edits to be saved out. See below for full details on the use of the write function.
+
+Getting access to a crontab can happen in five ways, three system methods that
+will work only on Unix and require you to have the right permissions::
+
+    from crontab import CronTab
+
+    empty_cron    = CronTab()
+    my_user_cron  = CronTab(user=True)
+    users_cron    = CronTab(user='username')
+
+And two ways from non-system sources that will work on Windows too::
+
+    file_cron = CronTab(tabfile='filename.tab')
+    mem_cron = CronTab(tab="""
+      * * * * * command
+    """)
+
+Special per-command user flag for vixie cron format (new in 1.9)::
+
+    system_cron = CronTab(tabfile='/etc/crontab', user=False)
+    job = system_cron[0]
+    job.user != None
+    system_cron.new(command='new_command', user='root')
+
+Creating a new job is as simple as::
+
+    job  = cron.new(command='/usr/bin/echo')
+
+And setting the job's time restrictions::
+
+    job.minute.during(5,50).every(5)
+    job.hour.every(4)
+    job.day.on(4, 5, 6)
+
+    job.dow.on('SUN')
+    job.dow.on('SUN', 'FRI')
+    job.month.during('APR', 'NOV')
+
+Each time restriction will clear the previous restriction::
+
+    job.hour.every(10) # Set to * */10 * * *
+    job.hour.on(2)     # Set to * 2 * * *
+
+Appending restrictions is explicit::
+
+    job.hour.every(10)  # Set to * */10 * * *
+    job.hour.also.on(2) # Set to * 2,*/10 * * *
+
+Setting all time slices at once::
+
+    job.setall(2, 10, '2-4', '*/2', None)
+    job.setall('2 10 * * *')
+
+Setting the slice to a python date object::
+
+    job.setall(time(10, 2))
+    job.setall(date(2000, 4, 2))
+    job.setall(datetime(2000, 4, 2, 10, 2))
+
+Run a jobs command. Running the job here will not effect it's
+existing schedule with another crontab process::
+
+    job_standard_output = job.run()
+
+Creating a job with a comment::
+
+    job = cron.new(command='/foo/bar', comment='SomeID')
+
+Get the comment or command for a job::
+
+    command = job.command
+    comment = job.comment
+
+Modify the comment or command on a job::
+
+    job.set_command("new_script.sh")
+    job.set_comment("New ID or comment here")
+
+Disabled or Enable Job::
+
+    job.enable()
+    job.enable(False)
+    False == job.is_enabled()
+
+Validity Check::
+
+    True == 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 * * * *')
+
+
+Environment Variables
+=====================
+
+Some versions of vixie cron support variables outside of the command line.
+Sometimes just update the envronment when commands are run, the Cronie fork
+of vixie cron also supports CRON_TZ which looks like a regular variable but
+actually changes the times the jobs are run at.
+
+Very old vixie crons don't support per-job variables, but most do.
+
+Iterate through cron level environment variables::
+
+    for (name, value) in cron.env.items():
+        print name
+        print value
+
+Create new or update cron level environment variables::
+
+    print cron.env['SHELL']
+    cron.env['SHELL'] = '/bin/bash'
+    print cron.env
+
+Each job can also have a list of environment variables::
+
+    for job in cron:
+        job.env['NEW_VAR'] = 'A'
+        print job.env
+
+
+Proceeding Unit Confusion
+=========================
+
+It is sometimes logical to think that job.hour.every(2) will set all proceeding
+units to '0' and thus result in "0 \*/2 * * \*". Instead you are controlling
+only the hours units and the minute column is unaffected. The real result would
+be "\* \*/2 * * \*" and maybe unexpected to those unfamiliar with crontabs.
+
+There is a special 'every' method on a job to clear the job's existing schedule
+and replace it with a simple single unit::
+
+    job.every(4).hours()  == '0 */4 * * *'
+    job.every().dom()     == '0 0 * * *'
+    job.every().month()   == '0 0 0 * *'
+    job.every(2).dows()   == '0 0 * * */2'
+
+This is a convenience method only, it does normal things with the existing api.
+
+Running the Scheduler
+=====================
+
+The module is able to run a cron tab as a daemon as long as the optional
+croniter module is installed; each process will block and errors will
+be logged (new in 2.0).
+
+(note this functionality is new and not perfect, if you find bugs report them!)
+
+Running the scheduler::
+
+    tab = CronTab(tabfile='MyScripts.tab')
+    for result in tab.run_scheduler():
+        print "This was printed to stdout by the process."
+
+Do not do this, it won't work because it returns generator function::
+
+    tab.run_scheduler()
+
+Timeout and cadence can be changed for testing or error management::
+
+    for result in tab.run_scheduler(timeout=600):
+        print "Will run jobs every 1 minutes for ten minutes from now()"
+
+    for result in tab.run_scheduler(cadence=1, warp=True):
+        print "Will run jobs every 1 second, counting each second as 1 minute"
+
+Frequency Calculation
+=====================
+
+Every job's schedule has a frequency. We can attempt to calculate the number
+of times a job would execute in a give amount of time. We have three simple
+methods::
+
+    job.setall("1,2 1,2 * * *")
+    job.frequency_per_day() == 4
+
+The per year frequency method will tell you how many days a year the
+job would execute::
+
+    job.setall("* * 1,2 1,2 *")
+    job.frequency_per_year(year=2010) == 4
+
+These are combined to give the number of times a job will execute in any year::
+
+    job.setall("1,2 1,2 1,2 1,2 *")
+    job.frequency(year=2010) == 16
+
+Frequency can be quickly checked using python built-in operators::
+
+    job < "*/2 * * * *"
+    job > job2
+    job.slices == "*/5"
+
+Log Functionality
+=================
+
+The log functionality will read a cron log backwards to find you the last run
+instances of your crontab and cron jobs.
+
+The crontab will limit the returned entries to the user the crontab is for::
+
+    cron = CronTab(user='root')
+
+    for d in cron.log:
+        print d['pid'] + " - " + d['date']
+
+Each job can return a log iterator too, these are filtered so you can see when
+the last execution was::
+
+    for d in cron.find_command('echo')[0].log:
+        print d['pid'] + " - " + d['date']
+
+All System CronTabs Functionality
+=================================
+
+The crontabs (note the plural) module can attempt to find all crontabs on the
+system. This works well for Linux systems with known locations for cron files
+and user spolls. It will even extract anacron jobs so you can get a picture
+of all the jobs running on your system::
+
+    from crontabs import CronTabs
+    
+    for cron in CronTabs():
+        print repr(cron)
+
+All jobs can be brought together to run various searches, all jobs are added
+to a CronTab object which can be used as documented above::
+
+   jobs = CronTabs().all.find_command('foo')
+
+Schedule Functionality
+======================
+
+If you have the croniter python module installed, you will have access to a
+schedule on each job. For example if you want to know when a job will next run::
+
+    schedule = job.schedule(date_from=datetime.now())
+
+This creates a schedule croniter based on the job from the time specified. The
+default date_from is the current date/time if not specified. Next we can get
+the datetime of the next job::
+
+    datetime = schedule.get_next()
+
+Or the previous::
+
+    datetime = schedule.get_prev()
+
+The get methods work in the same way as the default croniter, except that they
+will return datetime objects by default instead of floats. If you want the
+original functionality, pass float into the method when calling::
+
+    datetime = schedule.get_current(float)
+
+If you don't have the croniter module installed, you'll get an ImportError when
+you first try using the schedule function on your cron job object.
+
+Descriptor Functionality
+========================
+
+If you have the cron-descriptor module installed, you will be able to ask for a
+translated string which describes the frequency of the job in the current
+locale language. This should be mostly human readable.
+
+
+    print(job.description(use_24hour_time_format=True))
+
+See cron-descriptor for details of the supported languages and options.
+
+Extra Support
+=============
+
+ - Support for vixie cron with username addition with user flag
+ - Support for SunOS, AIX & HP with compatibility 'SystemV' mode.
+ - Python 3.5.2 and Python 2.7 tested, python 2.6 removed from support.
+ - Windows support works for non-system crontabs only.
+   ( see mem_cron and file_cron examples above for usage )

+ 123 - 0
desktop/core/ext-py/python-crontab-2.3.6/cronlog.py

@@ -0,0 +1,123 @@
+#
+# Copyright 2013, Martin Owens <doctormo@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Access logs in known locations to find information about them.
+"""
+
+import os
+import re
+import codecs
+import platform
+
+PY3 = platform.python_version()[0] == '3'
+
+if PY3:
+     # pylint: disable=W0622
+     unicode = str
+     basestring = str
+
+from dateutil import parser as dateparse
+
+MATCHER = r'(?P<date>\w+ +\d+ +\d\d:\d\d:\d\d) (?P<host>\w+) ' + \
+        r'CRON\[(?P<pid>\d+)\]: \((?P<user>\w+)\) CMD \((?P<cmd>.*)\)'
+
+class LogReader(object):
+    """Opens a Log file, reading backwards and watching for changes"""
+    def __init__(self, filename, mass=4096):
+        self.filename = filename
+        self.mass = mass
+        self.size = -1
+        self.read = -1
+        self.pipe = None
+
+    def __enter__(self):
+        self.size = os.stat(self.filename)[6]
+        self.pipe = codecs.open(self.filename, 'r', encoding='utf-8')
+        return self
+
+    def __exit__(self, error_type, value, traceback):
+        self.pipe.close()
+
+    def __iter__(self):
+        if self.pipe is None:
+            with self as reader:
+                for (offset, line) in reader.readlines():
+                    yield line
+        else:
+            for (offset, line) in self.readlines():
+                yield line
+
+    def readlines(self, until=0):
+        """Iterator for reading lines from a file backwards"""
+        if not self.pipe or self.pipe.closed:
+            raise IOError("Can't readline, no opened file.")
+        # Always seek to the end of the file, this accounts for file updates
+        # that happen during our running process.
+        location = self.size
+        halfline = ''
+
+        while location > until:
+            location -= self.mass
+            mass = self.mass
+            if location < 0:
+                mass = self.mass + location
+                location = 0
+            self.pipe.seek(location)
+            line = self.pipe.read(mass) + halfline
+            data = line.split('\n')
+            if location != 0:
+                halfline = data.pop(0)
+            loc = location + mass
+            data.reverse()
+            for line in data:
+                if line.strip() == '':
+                    continue
+                yield (loc, line)
+                loc -= len(line)
+
+
+
+class CronLog(LogReader):
+    """Use the LogReader to make a Cron specific log reader"""
+    def __init__(self, filename='/var/log/syslog', user=None):
+        LogReader.__init__(self, filename)
+        self.user = user
+
+    def for_program(self, command):
+        """Return log entries for this specific command name"""
+        return ProgramLog(self, command)
+
+    def __iter__(self):
+        for line in super(CronLog, self).__iter__():
+            match = re.match(MATCHER, unicode(line))
+            datum = match and match.groupdict()
+            if datum and (not self.user or datum['user'] == self.user):
+                datum['date'] = dateparse.parse(datum['date'])
+                yield datum
+
+
+class ProgramLog(object):
+    """Specific log control for a single command/program"""
+    def __init__(self, log, command):
+        self.log = log
+        self.command = command
+
+    def __iter__(self):
+        for entry in self.log:
+            if entry['cmd'] == unicode(self.command):
+                yield entry
+

+ 1346 - 0
desktop/core/ext-py/python-crontab-2.3.6/crontab.py

@@ -0,0 +1,1346 @@
+#
+# Copyright 2017, Martin Owens <doctormo@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+# pylint: disable=logging-format-interpolation,too-many-lines
+"""
+from crontab import CronTab
+import sys
+
+# Create a new non-installed crontab
+cron = CronTab(tab='')
+job  = cron.new(command='/usr/bin/echo')
+
+job.minute.during(5,50).every(5)
+job.hour.every(4)
+
+job.dow.on('SUN')
+job.month.during('APR', 'JUN')
+job.month.also.during('OCT', 'DEC')
+
+job.every(2).days()
+job.setall(1, 12, None, None, None)
+
+job2 = cron.new(command='/foo/bar', comment='SomeID')
+job2.every_reboot()
+
+jobs = list(cron.find_command('bar'))
+job3 = jobs[0]
+job3.clear()
+job3.minute.every(1)
+
+sys.stdout.write(str(cron.render()))
+
+job3.enable(False)
+
+for job4 in cron.find_command('echo'):
+    sys.stdout.write(job4)
+
+for job5 in cron.find_comment('SomeID'):
+    sys.stdout.write(job5)
+
+for job6 in cron:
+    sys.stdout.write(job6)
+
+for job7 in cron:
+    job7.every(3).hours()
+    sys.stdout.write(job7)
+    job7.every().dow()
+
+cron.remove_all(command='/foo/bar')
+cron.remove_all(comment='This command')
+cron.remove_all(time='* * * * *')
+cron.remove_all()
+
+output = cron.render()
+
+cron.write()
+
+cron.write(filename='/tmp/output.txt')
+
+#cron.write_to_user(user=True)
+
+#cron.write_to_user(user='root')
+
+# Croniter Extentions allow you to ask for the scheduled job times, make
+# sure you have croniter installed, it's not a hard dependancy.
+
+job3.schedule().get_next()
+job3.schedule().get_prev()
+
+"""
+
+import os
+import re
+import shlex
+
+import types
+import codecs
+import logging
+import tempfile
+import platform
+import subprocess as sp
+
+from time import sleep
+from datetime import time, date, datetime, timedelta
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    # python 2.6 and below causes this error
+    try:
+        from ordereddict import OrderedDict
+    except ImportError:
+        raise ImportError("OrderedDict is required for python-crontab, you can"
+                          " install ordereddict 1.1 from pypi for python2.6")
+
+__pkgname__ = 'python-crontab'
+__version__ = '2.3.6'
+
+ITEMREX = re.compile(r'^\s*([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)\s+([^@#\s]+)'
+                     r'\s+([^@#\s]+)\s+([^\n]*?)(\s+#\s*([^\n]*)|$)')
+SPECREX = re.compile(r'^\s*@(\w+)\s([^#\n]*)(\s+#\s*([^\n]*)|$)')
+DEVNULL = ">/dev/null 2>&1"
+
+WEEK_ENUM = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
+
+MONTH_ENUM = [None, 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
+              'sep', 'oct', 'nov', 'dec']
+
+SPECIALS = {"reboot":   '@reboot',
+            "hourly":   '0 * * * *',
+            "daily":    '0 0 * * *',
+            "weekly":   '0 0 * * 0',
+            "monthly":  '0 0 1 * *',
+            "yearly":   '0 0 1 1 *',
+            "annually": '0 0 1 1 *',
+            "midnight": '0 0 * * *'}
+
+SPECIAL_IGNORE = ['midnight', 'annually']
+
+S_INFO = [
+    {'max': 59, 'min': 0, 'name': 'Minutes'},
+    {'max': 23, 'min': 0, 'name': 'Hours'},
+    {'max': 31, 'min': 1, 'name': 'Day of Month'},
+    {'max': 12, 'min': 1, 'name': 'Month', 'enum': MONTH_ENUM},
+    {'max': 6, 'min': 0, 'name': 'Day of Week', 'enum': WEEK_ENUM},
+]
+
+# Detect Python3 and which OS for temperments.
+PY3 = platform.python_version()[0] == '3'
+WINOS = platform.system() == 'Windows'
+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')
+
+CRONCMD = "/usr/bin/crontab"
+SHELL = os.environ.get('SHELL', '/bin/sh')
+# The shell won't actually work on windows here, but
+# it should be updated later in the below conditional.
+
+# pylint: disable=W0622,invalid-name,too-many-public-methods
+# pylint: disable=function-redefined,too-many-instance-attributes
+current_user = lambda: None
+if not WINOS:
+    import pwd
+    def current_user():
+        """Returns the username of the current user"""
+        return pwd.getpwuid(os.getuid())[0]
+
+if PY3:
+    unicode = str
+    basestring = str
+
+def open_pipe(cmd, *args, **flags):
+    """Runs a program and orders the arguments for compatability.
+
+    a. keyword args are flags and always appear /before/ arguments for bsd
+    """
+    cmd_args = tuple(shlex.split(cmd))
+    env = flags.pop('env', None)
+    for (key, value) in flags.items():
+        if len(key) == 1:
+            cmd_args += ("-%s" % key),
+            if value is not None:
+                cmd_args += unicode(value),
+        else:
+            cmd_args += ("--%s=%s" % (key, value)),
+    args = tuple(arg for arg in (cmd_args + tuple(args)) if arg)
+    return sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE, env=env)
+
+def _unicode(text):
+    """Convert to the best string format for this python version"""
+    if isinstance(text, str) and not PY3:
+        return unicode(text, 'utf-8')
+    if isinstance(text, bytes) and PY3:
+        return text.decode('utf-8')
+    return text
+
+
+class CronTab(object):
+    """
+    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.env = None
+        self._parked_env = OrderedDict()
+        # Protect windows users
+        self.root = not WINOS and os.getuid() == 0
+        # Storing user flag / username
+        self._user = user
+        # Load string or filename as inital crontab
+        self.intab = tab
+        self.read(tabfile)
+        self._log = log
+
+    @property
+    def log(self):
+        """Returns the CronLog object for this tab (user or root tab only)"""
+        from cronlog import CronLog
+        if self._log is None or isinstance(self._log, basestring):
+            self._log = CronLog(self._log, user=self.user or 'root')
+        return self._log
+
+    @property
+    def user(self):
+        """Return user's username of this crontab if applicable"""
+        if self._user is True:
+            return current_user()
+        return self._user
+
+    @property
+    def user_opt(self):
+        """Returns the user option for the crontab commandline"""
+        # Fedora and Mac require the current user to not specify
+        # But Ubuntu/Debian doesn't care. Be careful here.
+        if self._user and self._user is not True:
+            if self._user != current_user():
+                return {'u': self._user}
+        return {}
+
+    def __setattr__(self, name, value):
+        """Catch setting crons and lines directly"""
+        if name == 'lines' and value:
+            for line in value:
+                self.append(CronItem.from_line(line, cron=self), line, read=True)
+        elif name == 'crons' and value:
+            raise AttributeError("You can NOT set crons attribute directly")
+        else:
+            super(CronTab, self).__setattr__(name, value)
+
+    def read(self, filename=None):
+        """
+        Read in the crontab from the system into the object, called
+        automatically when listing or using the object. use for refresh.
+        """
+        self.crons = []
+        self.lines = []
+        self.env = OrderedVariableList()
+        lines = []
+
+        if self.intab is not None:
+            lines = self.intab.split('\n')
+
+        elif filename:
+            self.filen = filename
+            with codecs.open(filename, 'r', encoding='utf-8') as fhl:
+                lines = fhl.readlines()
+
+        elif self.user:
+            (out, err) = open_pipe(CRONCMD, l='', **self.user_opt).communicate()
+            if err and 'no crontab for' in unicode(err):
+                pass
+            elif err:
+                raise IOError("Read crontab %s: %s" % (self.user, err))
+            lines = out.decode('utf-8').split("\n")
+
+        self.lines = lines
+
+    def append(self, item, line='', read=False):
+        """Append a CronItem object to this CronTab"""
+        if item.is_valid():
+            item.env.update(self._parked_env)
+            self._parked_env = OrderedDict()
+            if read and not item.comment and self.lines and \
+              self.lines[-1] and self.lines[-1][0] == '#':
+                item.set_comment(self.lines.pop()[1:].strip())
+
+            self.crons.append(item)
+            self.lines.append(item)
+        elif '=' in line:
+            if ' ' not in line or line.index('=') < line.index(' '):
+                (name, value) = line.split('=', 1)
+                value = value.strip()
+                for quot in "\"'":
+                    if value[0] == quot and value[-1] == quot:
+                        value = value.strip(quot)
+                        break
+                self._parked_env[name.strip()] = value
+        else:
+            if not self.crons and self._parked_env:
+                self.env.update(self._parked_env)
+                self._parked_env = OrderedDict()
+            self.lines.append(line.replace('\n', ''))
+
+    def write(self, filename=None, user=None, errors=False):
+        """Write the crontab to it's source or a given filename."""
+        if filename:
+            self.filen = filename
+        elif user is not None:
+            self.filen = None
+            self.intab = None
+            self._user = user
+
+        # Add to either the crontab or the internal tab.
+        if self.intab is not None:
+            self.intab = self.render()
+            # And that's it if we never saved to a file
+            if not self.filen:
+                return
+
+        if self.filen:
+            fileh = open(self.filen, 'wb')
+        else:
+            filed, path = tempfile.mkstemp()
+            fileh = os.fdopen(filed, 'wb')
+
+        fileh.write(self.render(errors=errors).encode('utf-8'))
+        fileh.close()
+
+        if not self.filen:
+            # Add the entire crontab back to the user crontab
+            if self.user:
+                proc = open_pipe(CRONCMD, path, **self.user_opt)
+                # This could do with being cleaned up quite a bit
+                proc.wait()
+                proc.stdout.close()
+                proc.stderr.close()
+                os.unlink(path)
+            else:
+                os.unlink(path)
+                raise IOError("Please specify user or filename to write.")
+
+    def write_to_user(self, user=True):
+        """Write the crontab to a user (or root) instead of a file."""
+        return self.write(user=user)
+
+    def run_pending(self, **kwargs):
+        """Run all commands in this crontab if pending (generator)"""
+        for job in self:
+            ret = job.run_pending(**kwargs)
+            if ret not in [None, -1]:
+                yield ret
+
+    def run_scheduler(self, timeout=-1, **kwargs):
+        """Run the CronTab as an internal scheduler (generator)"""
+        count = 0
+        while count != timeout:
+            now = datetime.now()
+            if 'warp' in kwargs:
+                now += timedelta(seconds=count * 60)
+            for value in self.run_pending(now=now):
+                yield value
+
+            sleep(kwargs.get('cadence', 60))
+            count += 1
+
+    def render(self, errors=False):
+        """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, (unicode, str)):
+                if line.strip().startswith('#') or not line.strip():
+                    crons.append(line)
+                elif not errors:
+                    crons.append('# DISABLED LINE\n# ' + line)
+                else:
+                    raise ValueError("Invalid line: %s" % line)
+            elif isinstance(line, CronItem):
+                if not line.is_valid() and not errors:
+                    line.enabled = False
+                crons.append(unicode(line))
+
+        # Environment variables are attached to cron lines so order will
+        # always work no matter how you add lines in the middle of the stack.
+        result = unicode(self.env) + u'\n'.join(crons)
+        if result and result[-1] not in (u'\n', u'\r'):
+            result += u'\n'
+        return result
+
+    def new(self, command='', comment='', user=None):
+        """
+        Create a new cron with a command and comment.
+
+        Returns the new CronItem object.
+        """
+        if not user and self.user is False:
+            raise ValueError("User is required for system crontabs.")
+        item = CronItem(command, comment, user=user, cron=self)
+        self.append(item)
+        return item
+
+    def find_command(self, command):
+        """Return an iter of jobs matching any part of the command."""
+        for job in list(self.crons):
+            if isinstance(command, type(ITEMREX)):
+                if command.findall(job.command):
+                    yield job
+            elif command in job.command:
+                yield job
+
+    def find_comment(self, comment):
+        """Return an iter of jobs that match the comment field exactly."""
+        for job in list(self.crons):
+            if isinstance(comment, type(ITEMREX)):
+                if comment.findall(job.comment):
+                    yield job
+            elif comment == job.comment:
+                yield job
+
+    def find_time(self, *args):
+        """Return an iter of jobs that match this time pattern"""
+        for job in list(self.crons):
+            if job.slices == CronSlices(*args):
+                yield job
+
+    @property
+    def commands(self):
+        """Return a generator of all unqiue commands used in this crontab"""
+        returned = []
+        for cron in self.crons:
+            if cron.command not in returned:
+                yield cron.command
+                returned.append(cron.command)
+
+    @property
+    def comments(self):
+        """Return a generator of all unique comments/Id used in this crontab"""
+        returned = []
+        for cron in self.crons:
+            if cron.comment and cron.comment not in returned:
+                yield cron.comment
+                returned.append(cron.comment)
+
+    def remove_all(self, *args, **kwargs):
+        """Removes all crons using the stated command OR that have the
+        stated comment OR removes everything if no arguments specified.
+
+           command - Remove all with this command
+           comment - Remove all with this comment or ID
+           time    - Remove all with this time code
+        """
+        if args:
+            raise AttributeError("Invalid use: remove_all(command='cmd')")
+        if 'command' in kwargs:
+            return self.remove(*self.find_command(kwargs['command']))
+        elif 'comment' in kwargs:
+            return self.remove(*self.find_comment(kwargs['comment']))
+        elif '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
+            elif sibling == '':
+                self.lines.remove(sibling)
+            else:
+                break
+
+        self.crons.remove(item)
+        self.lines.remove(item)
+        return 1
+
+    def __repr__(self):
+        kind = 'System ' if self._user is False else ''
+        if self.filen:
+            return "<%sCronTab '%s'>" % (kind, self.filen)
+        elif self.user and not self.user_opt:
+            return "<My CronTab>"
+        elif self.user:
+            return "<User CronTab '%s'>" % self.user
+        return "<Unattached %sCronTab>" % kind
+
+    def __iter__(self):
+        """Return generator so we can track jobs after removal"""
+        for job in list(self.crons.__iter__()):
+            yield job
+
+    def __getitem__(self, i):
+        return self.crons[i]
+
+    def __unicode__(self):
+        return self.render()
+
+    def __len__(self):
+        return len(self.crons)
+
+    def __str__(self):
+        return self.render()
+
+
+class CronItem(object):
+    """
+    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, cron=None):
+        self.cron = cron
+        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.marker = None
+        self.pre_comment = False
+        self._log = None
+
+        # Initalise five cron slices using static info.
+        self.slices = CronSlices()
+
+        self.set_comment(comment)
+
+        if command:
+            self.set_command(command)
+            self.valid = True
+
+    @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, 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!")
+        else:
+            self.cron.remove(self)
+
+    def set_command(self, cmd):
+        """Set the command and filter as needed"""
+        self.command = cmd.strip()
+
+    def set_comment(self, cmt):
+        """Set the comment and don't filter"""
+        if cmt and cmt[:8] == 'Ansible:':
+            self.marker = 'Ansible'
+            self.pre_comment = True
+            self.comment = cmt[8:].lstrip()
+        else:
+            self.comment = cmt
+
+    def parse(self, line):
+        """Parse a cron line string and save the info as the objects."""
+        line = _unicode(line)
+        if not line or line[0] == '#':
+            self.enabled = False
+            line = line[1:].strip()
+        # We parse all lines so we can detect disabled entries.
+        self._set_parse(ITEMREX.findall(line))
+        self._set_parse(SPECREX.findall(line))
+
+    def _set_parse(self, result):
+        """Set all the parsed variables into the item"""
+        if not result:
+            return
+        self.comment = result[0][-1]
+        if self.cron.user is False:
+            # Special flag to look for per-command user
+            ret = result[0][-3].split(None, 1)
+            self.set_command(ret[-1])
+            if len(ret) == 2:
+                self.user = ret[0]
+            else:
+                self.valid = False
+                self.enabled = False
+                LOG.error(str("Missing user or command in system cron line."))
+        else:
+            self.set_command(result[0][-3])
+        try:
+            self.setall(*result[0][:-3])
+            self.valid = True
+        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"""
+        self.command = _unicode(self.command)
+        user = ''
+        if self.cron and self.cron.user is False:
+            if not self.user:
+                raise ValueError("Job to system-cron format, no user set!")
+            user = self.user + ' '
+        result = u"%s %s%s" % (unicode(self.slices), user, self.command)
+        if self.comment:
+            comment = self.comment = _unicode(self.comment)
+            if self.marker:
+                comment = u"#%s: %s" % (self.marker, comment)
+            else:
+                comment = u"# " + comment
+
+            if SYSTEMV or self.pre_comment:
+                result = comment + "\n" + result
+            else:
+                result += ' ' + comment
+
+        if not self.enabled:
+            result = u"# " + result
+        return unicode(self.env) + result
+
+    def every_reboot(self):
+        """Set to every reboot instead of a time pattern: @reboot"""
+        self.clear()
+        return self.slices.setall('@reboot')
+
+    def every(self, unit=1):
+        """
+        Replace existing time pattern with a single unit, setting all lower
+        units to first value in valid range.
+
+        For instance job.every(3).days() will be `0 0 */3 * *`
+        while job.day().every(3) would be `* * */3 * *`
+
+        Many of these patterns exist as special tokens on Linux, such as
+        `@midnight` and `@hourly`
+        """
+        return Every(self.slices, unit)
+
+    def setall(self, *args):
+        """Replace existing time pattern with these five values given as args:
+
+           job.setall("1 2 * * *")
+           job.setall(1, 2) == '1 2 * * *'
+           job.setall(0, 0, None, '>', 'SUN') == '0 0 * 12 SUN'
+        """
+        return self.slices.setall(*args)
+
+    def clear(self):
+        """Clear the special and set values"""
+        return self.slices.clear()
+
+    def frequency(self, year=None):
+        """Returns the number of times this item will execute in a given year
+           (defaults to this year)
+        """
+        return self.slices.frequency(year=year)
+
+    def frequency_per_year(self, year=None):
+        """Returns the number of /days/ this item will execute on in a year
+           (defaults to this year)
+        """
+        return self.slices.frequency_per_year(year=year)
+
+    def frequency_per_day(self):
+        """Returns the number of time this item will execute in any day"""
+        return self.slices.frequency_per_day()
+
+    def frequency_per_hour(self):
+        """Returns the number of times this item will execute in any hour"""
+        return self.slices.frequency_per_hour()
+
+    def run_pending(self, now=None):
+        """Runs the command if scheduled"""
+        now = now or datetime.now()
+        if self.is_enabled():
+            if self.last_run is None:
+                self.last_run = now
+
+            next_time = self.schedule(self.last_run).get_next()
+            if next_time < now:
+                self.last_run = now
+                return self.run()
+        return -1
+
+    def run(self):
+        """Runs the given command as a pipe"""
+        env = os.environ.copy()
+        env.update(self.env.all())
+        shell = self.env.get('SHELL', SHELL)
+        (out, err) = open_pipe(shell, '-c', self.command, env=env).communicate()
+        if err:
+            LOG.error(err.decode("utf-8"))
+        return out.decode("utf-8").strip()
+
+    def schedule(self, date_from=None):
+        """Return a croniter schedule if available."""
+        if not date_from:
+            date_from = datetime.now()
+        try:
+            # Croniter is an optional import
+            from croniter.croniter import croniter
+        except ImportError:
+            raise ImportError("Croniter not available. Please install croniter"
+                              " python module via pip or your package manager")
+        return croniter(self.slices.clean_render(), date_from, ret_type=datetime)
+
+    def description(self, **kw):
+        """
+        Returns a description of the crontab's schedule (if available)
+
+        **kw - Keyword arguments to pass to cron_descriptor (see docs)
+        """
+        try:
+            from cron_descriptor import ExpressionDescriptor
+        except ImportError:
+            raise ImportError("cron_descriptor not available. Please install"\
+              "cron_descriptor python module via pip or your package manager")
+
+        exdesc = ExpressionDescriptor(self.slices.clean_render(), **kw)
+        return exdesc.get_description()
+
+    @property
+    def log(self):
+        """Return a cron log specific for this job only"""
+        if not self._log and self.cron:
+            self._log = self.cron.log.for_program(self.command)
+        return self._log
+
+    @property
+    def minute(self):
+        """Return the minute slice"""
+        return self.slices[0]
+
+    @property
+    def minutes(self):
+        """Same as minute"""
+        return self.minute
+
+    @property
+    def hour(self):
+        """Return the hour slice"""
+        return self.slices[1]
+
+    @property
+    def hours(self):
+        """Same as hour"""
+        return self.hour
+
+    @property
+    def day(self):
+        """Return the day slice"""
+        return self.dom
+
+    @property
+    def dom(self):
+        """Return the day-of-the month slice"""
+        return self.slices[2]
+
+    @property
+    def month(self):
+        """Return the month slice"""
+        return self.slices[3]
+
+    @property
+    def months(self):
+        """Same as month"""
+        return self.month
+
+    @property
+    def dow(self):
+        """Return the day of the week slice"""
+        return self.slices[4]
+
+    def __repr__(self):
+        return "<CronItem '%s'>" % unicode(self)
+
+    def __len__(self):
+        return len(unicode(self))
+
+    def __getitem__(self, key):
+        return self.slices[key]
+
+    def __lt__(self, value):
+        return self.frequency() < CronSlices(value).frequency()
+
+    def __gt__(self, value):
+        return self.frequency() > CronSlices(value).frequency()
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        if not self.is_valid() and self.enabled:
+            raise ValueError('Refusing to render invalid crontab.'
+                             ' Disable to continue.')
+        return self.render()
+
+
+class Every(object):
+    """Provide an interface to the job.every() method:
+        Available Calls:
+          minute, minutes, hour, hours, dom, doms, month, months, dow, dows
+
+       Once run all units will be cleared (set to *) then proceeding units
+       will be set to '0' and the target unit will be set as every x units.
+    """
+    def __init__(self, item, units):
+        self.slices = item
+        self.unit = units
+        for (key, name) in enumerate(['minute', 'hour', 'dom', 'month', 'dow',
+                                      'min', 'hour', 'day', 'moon', 'weekday']):
+            setattr(self, name, self.set_attr(key % 5))
+            setattr(self, name+'s', self.set_attr(key % 5))
+
+    def set_attr(self, target):
+        """Inner set target, returns function"""
+        def innercall():
+            """Returned inner call for setting slice targets"""
+            self.slices.clear()
+            # Day-of-week is actually a level 2 set, not level 4.
+            for key in range(target == 4 and 2 or target):
+                self.slices[key].on('<')
+            self.slices[target].every(self.unit)
+        return innercall
+
+    def year(self):
+        """Special every year target"""
+        if self.unit > 1:
+            raise ValueError("Invalid value '%s', outside 1 year" % self.unit)
+        self.slices.setall('@yearly')
+
+
+class CronSlices(list):
+    """Controls a list of five time 'slices' which reprisent:
+        minute frequency, hour frequency, day of month frequency,
+        month requency and finally day of the week frequency.
+     """
+    def __init__(self, *args):
+        super(CronSlices, self).__init__([CronSlice(info) for info in S_INFO])
+        self.special = None
+        self.setall(*args)
+        self.is_valid = self.is_self_valid
+
+    def is_self_valid(self, *args):
+        """Object version of is_valid"""
+        return CronSlices.is_valid(*(args or (self,)))
+
+    @classmethod
+    def is_valid(cls, *args): #pylint: disable=method-hidden
+        """Returns true if the arguments are valid cron pattern"""
+        try:
+            return bool(cls(*args))
+        except (ValueError, KeyError):
+            return False
+
+    def setall(self, *slices):
+        """Parses the various ways date/time frequency can be specified"""
+        self.clear()
+        if len(slices) == 1:
+            (slices, self.special) = self._parse_value(slices[0])
+            if slices[0] == '@reboot':
+                return
+        if id(slices) == id(self):
+            raise AssertionError("Can not set cron to itself!")
+        for set_a, set_b in zip(self, slices):
+            set_a.parse(set_b)
+
+    @staticmethod
+    def _parse_value(value):
+        """Parse a single value into an array of slices"""
+        if isinstance(value, basestring) and value:
+            return CronSlices._parse_str(value)
+        if isinstance(value, CronItem):
+            return value.slices, None
+        elif isinstance(value, datetime):
+            return [value.minute, value.hour, value.day, value.month, '*'], None
+        elif isinstance(value, time):
+            return [value.minute, value.hour, '*', '*', '*'], None
+        elif 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.
+        elif not isinstance(value, (list, tuple)):
+            raise ValueError("Unknown type: {}".format(type(value).__name__))
+        return value, None
+
+    @staticmethod
+    def _parse_str(value):
+        """Parse a string which contains slice information"""
+        key = value.lstrip('@').lower()
+        if value.count(' ') == 4:
+            return value.strip().split(' '), None
+        elif key in SPECIALS.keys():
+            return SPECIALS[key].split(' '), '@' + key
+        elif value.startswith('@'):
+            raise ValueError("Unknown special '{}'".format(value))
+        return [value], None
+
+    def clean_render(self):
+        """Return just numbered parts of this crontab"""
+        return ' '.join([unicode(s) for s in self])
+
+    def render(self):
+        "Return just the first part of a cron job (the numbers or special)"
+        slices = self.clean_render()
+        if self.special:
+            return self.special
+        elif not SYSTEMV:
+            for (name, value) in SPECIALS.items():
+                if value == slices and name not in SPECIAL_IGNORE:
+                    return "@%s" % name
+        return slices
+
+    def clear(self):
+        """Clear the special and set values"""
+        self.special = None
+        for item in self:
+            item.clear()
+
+    def frequency(self, year=None):
+        """Return frequence per year times frequency per day"""
+        return self.frequency_per_year(year=year) * self.frequency_per_day()
+
+    def frequency_per_year(self, year=None):
+        """Returns the number of times this item will execute
+           in a given year (default is this year)"""
+        result = 0
+        if not year:
+            year = date.today().year
+
+        weekdays = list(self[4])
+
+        for month in self[3]:
+            for day in self[2]:
+                try:
+                    if date(year, month, day).weekday() in weekdays:
+                        result += 1
+                except ValueError:
+                    continue
+        return result
+
+    def frequency_per_day(self):
+        """Returns the number of times this item will execute in any day"""
+        return len(self[0]) * len(self[1])
+
+    def frequency_per_hour(self):
+        """Returns the number of times this item will execute in any hour"""
+        return len(self[0])
+
+    def __str__(self):
+        return self.render()
+
+    def __eq__(self, arg):
+        return self.render() == CronSlices(arg).render()
+
+
+class SundayError(KeyError):
+    """Sunday was specified as 7 instead of 0"""
+    pass
+
+class Also(object):
+    """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(object):
+    """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 parse(self, value):
+        """Set values into the slice."""
+        self.clear()
+        if value is not None:
+            for part in unicode(value).split(','):
+                if part.find("/") > 0 or part.find("-") > 0 or part == '*':
+                    self.parts += self.get_range(part)
+                    continue
+                self.parts.append(self.parse_value(part, sunday=0))
+
+    def render(self, resolve=False):
+        """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 "<CronSlice '%s'>" % unicode(self)
+
+    def __eq__(self, value):
+        return unicode(self) == unicode(value)
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.render()
+
+    def every(self, n_value, also=False):
+        """Set the every X units value"""
+        if not also:
+            self.clear()
+        self.parts += self.get_range(int(n_value))
+        return self.parts[-1]
+
+    def on(self, *n_value, **opts):
+        """Set the time values to the specified placements."""
+        if not opts.get('also', False):
+            self.clear()
+        for set_a in n_value:
+            self.parts += self.parse_value(set_a, sunday=0),
+        return self.parts
+
+    def during(self, vfrom, vto, also=False):
+        """Set the During value, which sets a range"""
+        if not also:
+            self.clear()
+        self.parts += self.get_range(unicode(vfrom) + '-' + unicode(vto))
+        return self.parts[-1]
+
+    @property
+    def also(self):
+        """Appends rather than replaces the new values"""
+        return Also(self)
+
+    def clear(self):
+        """clear the slice ready for new vaues"""
+        self.parts = []
+
+    def get_range(self, *vrange):
+        """Return a cron range for this slice"""
+        ret = CronRange(self, *vrange)
+        if ret.dangling is not None:
+            return [ret.dangling, ret]
+        return [ret]
+
+    def __iter__(self):
+        """Return the entire element as an iterable"""
+        ret = {}
+        # An empty part means '*' which is every(1)
+        if not self.parts:
+            self.every(1)
+        for part in self.parts:
+            if isinstance(part, CronRange):
+                for bit in part.range():
+                    ret[bit] = 1
+            else:
+                ret[int(part)] = 1
+        for val in ret:
+            yield val
+
+    def __len__(self):
+        """Returns the number of times this slice happens in it's range"""
+        return len(list(self.__iter__()))
+
+    def parse_value(self, val, sunday=None):
+        """Parse the value of the cron slice and raise any errors needed"""
+        if val == '>':
+            val = self.max
+        elif val == '<':
+            val = self.min
+        try:
+            out = get_cronvalue(val, self.enum)
+        except ValueError:
+            raise ValueError("Unrecognised %s: '%s'" % (self.name, val))
+        except KeyError:
+            raise KeyError("No enumeration for %s: '%s'" % (self.name, val))
+
+        if self.max == 6 and int(out) == 7:
+            if sunday is not None:
+                return sunday
+            raise SundayError("Detected Sunday as 7 instead of 0!")
+
+        if int(out) < self.min or int(out) > self.max:
+            raise ValueError("'{1}', not in {0.min}-{0.max} for {0.name}".format(self, val))
+        return out
+
+
+def get_cronvalue(value, enums):
+    """Returns a value as int (pass-through) or a special enum value"""
+    if isinstance(value, int):
+        return value
+    elif unicode(value).isdigit():
+        return int(str(value))
+    if not enums:
+        raise KeyError("No enumeration allowed")
+    return CronValue(unicode(value), enums)
+
+
+class CronValue(object): # pylint: disable=too-few-public-methods
+    """Represent a special value in the cron line"""
+    def __init__(self, value, enums):
+        self.text = value
+        self.value = enums.index(value.lower())
+
+    def __lt__(self, value):
+        return self.value < int(value)
+
+    def __repr__(self):
+        return unicode(self)
+
+    def __str__(self):
+        return self.text
+
+    def __int__(self):
+        return self.value
+
+
+def _render_values(values, sep=',', resolve=False):
+    """Returns a rendered list, sorted and optionally resolved"""
+    if len(values) > 1:
+        values.sort()
+    return sep.join([_render(val, resolve) for val in values])
+
+
+def _render(value, resolve=False):
+    """Return a single value rendered"""
+    if isinstance(value, CronRange):
+        return value.render(resolve)
+    if resolve:
+        return str(int(value))
+    return unicode(u'{:02d}'.format(value) if ZERO_PAD else value)
+
+
+class CronRange(object):
+    """A range between one value and another for a time range."""
+    def __init__(self, vslice, *vrange):
+        # holds an extra dangling entry, for example sundays.
+        self.dangling = None
+        self.slice = vslice
+        self.cron = None
+        self.seq = 1
+
+        if not vrange:
+            self.all()
+        elif isinstance(vrange[0], basestring):
+            self.parse(vrange[0])
+        elif isinstance(vrange[0], (int, CronValue)):
+            if len(vrange) == 2:
+                (self.vfrom, self.vto) = vrange
+            else:
+                self.seq = vrange[0]
+                self.all()
+
+    def parse(self, value):
+        """Parse a ranged value in a cronjob"""
+        if value.count('/') == 1:
+            value, seq = value.split('/')
+            try:
+                self.seq = self.slice.parse_value(seq)
+            except SundayError:
+                self.seq = 1
+                value = "0-0"
+            if self.seq < 1 or self.seq > self.slice.max:
+                raise ValueError("Sequence can not be divided by zero or max")
+        if value.count('-') == 1:
+            vfrom, vto = value.split('-')
+            self.vfrom = self.slice.parse_value(vfrom, sunday=0)
+            try:
+                self.vto = self.slice.parse_value(vto)
+            except SundayError:
+                if self.vfrom == 1:
+                    self.vfrom = 0
+                else:
+                    self.dangling = 0
+                self.vto = self.slice.parse_value(vto, sunday=6)
+            if self.vto < self.vfrom:
+                LOG.warning("Bad range '{0.vfrom}-{0.vto}'".format(self))
+        elif value == '*':
+            self.all()
+        else:
+            raise ValueError('Unknown cron range value "%s"' % value)
+
+    def all(self):
+        """Set this slice to all units between the miniumum and maximum"""
+        self.vfrom = self.slice.min
+        self.vto = self.slice.max
+
+    def render(self, resolve=False):
+        """Render the ranged value for a cronjob"""
+        value = '*'
+        if int(self.vfrom) > self.slice.min or int(self.vto) < self.slice.max:
+            if self.vfrom == self.vto:
+                value = unicode(self.vfrom)
+            else:
+                value = _render_values([self.vfrom, self.vto], '-', resolve)
+        if self.seq != 1:
+            value += "/%d" % self.seq
+        if value != '*' and SYSTEMV:
+            value = ','.join([unicode(val) for val in self.range()])
+        return value
+
+    def range(self):
+        """Returns the range of this cron slice as a iterable list"""
+        return range(int(self.vfrom), int(self.vto)+1, self.seq)
+
+    def every(self, value):
+        """Set the sequence value for this range."""
+        self.seq = int(value)
+
+    def __lt__(self, value):
+        return int(self.vfrom) < int(value)
+
+    def __gt__(self, value):
+        return int(self.vto) > int(value)
+
+    def __int__(self):
+        return int(self.vfrom)
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.render()
+
+
+class OrderedVariableList(OrderedDict):
+    """An ordered dictionary with a linked list containing
+    the previous OrderedVariableList which this list depends.
+
+    Duplicates in this list are weeded out in favour of the previous
+    list in the chain.
+
+    This is all in aid of the ENV variables list which must exist one
+    per job in the chain.
+    """
+    def __init__(self, *args, **kw):
+        self.job = kw.pop('job', None)
+        super(OrderedVariableList, self).__init__(*args, **kw)
+
+    @property
+    def previous(self):
+        """Returns the previous env in the list of jobs in the cron"""
+        if self.job is not None and self.job.cron is not None:
+            index = self.job.cron.crons.index(self.job)
+            if index == 0:
+                return self.job.cron.env
+            return self.job.cron[index-1].env
+        return None
+
+    def all(self):
+        """
+        Returns the full dictionary, everything from this dictionary
+        plus all those in the chain above us.
+        """
+        if self.job is not None:
+            ret = self.previous.all().copy()
+            ret.update(self)
+            return ret
+        return self.copy()
+
+    def __getitem__(self, key):
+        previous = self.previous
+        if key in self:
+            return super(OrderedVariableList, self).__getitem__(key)
+        elif previous is not None:
+            return previous.all()[key]
+        raise KeyError("Environment Variable '%s' not found." % key)
+
+    def __str__(self):
+        """Constructs to variable list output used in cron jobs"""
+        ret = []
+        for key, value in self.items():
+            if self.previous:
+                if self.previous.all().get(key, None) == value:
+                    continue
+            if ' ' in unicode(value) or value == '':
+                value = '"%s"' % value
+            ret.append("%s=%s" % (key, unicode(value)))
+        ret.append('')
+        return "\n".join(ret)

+ 145 - 0
desktop/core/ext-py/python-crontab-2.3.6/crontabs.py

@@ -0,0 +1,145 @@
+#
+# Copyright 2016, Martin Owens <doctormo@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+The crontabs manager will list all available crontabs on the system.
+"""
+
+import os
+import sys
+import pwd
+import itertools
+
+from os import stat, access, X_OK
+from pwd import getpwuid
+from crontab import CronTab
+
+class UserSpool(list):
+    """Generates all user crontabs, yields both owned and abandoned tabs"""
+    def __init__(self, loc, tabs=None):
+        for username in self.listdir(loc):
+            tab = self.generate(loc, username)
+            if tab:
+                self.append(tab)
+        if not self:
+            tab = CronTab(user=True)
+            if tab:
+                self.append(tab)
+
+    def listdir(self, loc):
+        try:
+            return os.listdir(loc)
+        except OSError:
+            return []
+
+    def get_owner(self, path):
+        """Returns user file at path"""
+        try:
+            return getpwuid(stat(path).st_uid).pw_name
+        except KeyError:
+            return
+
+    def generate(self, loc, username):
+        path = os.path.join(loc, username)
+        if username != self.get_owner(path):
+            # Abandoned crontab pool entry!
+            return CronTab(tabfile=path)
+        return CronTab(user=username)
+
+
+class SystemTab(list):
+    """Generates all system tabs"""
+    def __init__(self, loc, tabs=None):
+        if os.path.isdir(loc):
+            for item in os.listdir(loc):
+                if item[0] == '.':
+                    continue
+                path = os.path.join(loc, item)
+                self.append(CronTab(user=False, tabfile=path))
+        elif os.path.isfile(loc):
+            self.append(CronTab(user=False, tabfile=loc))
+
+
+class AnaCronTab(list):
+    """Attempts to digest anacron entries (if possible)"""
+    def __init__(self, loc, tabs=None):
+        if tabs and os.path.isdir(loc):
+            self.append(CronTab(user=False))
+            jobs = list(tabs.all.find_command(loc))
+            if jobs:
+                for item in os.listdir(loc):
+                    self.add(loc, item, jobs[0])
+                jobs[0].delete()
+
+    def add(self, loc, item, anajob):
+        path = os.path.join(loc, item)
+        if item in ['0anacron'] or item[0] == '.' or not access(path, X_OK):
+            return
+        job = self[0].new(command=path, user=anajob.user)
+        job.set_comment('Anacron %s' % loc.split('.')[-1])
+        job.setall(anajob)
+        return job
+
+
+# Files are direct, directories are listed (recursively)
+KNOWN_LOCATIONS = [
+  # Known linux locations (Debian, RedHat, etc)
+  (UserSpool, '/var/spool/cron/crontabs/'),
+  (SystemTab, '/etc/crontab'),
+  (SystemTab, '/etc/cron.d/'),
+  # Anacron digestion (we want to know more)
+  (AnaCronTab, '/etc/cron.hourly'),
+  (AnaCronTab, '/etc/cron.daily'),
+  (AnaCronTab, '/etc/cron.weekly'),
+  (AnaCronTab, '/etc/cron.monthly'),
+  # Known MacOSX locations
+   # None
+  # Other (windows, bsd)
+   # None
+]
+
+class CronTabs(list):
+    """Singleton dictionary of all detectable crontabs"""
+    _all = None
+    _self = None
+
+    def __new__(cls, *args, **kw):
+        if not cls._self:
+            cls._self = super(CronTabs, cls).__new__(cls, *args, **kw)
+        return cls._self
+
+    def __init__(self):
+        if not self:
+            for loc in KNOWN_LOCATIONS:
+                self.add(*loc)
+
+    def add(self, cls, *args):
+        for tab in cls(*args, tabs=self):
+            self.append(tab)
+            self._all = None
+
+    @property
+    def all(self):
+        """Return a CronTab object with all jobs (read-only)"""
+        if self._all is None:
+            self._all = CronTab(user=False)
+            for tab in self:
+                for job in tab:
+                    if job.user is None:
+                        job.user = tab.user or 'unknown'
+                    self._all.append(job)
+        return self._all
+

+ 4 - 0
desktop/core/ext-py/python-crontab-2.3.6/setup.cfg

@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 79 - 0
desktop/core/ext-py/python-crontab-2.3.6/setup.py

@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2008-2018 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+# pylint: disable=bad-whitespace
+"""Setup the crontab module"""
+
+import os
+from setuptools import setup
+from crontab import __version__, __pkgname__
+
+# remove MANIFEST. distutils doesn't properly update it when the
+# contents of directories change.
+if os.path.exists('MANIFEST'):
+    os.remove('MANIFEST')
+
+# Grab description for Pypi
+with open('README.rst') as fhl:
+    description = fhl.read()
+
+# Used for rpm building
+RELEASE = "1"
+
+setup(
+    name             = __pkgname__,
+    version          = __version__,
+    release          = RELEASE,
+    description      = 'Python Crontab API',
+    long_description = description,
+    author           = 'Martin Owens',
+    url              = 'https://gitlab.com/doctormo/python-crontab/',
+    author_email     = 'doctormo@gmail.com',
+    test_suite       = 'tests',
+    platforms        = 'linux',
+    license          = 'LGPLv3',
+    py_modules       = ['crontab', 'crontabs', 'cronlog'],
+    provides         = ['crontab', 'crontabs', 'cronlog'],
+    install_requires = ['python-dateutil'],
+    extras_require   = {
+        'cron-schedule': ['croniter'],
+        'cron-description': ['cron-descriptor'],
+    },
+    classifiers      = [
+        'Development Status :: 5 - Production/Stable',
+        'Development Status :: 6 - Mature',
+        'Intended Audience :: Developers',
+        'Intended Audience :: Information Technology',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)',
+        'Operating System :: POSIX',
+        'Operating System :: POSIX :: Linux',
+        'Operating System :: POSIX :: SunOS/Solaris',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.5',
+    ],
+    options = {
+        'bdist_rpm': {
+            'build_requires': [
+                'python',
+                'python-setuptools',
+            ],
+            'release': RELEASE,
+        },
+    },
+)

+ 1 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/__init__.py

@@ -0,0 +1 @@
+

+ 7 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/basic.log

@@ -0,0 +1,7 @@
+A really long line which can test log lines ability to put two bits together
+First Line
+
+9
+2
+Sickem
+The End

+ 4 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/crontab

@@ -0,0 +1,4 @@
+
+* * * * * peter parker
+* 2 * * * driver parker
+

+ 50 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/crontest

@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+import os
+import sys
+
+def main():
+    """Run this"""
+    args = sys.argv[1:]
+    loc = os.path.dirname(__file__)
+
+    if '-h' in args:
+        print('|'.join(sorted(args)))
+        return
+    elif '-e' in args:
+        sys.stderr.write('|'.join(sorted(args)) + "\n")
+        return
+    elif '-ev' in args:
+        print(os.environ.get('CR_VAR', 'FAILED') + '\n')
+        return
+
+    user = 'user'
+    if '-u' in args:
+        user = args[args.index('-u')+1]
+
+    if '-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: %s" % filename)
+        if os.path.exists(new_name):
+            raise KeyError("Can't write file: %s, exists" % new_name)
+        with open(filename, 'r') as source:
+            with open(new_name, 'w') as output:
+                output.write(source.read())
+
+if __name__ == '__main__':
+    main()
+    sys.exit(0)
+

+ 3 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/specials.tab

@@ -0,0 +1,3 @@
+0 * * * * hourly
+0 0 * * * daily
+0 0 * * 0 weekly

+ 5 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/specials_enc.tab

@@ -0,0 +1,5 @@
+@hourly hourly
+@daily daily
+@midnight midnight
+@weekly weekly
+@reboot reboot

+ 32 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/test.log

@@ -0,0 +1,32 @@
+Apr  4 21:24:01 servername CRON[16490]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:24:01 servername CRON[16489]: (root) CMD (rootscript &> /dev/null)
+Apr  4 21:24:01 servername CRON[16487]: (CRON) info (Cron information item)
+Apr  4 21:25:01 servername CRON[16496]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:25:01 servername CRON[16497]: (root) CMD (shadowscript &> /dev/null)
+Apr  4 21:25:01 servername CRON[16494]: (CRON) info (Cron information item)
+Apr  4 21:26:01 servername CRON[16513]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:26:01 servername CRON[16511]: (CRON) info (Cron information item)
+Apr  4 21:26:01 servername CRON[16514]: (root) CMD (rootscript &> /dev/null)
+Apr  4 21:27:01 servername CRON[16519]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:27:01 servername CRON[16518]: (CRON) info (Cron information item)
+Apr  4 21:28:01 servername CRON[16523]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:28:01 servername CRON[16522]: (root) CMD (rootscript &> /dev/null)
+Apr  4 21:28:01 servername CRON[16520]: (CRON) info (Cron information item)
+Apr  4 21:28:30 servername NOTCRON: Log entry that isn't a cron
+Apr  4 21:28:30 servername NOTCRON: entry in order to test
+Apr  4 21:28:31 servername NOTCRON: that these are ignored
+Apr  4 21:29:01 servername CRON[16539]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:29:01 servername CRON[16538]: (CRON) info (Cron information item)
+Apr  4 21:30:01 servername CRON[16551]: (root) CMD (shadowscript &> /dev/null)
+Apr  4 21:30:01 servername CRON[16552]: (root) CMD (rootscript &> /dev/null)
+Apr  4 21:30:01 servername CRON[16554]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:30:01 servername CRON[16548]: (CRON) info (Cron information item)
+Apr  4 21:31:01 servername CRON[16569]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:31:01 servername CRON[16568]: (CRON) info (Cron information item)
+Apr  4 21:32:01 servername CRON[16573]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:32:01 servername CRON[16574]: (root) CMD (rootscript &> /dev/null)
+Apr  4 21:32:01 servername CRON[16571]: (CRON) info (Cron information item)
+Apr  4 21:33:02 servername CRON[16588]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:33:02 servername CRON[16587]: (CRON) info (Cron information item)
+Apr  4 21:34:01 servername CRON[16591]: (user) CMD (userscript &> /dev/null)
+Apr  4 21:34:01 servername CRON[16592]: (root) CMD (rootscript &> /dev/null)

+ 17 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/data/test.tab

@@ -0,0 +1,17 @@
+# 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

+ 120 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_compatibility.py

@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2012 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab interaction.
+"""
+
+import os
+import sys
+
+import unittest
+import crontab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+INITAL_TAB = """
+# First Comment
+0,30 * * * * firstcommand
+"""
+
+class UserTestCase(unittest.TestCase):
+    def test_01_self_user(self):
+        tab = crontab.CronTab(user='foo')
+        self.assertEqual(tab.user_opt, {'u': 'foo'})
+        tab = crontab.CronTab(user=crontab.current_user())
+        self.assertEqual(tab.user_opt, {})
+
+
+class CompatTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    @classmethod
+    def setUpClass(cls):
+        crontab.SYSTEMV = True
+
+    @classmethod
+    def tearDownClass(cls):
+        crontab.SYSTEMV = False
+
+    def setUp(self):
+        self.crontab = crontab.CronTab(tab=INITAL_TAB)
+
+    def test_00_enabled(self):
+        """Test Compatability Mode"""
+        self.assertTrue(crontab.SYSTEMV)
+
+    def test_01_addition(self):
+        """New Job Rendering"""
+        job = self.crontab.new('addition1')
+        job.minute.during(0, 3)
+        job.hour.during(21, 23).every(1)
+        job.dom.every(1)
+
+        self.assertEqual(job.render(), '0,1,2,3 21,22,23 * * * addition1')
+
+    def test_02_addition(self):
+        """New Job Rendering"""
+        job = self.crontab.new(command='addition2')
+
+        job.minute.during(4, 9)
+        job.hour.during(2, 10).every(2)
+        job.dom.every(10)
+
+        self.assertNotEqual(job.render(), '4-9 2-10/2 */3 * * addition2')
+        self.assertEqual(job.render(), '4,5,6,7,8,9 2,4,6,8,10 1,11,21,31 * * addition2')
+
+    def test_03_specials(self):
+        """Ignore Special Symbols"""
+        tab = crontab.CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab'))
+        self.assertEqual(tab.render(), """0 * * * * hourly
+0 0 * * * daily
+0 0 * * 0 weekly
+""")
+
+    def test_04_comments(self):
+        """Comments should be on their own lines"""
+        self.assertEqual(self.crontab[0].comment, 'First Comment')
+        self.assertEqual(self.crontab.render(), INITAL_TAB)
+        job = self.crontab.new('command', comment="Test comment")
+        self.assertEqual(job.render(), "# Test comment\n* * * * * command")
+
+    def test_05_ansible(self):
+        """Crontab shouldn't break ansible cronjobs"""
+        cron = crontab.CronTab(tab="""
+#Ansible: {job_name}
+* * * * * {command}
+""")
+        self.assertEqual(cron[0].comment, '{job_name}')
+        self.assertEqual(cron[0].command, '{command}')
+        self.assertEqual(str(cron[0]), '#Ansible: {job_name}\n* * * * * {command}')
+
+    def test_06_escaped_chars(self):
+        """Do escaped chars parse correctly when read in"""
+        cron = crontab.CronTab(tab="""
+* * * * * cmd arg_with_\#_character # comment
+""")
+        self.assertEqual(cron[0].command, 'cmd arg_with_\\#_character')
+        self.assertEqual(cron[0].comment, 'comment')
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       CompatTestCase,
+    )

+ 89 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_croniter.py

@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test croniter extention to find out when items will next happen.
+"""
+
+import os
+import sys
+from datetime import datetime
+
+sys.path.insert(0, '../')
+
+import unittest
+import crontab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+INITAL_TAB = """
+# Basic Comment
+20 * * * * execute # comment
+"""
+
+class CroniterTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = crontab.CronTab(tab=INITAL_TAB)
+        self.job = list(self.crontab.find_command('execute'))[0]
+        try:
+            import croniter
+        except ImportError:
+            self.skipTest("Croniter not installed")
+
+    def test_00_nocroniter(self):
+        """No Croniter"""
+        # Remove croniter if imported before.
+        for i in range(1, 4):
+            name = '.'.join((['croniter'] * i))
+            if name in sys.modules:
+                del sys.modules[name]
+
+        old, sys.path = sys.path, []
+        with self.assertRaises(ImportError):
+            self.job.schedule(datetime(2001, 10, 11, 1, 12, 10))
+        sys.path = old
+
+    def test_01_schedule(self):
+        """Get Scheduler"""
+        ct = self.job.schedule(datetime(2009, 10, 11, 5, 12, 10))
+        self.assertTrue(ct)
+
+    def test_02_next(self):
+        """Get Next Scheduled Items"""
+        ct = self.job.schedule(datetime(2000, 10, 11, 5, 12, 10))
+        self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 5, 20, 0))
+        self.assertEqual(ct.get_next(), datetime(2000, 10, 11, 6, 20, 0))
+
+    def test_03_prev(self):
+        """Get Prev Scheduled Items"""
+        ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10))
+        self.assertEqual(ct.get_prev(), datetime(2001, 10, 11, 0, 20, 0))
+        self.assertEqual(ct.get_prev(), datetime(2001, 10, 10, 23, 20, 0))
+
+    def test_04_current(self):
+        """Get Current Item"""
+        ct = self.job.schedule(datetime(2001, 10, 11, 1, 12, 10))
+        self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10))
+        self.assertEqual(ct.get_current(), datetime(2001, 10, 11, 1, 12, 10))
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       CroniterTestCase,
+    )

+ 112 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_crontabs.py

@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 Martin Owens <doctormo@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontabs listing all available crontabs.
+"""
+
+import os
+import sys
+
+import unittest
+import crontab
+import crontabs
+
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+class fake_getpwuid(object):
+    def __init__(self, st):
+        self.pw_name = st
+        if st == 'error':
+            raise KeyError("Expected Error")
+
+class fake_stat(object):
+    def __init__(self, path):
+        self.st_uid = os.path.basename(path).split('_')[0]
+
+crontabs.getpwuid = fake_getpwuid
+crontabs.stat = fake_stat
+
+class AnacronSourceTab(list):
+    """This enables the anacron detection"""
+    slices = "56 4 * * 4"
+    def __init__(self, tabs=None):
+        self.append(crontab.CronTab(user=False, tab="%s root anacron_cmd %s" \
+            % (self.slices, os.path.join(TEST_DIR, 'data', 'anacron'))))
+
+crontabs.KNOWN_LOCATIONS = [
+  (AnacronSourceTab,),
+  (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'spool')),
+  (crontabs.UserSpool, os.path.join(TEST_DIR, 'data', 'bad_spool')),
+  (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontab')),
+  (crontabs.SystemTab, os.path.join(TEST_DIR, 'data', 'crontabs')),
+  (crontabs.AnaCronTab, os.path.join(TEST_DIR, 'data', 'anacron')),
+]
+
+crontab.CRONCMD = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest'))
+
+class CronTabsTestCase(unittest.TestCase):
+    """Test use documentation in crontab."""
+    def setUp(self):
+        self.tabs = crontabs.CronTabs()
+
+    def assertInTabs(self, command, *users):
+        jobs = list(self.tabs.all.find_command(command))
+        self.assertEqual(len(jobs), len(users))
+        users = sorted([job.user for job in jobs])
+        self.assertEqual(users, sorted(users))
+        return jobs
+
+    def assertNotInTabs(self, command):
+        return self.assertInTabs(command)
+
+    def test_05_spool(self):
+        """Test a user spool"""
+        self.assertInTabs('do_vlog_brothers', 'hgreen', 'jgreen')
+
+    def test_06_bad_spool(self):
+        """Test no access to spool (non-root)"""
+        # IMPORTANT! This is testing the fact that the bad-spool will load
+        # the user's own crontab, in this instance this is 'user' from the
+        # crontest script. This tab is already loaded by the previous User
+        # spool and so we expect to find two of them.
+        self.assertInTabs('user_command', 'user', 'user')
+
+    def test_10_crontab_dir(self):
+        """Test crontabs loaded from system directory"""
+        self.assertInTabs('baggins', 'bilbo', 'frodo', 'plastic')
+
+    def test_11_crontab_file(self):
+        """Test a single crontab file loaded from system tab"""
+        self.assertInTabs('parker', 'driver', 'peter')
+
+    def test_20_anacron(self):
+        """Anacron digested"""
+        self.assertNotInTabs('anacron_cmd')
+        jobs = self.assertInTabs('an_command.sh', 'root')
+        self.assertEqual(str(jobs[0].slices), AnacronSourceTab.slices)
+        self.assertNotInTabs('not_command.txt')
+        
+
+if __name__ == '__main__':
+    test_support.run_unittest(CronTabsTestCase)
+

+ 66 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_description.py

@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test optional crontab description intergration.
+"""
+
+import sys
+sys.path.insert(0, '../')
+
+import unittest
+import crontab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+INITAL_TAB = """
+# Basic Comment
+20 10-20 */2 * * execute # At 20 minutes past the hour, between 10:00 AM and 08:59 PM, every 2 days
+"""
+
+class DescriptorTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = crontab.CronTab(tab=INITAL_TAB)
+        self.job = list(self.crontab.find_command('execute'))[0]
+        try:
+            import cron_descriptor
+        except ImportError:
+            self.skipTest("Cron-descriptor module not installed")
+
+    def test_00_no_module(self):
+        """No module found"""
+        # Remove module if imported already
+        for i in range(1, 4):
+            name = '.'.join((['cron_descriptor'] * i))
+            if name in sys.modules:
+                del sys.modules[name]
+
+        old, sys.path = sys.path, []
+        with self.assertRaises(ImportError):
+            self.job.description()
+        sys.path = old
+
+    def test_01_description(self):
+        """Get Job Description"""
+        self.assertEqual(self.job.description(), self.job.comment)
+
+if __name__ == '__main__':
+    test_support.run_unittest(DescriptorTestCase)
+

+ 121 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_enums.py

@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab enumerations.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, '../')
+
+import unittest
+from crontab import CronTab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+INITAL_TAB = """
+* * * JAN     SAT          enums
+* * * MAR-APR *            ranges
+* * * *       MON,FRI,WED  multiples
+* * * *       *            com1 # Comment One
+* * * *       *            com2 # Comment One
+"""
+
+COMMANDS = [
+    'enums',
+    'ranges',
+    'multiples',
+    'com1', 'com2',
+]
+
+RESULT_TAB = """
+* * * JAN SAT enums
+* * * MAR-APR * ranges
+* * * * MON,WED,FRI multiples
+* * * * * com1 # Comment One
+* * * * * com2 # Comment One
+"""
+
+class EnumTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = CronTab(tab=INITAL_TAB)
+
+    def test_01_presevation(self):
+        """All Entries Re-Rendered Correctly"""
+        self.crontab.write()
+        results = RESULT_TAB.split('\n')
+        line_no = 0
+        for line in self.crontab.intab.split('\n'):
+            self.assertEqual(str(line), results[line_no])
+            line_no += 1
+
+    def test_02_simple_enum(self):
+        """Simple Enumerations"""
+        e = list(self.crontab.find_command('enums'))[0]
+        self.assertEqual(e.month, 'JAN')
+        self.assertEqual(e.month.render(True), '1')
+        self.assertEqual(e.dow, 'SAT')
+        self.assertEqual(e.dow.render(True), '6')
+
+    def test_03_enum_range(self):
+        """Enumeration Ranges"""
+        e = list(self.crontab.find_command('ranges'))[0]
+        self.assertEqual(e.month, 'MAR-APR')
+        self.assertEqual(e.month.render(True), '3-4' )
+
+    def test_04_sets(self):
+        """Enumeration Sets"""
+        e = list(self.crontab.find_command('multiples'))[0]
+        self.assertEqual(e.dow, 'MON,WED,FRI')
+        self.assertEqual(e.dow.render(True), '1,3,5' )
+
+    def test_05_create(self):
+        """Create by Enumeration"""
+        job = self.crontab.new(command='new')
+        job.month.on('JAN')
+        job.dow.on('SUN')
+        self.assertEqual(str(job), '* * * JAN SUN new')
+
+    def test_06_create_range(self):
+        """Created Enum Range"""
+        job = self.crontab.new(command='new2')
+        job.month.during('APR', 'NOV').every(2)
+        self.assertEqual(str(job), '* * * APR-NOV/2 * new2')
+
+    def test_07_create_set(self):
+        """Created Enum Set"""
+        job = self.crontab.new(command='new3')
+        job.month.on('APR')
+        job.month.also.on('NOV','JAN')
+        self.assertEqual(str(job), '* * * JAN,APR,NOV * new3')
+
+    def test_08_find_comment(self):
+        """Comment Set"""
+        jobs = list(self.crontab.find_comment('Comment One'))
+        self.assertEqual(len(jobs), 2)
+        for job in jobs:
+            self.assertEqual(job.comment, 'Comment One')
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       EnumTestCase,
+    )

+ 220 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_env.py

@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test the creation, reading and writing on environment variables.
+"""
+
+import os
+import sys
+sys.path.insert(0, '../')
+
+from .test_usage import TEST_DIR
+
+import unittest
+from collections import OrderedDict
+
+from crontab import CronTab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+INITIAL_TAB = """PERSONAL_VAR=bar
+
+CRON_VAR=fork
+34 12 * * * eat_soup
+
+CRON_VAR=spoon
+35 12 * * * eat_salad
+
+CRON_VAR=knife
+36 12 * * * eat_icecream
+
+SECONDARY=fork
+38 12 * * * eat_steak
+"""
+
+class EnvTestCase(unittest.TestCase):
+    """Test vixie cron user addition."""
+    def setUp(self):
+        self.crontab = CronTab(tab=INITIAL_TAB)
+
+    def test_01_consistancy(self):
+        """Read in crontab and write out the same"""
+        self.assertEqual(INITIAL_TAB, str(self.crontab))
+
+    def test_02_top_vars(self):
+        """Whole file env variables"""
+        crontab = CronTab(tab="SHELL=dash\n")
+        self.assertEqual(str(crontab), "SHELL=dash\n")
+        self.assertEqual(crontab.env['SHELL'], 'dash')
+        crontab.env['SHELL'] = 'bash'
+        self.assertEqual(str(crontab), "SHELL=bash\n")
+        self.assertEqual(crontab.env['SHELL'], 'bash')
+
+    def test_03_get_job_var(self):
+        """Test each of the job env structures"""
+        for job, expected in zip(self.crontab, [
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'fork'},
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'spoon'},
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife'},
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'knife', 'SECONDARY': 'fork'},
+          ]):
+            self.assertEqual(OrderedDict(job.env.all()), expected)
+
+    def test_04_set_job_var(self):
+        """Test that variables set are applied correctly"""
+        self.crontab[1].env['CRON_VAR'] = 'javlin'
+        self.assertEqual(self.crontab[1].env.all(),
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'})
+        self.crontab[2].env['CRON_VAR'] = 'javlin'
+        self.assertEqual(self.crontab[2].env.all(),
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin'})
+        self.assertEqual(self.crontab[3].env['PERSONAL_VAR'], 'bar')
+        self.assertEqual(self.crontab[3].env.all(),
+            {'PERSONAL_VAR': 'bar', 'CRON_VAR': 'javlin', 'SECONDARY': 'fork'})
+
+        self.crontab.env['PERSONAL_VAR'] = 'foo'
+        self.assertEqual(self.crontab[2].env.all(),
+            {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'javlin'})
+
+        self.crontab.env['CRON_VAR'] = 'fork'
+        self.assertEqual(self.crontab[0].env.all(),
+            {'PERSONAL_VAR': 'foo', 'CRON_VAR': 'fork'})
+
+        self.assertEqual(str(self.crontab), """PERSONAL_VAR=foo
+CRON_VAR=fork
+
+34 12 * * * eat_soup
+
+CRON_VAR=javlin
+35 12 * * * eat_salad
+
+36 12 * * * eat_icecream
+
+SECONDARY=fork
+38 12 * * * eat_steak
+""")
+
+    def test_05_no_env(self):
+        """Test that we get an error asking for no var"""
+        with self.assertRaises(KeyError):
+            self.crontab.env['BLUE_BOTTLE']
+        with self.assertRaises(KeyError):
+            self.crontab[0].env['RED_BOTTLE']
+
+    def test_06_env_access(self):
+        cron = CronTab(tab="""
+MYNAME='Random'
+
+* * * * * echo "first: $MYNAME"
+* * * * * echo "second: $MYNAME"
+* * * * * echo "third: $MYNAME"
+        """)
+        for job in cron:
+            self.assertEqual(job.env['MYNAME'], "Random")
+
+    def test_07_mutated_dict(self):
+        """Test when the ordered dict is changed during loop"""
+        cron = CronTab(tab="""
+ALL='all'
+ABCD='first'
+* * * * * echo "first"
+        """)
+
+    def test_08_space_quotes(self):
+        """Test that spaces and quotes are handled correctly"""
+        cron = CronTab(tab="""
+A=   123   
+B="   123   "
+C='   123   '
+D=  " 123 "  
+E= 1 2 3 
+""")
+        self.assertEqual(cron.env['A'], '123')
+        self.assertEqual(cron.env['B'], '   123   ')
+        self.assertEqual(cron.env['C'], '   123   ')
+        self.assertEqual(cron.env['D'], ' 123 ')
+        self.assertEqual(cron.env['E'], '1 2 3')
+
+        self.assertEqual(str(cron), """A=123
+B="   123   "
+C="   123   "
+D=" 123 "
+E="1 2 3"
+
+""")
+
+    def test_09_delete_middle(self):
+        """Test that a delete doesn't remove vars"""
+        self.crontab.remove_all(command='eat_icecream')
+        self.crontab.remove_all(command='eat_soup')
+        self.assertEqual(str(self.crontab), """PERSONAL_VAR=bar
+CRON_VAR=spoon
+35 12 * * * eat_salad
+
+
+CRON_VAR=knife
+SECONDARY=fork
+38 12 * * * eat_steak
+""")
+
+    def test_10_empty_env(self):
+        """Test when an env is an empty string it should have quotes"""
+        tab='MAILTO=""\n'
+        self.assertEqual(str(CronTab(tab=tab)), tab)
+
+    def test_11_empty_flow(self):
+        """Test what happends when an env is involved in flow"""
+        tab = """
+# A
+# B
+MAILTO=""
+# C
+*/10 * * * * /home/pi/job.py # any job
+"""
+        cron = CronTab(tab=tab)
+        job = cron.new('update.py', 'update')
+        job.setall('1 12 * * 3')
+        self.assertTrue(job.is_valid())
+        self.assertEqual(str(cron), """MAILTO=""
+
+# A
+# B
+# C
+*/10 * * * * /home/pi/job.py # any job
+
+1 12 * * 3 update.py # update
+""")
+
+        cron.remove_all(comment='update')
+
+        cron.write_to_user("bob")
+        filename = os.path.join(TEST_DIR, 'data', 'spool', 'bob')
+        with open(filename, 'r') as fhl:
+            self.assertEqual(fhl.read(), """MAILTO=""
+
+# A
+# B
+# C
+*/10 * * * * /home/pi/job.py # any job
+""")
+        os.unlink(filename)
+
+if __name__ == '__main__':
+    test_support.run_unittest(EnvTestCase)

+ 109 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_every.py

@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test simple 'every' api.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, '../')
+
+import unittest
+from crontab import CronTab, PY3
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+if PY3:
+    unicode = str
+
+class EveryTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'test.tab'))
+
+    def test_00_minutes(self):
+        """Every Minutes"""
+        for job in self.crontab:
+            job.every(3).minutes()
+            self.assertEqual(job.slices.clean_render(), '*/3 * * * *')
+            job.minutes.every(5)
+            self.assertEqual(job.slices.clean_render(), '*/5 * * * *')
+
+    def test_01_hours(self):
+        """Every Hours"""
+        for job in self.crontab:
+            job.every(3).hours()
+            self.assertEqual(job.slices.clean_render(), '0 */3 * * *')
+
+    def test_02_dom(self):
+        """Every Day of the Month"""
+        for job in self.crontab:
+            job.every(3).dom()
+            self.assertEqual(job.slices.clean_render(), '0 0 */3 * *')
+
+    def test_03_single(self):
+        """Every Single Hour"""
+        for job in self.crontab:
+            job.every().hour()
+            self.assertEqual(job.slices.clean_render(), '0 * * * *')
+
+    def test_04_month(self):
+        """Every Month"""
+        for job in self.crontab:
+            job.every(3).months()
+            self.assertEqual(job.slices.clean_render(), '0 0 1 */3 *')
+
+    def test_05_dow(self):
+        """Every Day of the Week"""
+        for job in self.crontab:
+            job.every(3).dow()
+            self.assertEqual(job.slices.clean_render(), '0 0 * * */3')
+
+    def test_06_year(self):
+        """Every Year"""
+        for job in self.crontab:
+            job.every().year()
+            self.assertEqual(job.slices.render(), '@yearly')
+            self.assertEqual(job.slices.clean_render(), '0 0 1 1 *')
+            self.assertRaises(ValueError, job.every(2).year)
+
+    def test_07_reboot(self):
+        """Every Reboot"""
+        for job in self.crontab:
+            job.every_reboot()
+            self.assertEqual(job.slices.render(), '@reboot')
+            self.assertEqual(job.slices.clean_render(), '* * * * *')
+
+    def test_08_newitem(self):
+        """Every on New Item"""
+        job = self.crontab.new(command='hourly')
+        job.every().hour()
+        self.assertEqual(job.slices.render(), '@hourly')
+        job = self.crontab.new(command='firstly')
+        job.hours.every(2)
+        self.assertEqual(job.slices.render(), '* */2 * * *')
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       EveryTestCase,
+    )

+ 161 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_frequency.py

@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test frequency calculations
+"""
+
+import os
+import sys
+
+sys.path.insert(0, '../')
+
+import unittest
+from crontab import CronTab, PY3
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+if PY3:
+    unicode = str
+
+START_TAB = """
+"""
+
+class FrequencyTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = CronTab(tab=START_TAB.strip())
+        self.job = self.crontab.new(command='freq')
+
+    def test_00_slice_frequency(self):
+        """Each Slice Frequency"""
+        # Make sure some of these overlap
+        self.job.setall("13-16,14-15 6,*/3 2,3,4 * *")
+        self.assertEqual(len(self.job[0]), 4)
+        self.assertEqual(len(self.job[1]), 8)
+        self.assertEqual(len(self.job[2]), 3)
+        self.assertEqual(len(self.job[3]), 12)
+        self.assertEqual(len(self.job[4]), 7)
+
+    def test_01_per_day(self):
+        """Frequency per Day"""
+        self.job.setall("21-29 10-14 * * *")
+        self.assertEqual(len(self.job[0]), 9)
+        self.assertEqual(len(self.job[1]), 5)
+        self.assertEqual(self.job.frequency_per_day(), 45)
+
+    def test_02_days_per_year(self):
+        """Frequency in Days per Year"""
+        self.job.setall("* * * * *")
+        self.assertEqual(self.job.frequency_per_year(year=2010), 365)
+        self.assertEqual(self.job.frequency_per_year(year=2012), 366)
+        self.job.setall("1 1 11-20 1,4,6,8 0,6")
+        self.assertEqual(len(self.job[2]), 10)
+        self.assertEqual(len(self.job[3]), 4)
+        self.assertEqual(len(self.job[4]), 2)
+        self.assertEqual(self.job.frequency_per_year(year=2013), 11)
+        self.assertEqual(self.job.frequency_per_year(year=2010), 12)
+
+    def test_03_job(self):
+        """Once Yearly"""
+        self.job.setall("0 0 1 1 *")
+        self.assertEqual(self.job.frequency(year=2010), 1)
+
+    def test_04_twice(self):
+        """Twice Yearly"""
+        self.job.setall("0 0 1 1,6 *")
+        self.assertEqual(self.job.frequency(year=2010), 2)
+
+    def test_05_thrice(self):
+        """Thrice Yearly"""
+        self.job.setall("0 0 1 1,3,6 *")
+        self.assertEqual(self.job.frequency(year=2010), 3)
+
+    def test_06_quart(self):
+        """Four Yearly"""
+        self.job.setall("0 0 1 */3 *")
+        self.assertEqual(self.job.frequency(year=2010), 4)
+
+    def test_07_monthly(self):
+        """Once a month"""
+        self.job.setall("0 0 1 * *")
+        self.assertEqual(self.job.frequency(year=2010), 12)
+
+    def test_08_six_monthly(self):
+        """Six a month"""
+        self.job.setall("0 0 1,2,3,4,5,6 * *")
+        self.assertEqual(self.job.frequency(year=2010), 72)
+
+    def test_09_every_day(self):
+        """Every Day"""
+        self.job.setall("0 0 * * *")
+        self.assertEqual(self.job.frequency(year=2010), 365)
+
+    def test_10_every_hour(self):
+        """Every Hour"""
+        self.job.setall("0 * * * *")
+        self.assertEqual(self.job.frequency(year=2010), 8760)
+
+    def test_11_every_other_hour(self):
+        """Every Other Hour"""
+        self.job.setall("0 */2 * * *")
+        self.assertEqual(self.job.frequency(year=2010), 4380)
+
+    def test_12_every_minute(self):
+        """Every Minute"""
+        self.job.setall("* * * * *")
+        self.assertEqual(self.job.frequency(year=2010), 525600)
+
+    def test_13_enum(self):
+        """Enumerations"""
+        self.job.setall("0 0 * * MON-WED")
+        self.assertEqual(self.job.frequency(year=2010), 156)
+        self.job.setall("0 0 * JAN-MAR *")
+        self.assertEqual(self.job.frequency(year=2010), 90)
+
+    def test_14_all(self):
+        """Test Maximum"""
+        self.job.setall("* * * * *")
+        self.assertEqual(self.job.frequency(2010), 525600)
+        self.assertEqual(self.job.frequency_per_year(year=2010), 365)
+        self.assertEqual(self.job.frequency_per_day(), 1440)
+        self.job.setall("*")
+        self.assertEqual(self.job.frequency_per_day(), 1440)
+        self.assertEqual(self.job.frequency_per_year(year=2010), 365)
+        self.assertEqual(self.job.frequency(2010), 525600)
+
+    def test_15_compare(self):
+        """Compare Times"""
+        job = self.crontab.new(command='match')
+        job.setall("*/2 * * * *")
+        self.assertEqual(job.slices, "*/2 * * * *")
+        self.assertEqual(job.slices, ["*/2"])
+        self.assertLess(job, ["*"])
+        self.assertGreater(job, "*/3")
+
+    def test_16_frequency_per_hour(self):
+        """Count per hour"""
+        job = self.crontab.new(command='per_hour')
+        job.setall("*/2 * * * *")
+        self.assertEqual(job.frequency_per_hour(), 30)
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       FrequencyTestCase,
+    )

+ 367 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_interaction.py

@@ -0,0 +1,367 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab interaction.
+"""
+
+import re
+import os
+import unittest
+
+from crontab import CronTab, CronSlices, CronSlice, PY3
+from .utils import LoggingMixin
+
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+if PY3:
+    unicode = str #pylint: disable=redefined-builtin
+
+COMMANDS = [
+    'firstcommand',
+    'range',
+    'byweek',
+    'disabled',
+    'spaced',
+    'python /example_app/testing.py',
+    'rebooted',
+]
+
+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
+
+*/59 * * * * python /example_app/testing.py # monitorCron
+
+@reboot rebooted # re-id
+# Last Comment @has this # extra
+"""
+
+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])
+
+    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, 7)
+
+    def test_03_blank(self):
+        """Render Blank"""
+        job = self.crontab.new(command='blank')
+        self.assertEqual(repr(job), '<CronItem \'* * * * * blank\'>')
+        self.assertEqual(job.render(), '* * * * * blank')
+
+    def test_04_number(self):
+        """Render Number"""
+        job = self.crontab.new(command='number')
+        job.minute.on(4)
+        self.assertEqual(job.render(), '4 * * * * number')
+
+    def test_05_fields(self):
+        """Render Hours Days and Weeks"""
+        job = self.crontab.new(command='fields')
+        job.hour.on(4)
+        self.assertEqual(job.render(), '* 4 * * * fields')
+        job.dom.on(5)
+        self.assertEqual(job.render(), '* 4 5 * * fields')
+        job.month.on(6)
+        self.assertEqual(job.render(), '* 4 5 6 * fields')
+        job.dow.on(7)
+        self.assertEqual(job.render(), '* 4 5 6 0 fields')
+
+    def test_05_multiple_fields(self):
+        """Test mutliple named fields"""
+        job = self.crontab.new(command='fields')
+        job.hour.on(4, 6, 7)
+        self.assertEqual(job.render(), '* 4,6,7 * * * fields')
+        job.dow.on('SUN', 'FRI')
+        self.assertEqual(job.render(), '* 4,6,7 * * SUN,FRI fields')
+
+    def test_06_clear(self):
+        """Render Hours Days and Weeks"""
+        job = self.crontab.new(command='clear')
+        job.minute.on(3)
+        job.hour.on(4)
+        job.dom.on(5)
+        job.month.on(6)
+        job.dow.on(7)
+        self.assertEqual(job.render(), '3 4 5 6 0 clear')
+        job.clear()
+        self.assertEqual(job.render(), '* * * * * clear')
+
+    def test_07_range(self):
+        """Render Time Ranges"""
+        job = self.crontab.new(command='range')
+        job.minute.during(4, 10)
+        self.assertEqual(job.render(), '4-10 * * * * range')
+        job.minute.during(15, 19)
+        self.assertEqual(job.render(), '15-19 * * * * range')
+        job.minute.clear()
+        self.assertEqual(job.render(), '* * * * * range')
+        job.minute.during(15, 19)
+        self.assertEqual(job.render(), '15-19 * * * * range')
+        job.minute.also.during(4, 10)
+        self.assertEqual(job.render(), '4-10,15-19 * * * * range')
+
+    def test_08_sequence(self):
+        """Render Time Sequences"""
+        job = self.crontab.new(command='seq')
+        job.hour.every(4)
+        self.assertEqual(job.render(), '* */4 * * * seq')
+        job.hour.during(2, 10)
+        self.assertEqual(job.render(), '* 2-10 * * * seq')
+        job.hour.clear()
+        self.assertEqual(job.render(), '* * * * * seq')
+        job.hour.during(2, 10).every(4)
+        self.assertEqual(job.render(), '* 2-10/4 * * * seq')
+        job.hour.also.during(1, 4)
+        self.assertEqual(job.render(), '* 1-4,2-10/4 * * * seq')
+        job.hour.also.every(4)
+        self.assertEqual(job.render(), '* */4,1-4,2-10/4 * * * seq')
+
+    def test_10_comment(self):
+        """Render cron Comments"""
+        job = self.crontab.new(command='com', comment='I love this')
+        self.assertEqual(unicode(job), '* * * * * com # I love this')
+
+    def test_11_disabled(self):
+        """Disabled Job"""
+        jobs = list(self.crontab.find_command('firstcommand'))
+        self.assertTrue(jobs[0].enabled)
+        jobs = list(self.crontab.find_command('disabled'))
+        self.assertFalse(jobs[0].enabled)
+
+    def test_12_disable(self):
+        """Disable and Enable Job"""
+        job = self.crontab.new(command='dis')
+        job.enable(False)
+        self.assertEqual(unicode(job), '# * * * * * dis')
+        job.enable()
+        self.assertEqual(unicode(job), '* * * * * dis')
+
+    def test_13_plural(self):
+        """Plural API"""
+        job = self.crontab.new(command='plural')
+        job.minutes.every(4)
+        job.hours.on(5, 6)
+        job.day.on(4)
+        job.months.on(2)
+        self.assertEqual(unicode(job), '*/4 5,6 4 2 * plural')
+
+    def test_14_valid(self):
+        """Valid and Invalid"""
+        job = self.crontab.new(command='valid')
+        job.minute.every(2)
+        job.valid = False
+        with self.assertRaises(ValueError):
+            unicode(job)
+        # Disabled jobs still work
+        job.enabled = False
+        unicode(job)
+
+        job.enabled = True
+        with self.assertRaises(ValueError):
+            self.crontab.render(errors=True)
+        self.crontab.render(errors=False)
+        self.assertFalse(job.enabled)
+
+    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), '<CronSlice \'*/6\'>')
+
+        self.assertEqual(repr(dow.parse_value('a')), 'a')
+        with self.assertRaises(ValueError):
+            dow.parse_value('b')
+
+        self.assertEqual(dow.get_range()[0].render(), '*')
+        with self.assertRaises(ValueError):
+            dow.get_range('%')
+
+    def test_17_slice_id(self):
+        """Single slice by Id"""
+        self.assertEqual(CronSlice(1).max, 23)
+
+    def test_18_range_cmp(self):
+        """Compare ranges"""
+        dow = CronSlice({'max': 5, 'min': 0})
+        three = dow.get_range(2, 4)[0]
+        self.assertGreater(three, 2)
+        self.assertLess(three, 4)
+        self.assertEqual(str(three), '2-4')
+
+    def test_18_find(self):
+        """Find a command and comments by name"""
+        cmds = list(self.crontab.find_command('byweek'))
+        self.assertEqual(len(cmds), 1)
+        self.assertEqual(cmds[0].comment, 'Comment One')
+
+        cmds = list(self.crontab.find_command(re.compile(r'stc\w')))
+        self.assertEqual(len(cmds), 1)
+        self.assertEqual(cmds[0].command, 'firstcommand')
+
+        cmds = list(self.crontab.find_command('Comment One'))
+        self.assertEqual(len(cmds), 0)
+
+        cmds = list(self.crontab.find_comment('Comment One'))
+        self.assertEqual(len(cmds), 1)
+        self.assertEqual(cmds[0].command, 'byweek')
+
+        cmds = list(self.crontab.find_comment(re.compile(r'om+en\w O')))
+        self.assertEqual(len(cmds), 1)
+        self.assertEqual(cmds[0].comment, 'Comment One')
+
+        cmds = list(self.crontab.find_comment(re.compile(r'stc\w')))
+        self.assertEqual(len(cmds), 0)
+
+    def test_20_write(self):
+        """Write CronTab to file"""
+        self.crontab.write('output.tab')
+        self.assertTrue(os.path.exists('output.tab'))
+        os.unlink('output.tab')
+
+    def test_21_multiuse(self):
+        """Multiple Renderings"""
+        cron = '# start of tab\n'
+        for i in range(10):
+            crontab = CronTab(tab=cron)
+            p = list(crontab.new(command='multi%d' % i))
+            cron = unicode(crontab)
+            crontab = CronTab(tab=cron)
+            list(crontab.find_command('multi%d' % i))[0].delete()
+            cron = unicode(crontab)
+        self.assertEqual(unicode(crontab), '# start of tab\n')
+
+    def test_22_min(self):
+        """Minimum Field Values"""
+        job = self.crontab.new(command='min')
+        job.minute.on('<')
+        job.hour.on('<')
+        job.dom.on('<')
+        job.month.on('<')
+        self.assertEqual(unicode(job), '@yearly min')
+
+    def test_23_max(self):
+        """Maximum Field Values"""
+        job = self.crontab.new(command='max')
+        job.minute.on('>')
+        job.hour.on('>')
+        job.dom.on('>')
+        job.month.on('>')
+        self.assertEqual(unicode(job), '59 23 31 12 * max')
+
+    def test_24_special_r(self):
+        """Special formats are read and written"""
+        tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials_enc.tab'))
+        self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@midnight midnight\n@weekly weekly\n@reboot reboot\n""")
+        self.assertEqual(len(list(tab)), 5)
+
+    def test_24_special_d(self):
+        """Removal All Specials"""
+        tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab'))
+        tab.remove_all()
+        self.assertEqual(len(list(tab)), 0)
+
+    def test_24_special_w(self):
+        """Write Specials"""
+        tab = CronTab(tabfile=os.path.join(TEST_DIR, 'data', 'specials.tab'))
+        self.assertEqual(tab.render(), """@hourly hourly\n@daily daily\n@weekly weekly\n""")
+        self.assertEqual(len(list(tab)), 3)
+
+    def test_25_setall(self):
+        """Set all values at once"""
+        job = self.crontab.new(command='all')
+        job.setall(1, '*/2', '2-4', '>', 'SUN')
+        self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all')
+        job.setall('*/2')
+        self.assertEqual(unicode(job), '*/2 * * * * all')
+        job.setall('1 */2 2-4 12 SUN')
+        self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all')
+        job.setall(['*'])
+        self.assertEqual(unicode(job), '* * * * * all')
+
+    def test_26_setall_obj(self):
+        """Copy all values"""
+        job = self.crontab.new(command='all')
+        job2 = self.crontab.new(command='ignore')
+        job2.setall("1 */2 2-4 12 SUN")
+        job.setall(job2)
+        self.assertEqual(unicode(job), '1 */2 2-4 12 SUN all')
+        job2.setall("2 */3 4-8 10 MON")
+        job.setall(job2.slices)
+        self.assertEqual(unicode(job), '2 */3 4-8 10 MON all')
+
+    def test_27_commands(self):
+        """Get all commands"""
+        self.assertEqual(list(self.crontab.commands),
+                         [u'firstcommand', u'range', u'byweek', u'disabled',
+                          u'spaced', u'python /example_app/testing.py',
+                          u'rebooted'])
+
+    def test_28_comments(self):
+        """Get all comments"""
+        self.assertEqual(list(self.crontab.comments),
+                         ['Comment One', 'Comment  Two', 'monitorCron', 're-id'])
+
+    def test_29_set_lines(self):
+        """Set crontab lines directly"""
+        cron = CronTab(tab='')
+        cron.lines = [
+            str(self.crontab.lines[5]),
+        ]
+        self.assertEqual(str(cron), '\n*/30 * * * * firstcommand\n')
+
+        with self.assertRaises(AttributeError):
+            cron.crons = ['A']
+
+
+if __name__ == '__main__':
+    test_support.run_unittest(InteractionTestCase)

+ 161 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_log.py

@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+#
+# Copyright (C) YEAR Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test the cron log extention with a test syslog example data file.
+"""
+
+import os
+import sys
+from datetime import datetime, date
+
+import unittest
+from crontab import CronTab
+from cronlog import CronLog, LogReader
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+INITAL_TAB = """
+* * * * * userscript &> /dev/null
+* * * * * rootscript &> /dev/null
+* * * * * shadowscript &> /dev/null
+"""
+
+ROOT_PIDS = ['16592', '16574', '16552', '16522', '16514', '16489']
+SHAD_PIDS = ['16551', '16497']
+USER_PIDS = ['16591', '16588', '16573', '16569', '16554', '16539', '16523',\
+            '16519','16513','16496','16490']
+
+YEAR = date.today().year
+
+ROOT_DATES = [
+    datetime(YEAR, 4, 4, 21, 34, 1),
+    datetime(YEAR, 4, 4, 21, 32, 1),
+    datetime(YEAR, 4, 4, 21, 30, 1),
+    datetime(YEAR, 4, 4, 21, 28, 1),
+    datetime(YEAR, 4, 4, 21, 26, 1),
+    datetime(YEAR, 4, 4, 21, 24, 1),
+  ]
+SHAD_DATES = [
+    datetime(YEAR, 4, 4, 21, 30, 1),
+    datetime(YEAR, 4, 4, 21, 25, 1),
+  ]
+USER_DATES = [
+    datetime(YEAR, 4, 4, 21, 34, 1),
+    datetime(YEAR, 4, 4, 21, 33, 2),
+    datetime(YEAR, 4, 4, 21, 32, 1),
+    datetime(YEAR, 4, 4, 21, 31, 1),
+    datetime(YEAR, 4, 4, 21, 30, 1),
+    datetime(YEAR, 4, 4, 21, 29, 1),
+    datetime(YEAR, 4, 4, 21, 28, 1),
+    datetime(YEAR, 4, 4, 21, 27, 1),
+    datetime(YEAR, 4, 4, 21, 26, 1),
+    datetime(YEAR, 4, 4, 21, 25, 1),
+    datetime(YEAR, 4, 4, 21, 24, 1),
+  ]
+LONG_LINE = 'A really long line which can test log lines ability to put two bits together'
+READ_LINE = [ 'The End', 'Sickem', '2', '9', 'First Line', LONG_LINE ]
+
+class BasicTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.log = os.path.join(TEST_DIR, 'data', 'test.log')
+        self.crontab = CronTab(tab=INITAL_TAB, log=self.log)
+
+    def test_00_logreader(self):
+        """Log Reader"""
+        lines = READ_LINE[:]
+        reader = LogReader(os.path.join(TEST_DIR, 'data', 'basic.log'), mass=50)
+        for line in reader:
+            self.assertEqual(line.strip(), lines.pop(0))
+
+    def test_01_cronreader(self):
+        """Cron Log Lines"""
+        with self.crontab.log as log:
+            lines = list(log.readlines())
+        self.assertEqual(len(lines), 32)
+        self.assertEqual(lines[0][1], "Apr  4 21:34:01 servername CRON[16592]"
+                                      ": (root) CMD (rootscript &> /dev/null)")
+        self.assertEqual(lines[15][1], "Apr  4 21:28:31 servername NOTCRON"
+                                      ": that these are ignored")
+        self.assertEqual(lines[-1][1], "Apr  4 21:24:01 servername CRON[16490]"
+                                      ": (user) CMD (userscript &> /dev/null)")
+
+    def test_01_iter(self):
+        """Iterate directly over log"""
+        with self.crontab.log as log:
+            self.assertEqual(len(list(log)), 8)
+
+    def test_02_cronlog(self):
+        """Cron Log Items"""
+        entries = list(CronLog(os.path.join(TEST_DIR, 'data', 'test.log')))
+        self.assertEqual(len(entries), 19)
+        self.assertEqual(entries[0]['pid'], "16592")
+        self.assertEqual(entries[3]['pid'], "16574")
+        self.assertEqual(entries[-1]['pid'], "16490")
+
+    def test_03_crontab(self):
+        """Cron Tab Items"""
+        entries = list(self.crontab.log)
+        self.assertEqual(len(entries), 8)
+        self.assertEqual(entries[0]['pid'], "16592")
+        self.assertEqual(entries[3]['pid'], "16551")
+        self.assertEqual(entries[-1]['pid'], "16489")
+
+    def test_04_root(self):
+        """Cron Job Items"""
+        pids, dates = ROOT_PIDS[:], ROOT_DATES[:]
+        job = list(self.crontab.find_command('rootscript'))[0]
+        for log in job.log:
+            self.assertEqual(log['pid'], pids.pop(0))
+            self.assertEqual(log['date'], dates.pop(0))
+        self.assertEqual(pids, [])
+
+    def tst_05_shadow(self):
+        """Seperate Job Items"""
+        pids, dates = SHAD_PIDS[:], SHAD_DATES[:]
+        job = self.crontab.find_command('shadowscript')[0]
+        for log in job.log:
+            self.assertEqual(log['pid'], pids.pop(0))
+            self.assertEqual(log['date'], dates.pop(0))
+        self.assertEqual(pids, [])
+
+    def tst_06_user(self):
+        """Seperate User Crontab"""
+        pids, dates = USER_PIDS[:], User_DATES[:]
+        self.crontab.user = 'user'
+        job = self.crontab.find_command('userscript')[0]
+        for log in job.log:
+            self.assertEqual(log['pid'], pids.pop(0))
+            self.assertEqual(log['date'], dates.pop(0))
+        self.assertEqual(pids, [])
+
+    def test_08_readerror(self):
+        """Cron Log Error"""
+        self.crontab.log.pipe = False
+        with self.assertRaises(IOError):
+            list(self.crontab.log.readlines())
+
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       BasicTestCase,
+    )

+ 136 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_range.py

@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab ranges.
+"""
+import unittest
+from crontab import CronTab, CronSlice, PY3
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+from .utils import LoggingMixin
+
+if PY3:
+    unicode = str #pylint: disable=redefined-builtin
+
+class RangeTestCase(LoggingMixin, unittest.TestCase):
+    """Test basic functionality of crontab."""
+    log_name = 'crontab'
+
+    def setUp(self):
+        super(RangeTestCase, self).setUp()
+        self.crontab = CronTab(tab="")
+
+    def test_01_atevery(self):
+        """At Every"""
+        tab = CronTab(tab="""
+*  *  *  *  * command
+61 *  *  *  * command
+*  25 *  *  * command
+*  *  32 *  * command
+*  *  *  13 * command
+*  *  *  *  8 command
+        """)
+        self.assertEqual(len(tab), 1)
+        self.assertLog("error", "'61', not in 0-59 for Minutes")
+        self.assertLog("error", "'25', not in 0-23 for Hours")
+        self.assertLog("error", "'32', not in 1-31 for Day of Month")
+        self.assertLog("error", "'13', not in 1-12 for Month")
+        self.assertLog("error", "'8', not in 0-6 for Day of Week")
+
+    def test_02_withinevery(self):
+        """Within Every"""
+        tab = CronTab(tab="""
+*    *    *    *    * command
+1-61 *    *    *    * command
+*    1-25 *    *    * command
+*    *    1-32 *    * command
+*    *    *    1-13 * command
+*    *    *    *    1-8 command
+        """)
+        self.assertEqual(len(tab), 1)
+        self.assertLog("error", "'61', not in 0-59 for Minutes")
+        self.assertLog("error", "'25', not in 0-23 for Hours")
+        self.assertLog("error", "'32', not in 1-31 for Day of Month")
+        self.assertLog("error", "'13', not in 1-12 for Month")
+        self.assertLog("error", "'8', not in 0-6 for Day of Week")
+
+    def test_03_outevery(self):
+        """Out of Every"""
+        tab = CronTab(tab="""
+*    *    *    *    *   command
+*/61 *    *    *    *   command
+*    */25 *    *    *   command
+*    *    */32 *    *   command
+*    *    *    */13 *   command
+*    *    *    *    */8 command
+        """)
+        self.assertEqual(len(tab), 1)
+        self.assertLog("error", "'61', not in 0-59 for Minutes")
+        self.assertLog("error", "'25', not in 0-23 for Hours")
+        self.assertLog("error", "'32', not in 1-31 for Day of Month")
+        self.assertLog("error", "'13', not in 1-12 for Month")
+        self.assertLog("error", "'8', not in 0-6 for Day of Week")
+
+    def test_03_inevery(self):
+        """Inside of Every"""
+        tab = CronTab(tab="""
+*    *    *    *    *   command
+*/59 *    *    *    *   command
+*    */23 *    *    *   command
+*    *    */30 *    *   command
+*    *    *    */11 *   command
+*    *    *    *    */7 command
+        """)
+        self.assertEqual(len(tab), 6, str(tab))
+
+    def test_04_zero_seq(self):
+        """Zero divisor in range"""
+        tab = CronTab(tab="""
+*/0 * * * * command
+        """)
+        self.assertEqual(len(tab), 0)
+        self.assertLog("error", "Sequence can not be divided by zero or max")
+
+    def test_14_invalid_range(self):
+        """No numerator in range"""
+        tab = CronTab(tab="/10 * * * * command")
+        self.assertEqual(len(tab), 0)
+        with self.assertRaises(ValueError):
+            tab.render(errors=True)
+        self.assertEqual(unicode(tab), "# DISABLED LINE\n# /10 * * * * command\n")
+        self.assertLog('error', u"No enumeration for Minutes: '/10'")
+
+    def test_05_sunday(self):
+        """Test all possible day of week combinations"""
+        for (a, b) in (\
+              ("7", "0"), ("5-7", "0,5-6"), ("1-7", "*"), ("*/7", "0"),\
+              ("0-6", "*"), ("2-7", "0,2-6"), ("1-5", "1-5"), ("0-5", "0-5")):
+            v = str(CronSlice(4, a))
+            self.assertEqual(v, b, "%s != %s, from %s" % (v, b, a))
+
+    def test_06_backwards(self):
+        """Test backwards ranges for error"""
+        tab = CronTab(tab="* * * * 3-1 command")
+        self.assertEqual(str(tab), "* * * * 1-3 command\n")
+        self.assertLog("warning", "Bad range '3-1'")
+
+if __name__ == '__main__':
+    test_support.run_unittest(RangeTestCase)

+ 154 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_removal.py

@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test cron item removal
+"""
+
+import os
+import sys
+
+sys.path.insert(0, '../')
+
+import unittest
+from crontab import CronTab, PY3
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+if PY3:
+    unicode = str
+
+START_TAB = """
+3 * * * * command1 # CommentID C
+2 * * * * command2 # CommentID AAB
+1 * * * * command3 # CommentID B3
+"""
+
+class RemovalTestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.filenames = []
+        self.crontab = CronTab(tab=START_TAB.strip())
+
+    def test_01_remove(self):
+        """Remove Item"""
+        self.assertEqual(len(self.crontab), 3)
+        self.crontab.remove(self.crontab.crons[0])
+        self.assertEqual(len(self.crontab), 2)
+        self.assertEqual(len(self.crontab.render()), 69)
+
+    def test_02_remove_all(self):
+        """Remove All"""
+        self.crontab.remove_all()
+        self.assertEqual(len(self.crontab), 0)
+        self.assertEqual(unicode(self.crontab), '')
+
+    def test_03_remove_cmd(self):
+        """Remove all with Command"""
+        self.crontab.remove_all(command='command2')
+        self.assertEqual(len(self.crontab), 2)
+        self.assertEqual(len(self.crontab.render()), 67)
+        self.crontab.remove_all(command='command3')
+        self.assertEqual(len(self.crontab), 1)
+        self.assertEqual(len(self.crontab.render()), 33)
+
+    def test_04_remove_id(self):
+        """Remove all with Comment/ID"""
+        self.crontab.remove_all(comment='CommentID B3')
+        self.assertEqual(len(self.crontab), 2)
+        self.assertEqual(len(self.crontab.render()), 68)
+
+    def test_05_remove_date(self):
+        """Remove all with Time Code"""
+        self.crontab.remove_all(time='2 * * * *')
+        self.assertEqual(len(self.crontab), 2)
+        self.assertEqual(len(self.crontab.render()), 67)
+
+    def test_05_remove_all_error(self):
+        """Remove all with old arg"""
+        with self.assertRaises(AttributeError):
+            self.crontab.remove_all('command')
+
+    def test_06_removal_of_none(self):
+        """Remove all respects None as a possible value"""
+        self.crontab[1].set_comment(None)
+        self.crontab.remove_all(comment=None)
+        self.assertEqual(len(self.crontab), 2)
+        self.assertEqual(len(self.crontab.render()), 67)
+
+    def test_07_removal_in_loop(self):
+        """Remove items in a loop"""
+        for job in self.crontab:
+            self.crontab.remove(job)
+        self.assertEqual(len(self.crontab), 0)
+
+    def test_08_removal_write_loop(self):
+        """Remove items in a loop and save each time"""
+        filename = self.get_new_file('removal')
+        crontab = CronTab()
+        for i in range(10):
+            crontab.new('test', comment=str(i))
+            crontab.write(filename)
+            crontab = CronTab(tabfile=filename)
+            self.assertEqual(len(crontab), 1)
+            self.assertNotEqual(str(crontab), '')
+
+            crontab.remove_all(comment=str(i))
+            crontab.write(filename)
+            crontab = CronTab(tabfile=filename)
+            self.assertEqual(len(crontab), 0)
+            self.assertEqual(str(crontab), '')
+
+    def test_09_removal_during_iter(self):
+        crontab = CronTab()
+        for x in range(0, 5, 1):
+            job = crontab.new(command="cmd", comment="SAME_ID")
+            job.setall("%d * * * *" % (x + 1))
+        for item in crontab.find_comment("SAME_ID"):
+            crontab.remove(item)
+        self.assertEqual(len(crontab), 0)
+
+    def test_11_remove_generator(self):
+        """Remove jobs from the find generator"""
+        tabs = self.crontab.find_command('command2')
+        self.crontab.remove(tabs)
+        self.assertEqual(len(self.crontab), 2)
+
+    def test_12_remove_nonsense(self):
+        """Fail to remove bad type"""
+        self.assertRaises(TypeError, self.crontab.remove, 5)
+        self.assertRaises(TypeError, self.crontab.remove, "foo")
+
+    def get_new_file(self, name):
+        """Gets a filename and records it for deletion"""
+        this_dir = os.path.dirname(__file__)
+        filename = os.path.join(this_dir, 'data', 'spool', name)
+        self.filenames.append(filename)
+        return filename
+
+    def tearDown(self):
+        for filename in self.filenames:
+            if os.path.isfile(filename):
+                os.unlink(filename)
+
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       RemovalTestCase,
+    )

+ 113 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_scheduler.py

@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test any internal scheduling
+"""
+
+import os
+import sys
+
+import unittest
+import crontab
+import logging
+import string
+import random
+
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
+
+logger = logging.getLogger()
+logger.level = logging.WARNING
+
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+COMMAND = os.path.join(TEST_DIR, 'data', 'crontest ')
+
+class SchedulerTestCase(unittest.TestCase):
+    """Test scheduling functions of CronTab."""
+    def setUp(self):
+        self.tab = crontab.CronTab()
+        try:
+            import croniter
+        except ImportError:
+            self.skipTest("Croniter not installed")
+
+        self.handlers = []
+        self.log = crontab.LOG
+        self.log.setLevel(logging.DEBUG)
+        self.log.propagate = False
+        for handler in self.log.handlers:
+            self.handlers.append(handler)
+            self.log.removeHandler(handler)
+
+        self.err = StringIO()
+        self.handler = logging.StreamHandler(self.err)
+        self.log.addHandler(self.handler)
+
+    def tearDown(self):
+        logger.removeHandler(self.handler)
+        for handler in self.handlers:
+            logger.addHandler(handler)
+        #self.err.close()
+
+    def assertLog(self, text):
+        self.handler.flush()
+        self.assertEqual(self.err.getvalue().strip(), text)
+
+    def assertSchedule(self, slices, count, result):
+        uid = random.choice(string.ascii_letters)
+        self.tab.new(command=COMMAND + '-h ' + uid).setall(slices)
+        ret = list(self.tab.run_scheduler(count, cadence=0.01, warp=True))
+        self.assertEqual(len(ret), result)
+        if count > 0:
+            self.assertEqual(ret[0], '-h|' + uid)
+
+    def test_01_run(self):
+        """Run the command"""
+        self.tab.env['SHELL'] = crontab.SHELL
+        ret = self.tab.new(command=COMMAND+'-h A').run()
+        self.assertEqual(ret, '-h|A')
+
+    def test_02_run_error(self):
+        """Run with errors"""
+        ret = self.tab.new(command=COMMAND+'-e B').run()
+        self.assertEqual(ret, '')
+        self.assertLog('-e|B')
+
+    def test_03_schedule(self):
+        """Simple Schedule"""
+        self.assertSchedule("* * * * *", 5, 4)
+
+    def test_04_schedule_ten(self):
+        """Every Ten Minutes"""
+        self.assertSchedule("*/10 * * * *", 12, 1)
+
+    def test_05_env_passed(self):
+        """Environment is passed in"""
+        self.tab.env['CR_VAR'] = 'BABARIAN'
+        ret = self.tab.new(command=COMMAND+' -ev').run()
+        self.assertEqual(ret, 'BABARIAN')
+
+if __name__ == '__main__':
+    test_support.run_unittest(SchedulerTestCase)

+ 122 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_system_cron.py

@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2015 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+System cron is prefixed with the username the process should run under.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, '../')
+
+import unittest
+from crontab import CronTab
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_FILE = os.path.join(os.path.dirname(__file__), 'data', 'crontab')
+INITIAL_TAB = """
+*/30 * * * * palin one_cross_each
+*/30 * * * * matias\ttab_user_error
+"""
+
+class SystemCronTestCase(unittest.TestCase):
+    """Test vixie cron user addition."""
+    def setUp(self):
+        self.crontab = CronTab(tab=INITIAL_TAB, user=False)
+
+    def test_00_repr(self):
+        """System crontab repr"""
+        self.assertEqual(repr(self.crontab), "<Unattached System 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), "<System CronTab '%s'>" % TEST_FILE)
+
+if __name__ == '__main__':
+    test_support.run_unittest(SystemCronTestCase)

+ 215 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_usage.py

@@ -0,0 +1,215 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2012 Jay Sigbrandt <jsigbrandt@slb.com>
+#                    Martin Owens <doctormo@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab usage.
+"""
+
+import os
+import sys
+
+import unittest
+import crontab
+
+from datetime import date, time, datetime, timedelta
+
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+crontab.LOG.setLevel(crontab.logging.ERROR)
+TEST_DIR = os.path.dirname(__file__)
+
+class DummyStdout(object):
+    def write(self, text):
+        pass
+
+BASIC = '@hourly firstcommand\n\n'
+USER = '\n*/4 * * * * user_command # user_comment\n\n\n'
+crontab.CRONCMD = "%s %s" % (sys.executable, os.path.join(TEST_DIR, 'data', 'crontest'))
+
+def flush():
+    pass
+
+class Attribute(object):
+    def __init__(self, obj, attr, value):
+        self.obj = obj
+        self.attr = attr
+        self.value = value
+
+    def __enter__(self, *args, **kw):
+        if hasattr(self.obj, self.attr):
+            self.previous = getattr(self.obj, self.attr)
+        setattr(self.obj, self.attr, self.value)
+
+    def __exit__(self, *args, **kw):
+        if hasattr(self, 'previous'):
+            setattr(self.obj, self.attr, self.previous)
+        else:
+            delattr(self.obj, self.attr)
+
+
+class UseTestCase(unittest.TestCase):
+    """Test use documentation in crontab."""
+    def setUp(self):
+        self.filenames = []
+
+    def test_01_empty(self):
+        """Open system crontab"""
+        cron = crontab.CronTab()
+        self.assertEqual(cron.render(), "")
+        self.assertEqual(cron.__unicode__(), "")
+        self.assertEqual(repr(cron), "<Unattached CronTab>")
+
+    def test_02_user(self):
+        """Open a user's crontab"""
+        cron = crontab.CronTab(user='basic')
+        self.assertEqual(cron.render(), BASIC)
+        self.assertEqual(repr(cron), "<User CronTab 'basic'>")
+
+    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), "<My CronTab>")
+
+    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 use API"""
+        cron = crontab.CronTab(tab=USER)
+        self.assertEqual(repr(cron), "<Unattached CronTab>")
+        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), "<User CronTab 'bob'>")
+
+    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.command = str('nothing')
+        self.assertEqual(item.render(), '* * * * * nothing')
+
+    def test_10_time_object(self):
+        """Set slices using time object"""
+        item = crontab.CronItem(command='cmd')
+        self.assertEqual(str(item.slices), '* * * * *')
+        item.setall(time(1, 2))
+        self.assertEqual(str(item.slices), '2 1 * * *')
+        self.assertTrue(item.is_valid())
+        item.setall(time(0, 30, 0, 0))
+        self.assertEqual(str(item.slices), '30 0 * * *')
+        self.assertTrue(item.is_valid())
+        self.assertEqual(str(item), '30 0 * * * cmd')
+
+    def test_11_date_object(self):
+        """Set slices using date object"""
+        item = crontab.CronItem(command='cmd')
+        self.assertEqual(str(item.slices), '* * * * *')
+        item.setall(date(2010, 6, 7))
+        self.assertEqual(str(item.slices), '0 0 7 6 *')
+        self.assertTrue(item.is_valid())
+
+    def test_12_datetime_object(self):
+        """Set slices using datetime object"""
+        item = crontab.CronItem(command='cmd')
+        self.assertEqual(str(item.slices), '* * * * *')
+        item.setall(datetime(2009, 8, 9, 3, 4))
+        self.assertTrue(item.is_valid())
+        self.assertEqual(str(item.slices), '4 3 9 8 *')
+
+    def test_20_slice_validation(self):
+        """CronSlices class and objects can validate"""
+        CronSlices = crontab.CronSlices
+        self.assertTrue(CronSlices('* * * * *').is_valid())
+        self.assertTrue(CronSlices.is_valid('* * * * *'))
+        self.assertTrue(CronSlices.is_valid('*/2 * * * *'))
+        self.assertTrue(CronSlices.is_valid('* 1,2 * * *'))
+        self.assertTrue(CronSlices.is_valid('* * 1-5 * *'))
+        self.assertTrue(CronSlices.is_valid('* * * * MON-WED'))
+        self.assertTrue(CronSlices.is_valid('@reboot'))
+
+        sliced = CronSlices('* * * * *')
+        sliced[0].parts = [300]
+        self.assertEqual(str(sliced), '300 * * * *')
+        self.assertFalse(sliced.is_valid())
+        self.assertFalse(CronSlices.is_valid('P'))
+        self.assertFalse(CronSlices.is_valid('*/61 * * * *'))
+        self.assertFalse(CronSlices.is_valid('* 1,300 * * *'))
+        self.assertFalse(CronSlices.is_valid('* * 50-1 * *'))
+        self.assertFalse(CronSlices.is_valid('* * * * FRO-TOO'))
+        self.assertFalse(CronSlices.is_valid('@retool'))
+
+    def test_25_open_pipe(self):
+        """Test opening pipes"""
+        from crontab import open_pipe, CRONCMD
+        pipe = open_pipe(CRONCMD, h=None, a='one', abc='two')
+        (out, err) = pipe.communicate()
+        self.assertEqual(err, b'')
+        self.assertEqual(out, b'--abc=two|-a|-h|one\n')
+
+    def test_07_zero_padding(self):
+        """Can we get zero padded output"""
+        cron = crontab.CronTab(tab="02 3-5 2,4 */2 01 cmd")
+        self.assertEqual(str(cron), '2 3-5 2,4 */2 1 cmd\n')
+        with Attribute(crontab, 'ZERO_PAD', True):
+            self.assertEqual(str(cron), '02 03-05 02,04 */2 01 cmd\n')
+
+    def tearDown(self):
+        for filename in self.filenames:
+            if os.path.exists(filename):
+                os.unlink(filename)
+
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       UseTestCase,
+    )

+ 116 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/test_utf8.py

@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 Martin Owens
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.
+#
+"""
+Test crontab use of UTF-8 filenames and strings
+"""
+
+import os
+import sys
+
+import locale
+import unittest
+from crontab import CronTab, PY3
+try:
+    from test import test_support
+except ImportError:
+    from test import support as test_support
+
+TEST_DIR = os.path.dirname(__file__)
+
+if PY3:
+    unicode = str
+
+content = """
+*/4 * * * * ůțƒ_command # ůțƒ_comment
+"""
+filename = os.path.join(TEST_DIR, 'data', 'output-ůțƒ-8.tab')
+
+class Utf8TestCase(unittest.TestCase):
+    """Test basic functionality of crontab."""
+    def setUp(self):
+        self.crontab = CronTab(tab=content)
+
+    def test_01_input(self):
+        """Read UTF-8 contents"""
+        self.assertTrue(self.crontab)
+
+    def test_02_write(self):
+        """Write/Read UTF-8 Filename"""
+        self.assertEqual(locale.getpreferredencoding(), 'UTF-8')
+        self.crontab.write(filename)
+        crontab = CronTab(tabfile=filename)
+        self.assertTrue(crontab)
+        with open(filename, "r") as fhl:
+            self.assertEqual(content, fhl.read())
+        os.unlink(filename)
+
+    def test_04_command(self):
+        """Read Command String"""
+        self.assertEqual(self.crontab[0].command, u"ůțƒ_command")
+
+    def test_05_comment(self):
+        """Read Comment String"""
+        self.assertEqual(self.crontab[0].comment, u'ůțƒ_comment')
+
+    def test_06_unicode(self):
+        """Write New via Unicode"""
+        job = self.crontab.new(command=u"ůțƒ_command", comment=u'ůțƒ_comment')
+        self.assertEqual(job.command, u"ůțƒ_command")
+        self.assertEqual(job.comment, u"ůțƒ_comment")
+        self.crontab.render()
+
+    def test_07_utf8(self):
+        """Write New via UTF-8"""
+        job = self.crontab.new(command=b'\xc5\xaf\xc8\x9b\xc6\x92_command',
+                               comment=b'\xc5\xaf\xc8\x9b\xc6\x92_comment')
+        self.assertEqual(self.crontab.render(), u"""
+*/4 * * * * ůțƒ_command # ůțƒ_comment
+
+* * * * * ůțƒ_command # ůțƒ_comment
+""")
+        self.assertEqual(type(job.command), unicode)
+        self.assertEqual(type(job.comment), unicode)
+
+    def test_08_utf8_str(self):
+        """Test UTF8 (non unicode) strings"""
+        self.crontab[0].command = '£12'
+        self.crontab[0].comment = '𝗔𝗕𝗖𝗗'
+        self.assertEqual(self.crontab.render(), u"""
+*/4 * * * * £12 # 𝗔𝗕𝗖𝗗
+""")
+
+    def test_09_utf8_again(self):
+        """Test Extra UTF8 input"""
+        filename = os.path.join(TEST_DIR, 'data', 'utf_extra')
+        with open(filename + '.in', 'w') as fhl:
+            fhl.write('# 中文\n30 2 * * * source /etc/profile\n30 1 * * * source /etc/profile')
+
+        try:
+            cron = CronTab(tabfile=filename + '.in')
+        finally:
+            os.unlink(filename + '.in')
+
+        cron.write(filename + '.out')
+        if os.path.isfile(filename + '.out'):
+            os.unlink(filename + '.out')
+
+if __name__ == '__main__':
+    test_support.run_unittest(
+       Utf8TestCase,
+    )

+ 81 - 0
desktop/core/ext-py/python-crontab-2.3.6/tests/utils.py

@@ -0,0 +1,81 @@
+#
+# 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])