[
  {
    "path": "CONTRIBUTORS.txt",
    "content": "Rob deCarvalho (unlisted@unlisted.net)\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Rob deCarvalho\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude CONTRIBUTORS.txt\ninclude LICENSE\nprune */tests\n"
  },
  {
    "path": "README.md",
    "content": "# Crontabs\n\n---\n\n\n**NOTE:** \nI've recently discovered the [Rocketry](https://github.com/Miksus/rocketry/) and  [Huey](https://github.com/coleifer/huey) projects, which you should probably use instead of crontabs.  They are just better than crontabs.\n\n\n---\n\n\nThink of crontabs as a quick-and-dirty solution you can throw into one-off python scripts to execute tasks on a cron-like schedule.\n\nCrontabs is a small pure-python library that was inspired by the excellent [schedule](https://github.com/dbader/schedule) library for python.\n\nIn addition to having a slightly different API, crontabs differs from the schedule module in the following\nways.\n\n  * You do not need to provide your own event loop.\n  * Job timing is guaranteed not to drift over time.  For example, if you specify to run a job every five minutes,\n    you can rest assured that it will always run at 5, 10, 15, etc. passed the hour with no drift.\n  * The python functions are all run in child processes.  A memory-friendly flag is available to run each\n    iteration of your task in its own process thereby mitigating memory problems due to Python's\n    [high watermark issue](https://hbfs.wordpress.com/2013/01/08/python-memory-management-part-ii/)\n\n# Why Crontabs\nPython has no shortage of [cron-like job scheduling libraries](https://pypi.python.org/pypi?%3Aaction=search&term=cron), so why create yet another.  The honest answer is that I couldn't find one that met a simple list of criteria.\n* **Simple installation with no configuration.** An extremely robust and scalable solution to this problem already exists.  [Celery](http://www.celeryproject.org/). But for quick and dirty work, I didn't want the hastle of setting up and configuring a broker, which celery requires to do its magic.  For simple jobs, I just wanted to pip install and go.\n* **Human readable interface.**  I loved the interface provided by the [schedule](https://github.com/dbader/schedule) library and wanted something similarly intuitive to use.\n* **Memory safe for long running jobs.** Celery workers can suffer from severe memory bloat due to the way Python manages memory.  As of 2017, the recommended solution for this was to periodically restart the workers.  Crontabs runs each job in a subprocess.  It can optionally also run each iteration of a task in it's own process thereby mitigating the memory bloat issue.\n* **Simple solution for cron-style workflow and nothing more.**  I was only interested in supporting cron-like functionality, and wasn't interested in all the other capabilities and guarantees offered by a real task-queue solution like celery.\n* **Suggestions for improvement welcome.** If you encounter a bug or have an improvement that remains within the scope listed above, please feel free to open an issue (or even better... a PR).\n\n# Installation\n```bash\npip install crontabs\n```\n# Usage\n\n### Schedule a single job\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\n\ndef my_job(*args, **kwargs):\n    print('args={} kwargs={} running at {}'.format(args, kwargs, datetime.now()))\n\n\n# Will run with a 5 second interval synced to the top of the minute\nCron().schedule(\n    Tab(name='run_my_job').every(seconds=5).run(my_job, 'my_arg', my_kwarg='hello')\n).go()\n\n```\n\n### Schedule multiple jobs\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\n\ndef my_job(*args, **kwargs):\n    print('args={} kwargs={} running at {}'.format(args, kwargs, datetime.now()))\n\n\n# All logging messages are sent to sdtout\nCron().schedule(\n    # Turn off logging for job that runs every five seconds\n    Tab(name='my_fast_job', verbose=False).every(seconds=5).run(my_job, 'fast', seconds=5),\n\n    # Go ahead and let this job emit logging messages\n    Tab(name='my_slow_job').every(seconds=20).run(my_job, 'slow', seconds=20),\n).go()\n\n```\n\n### Schedule future job to run repeatedly for a fixed amount of time\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\n\ndef my_job(*args, **kwargs):\n    print('args={} kwargs={} running at {}'.format(args, kwargs, datetime.now()))\n\n\nCron().schedule(\n    Tab(\n        name='future_job'\n    ).every(\n        seconds=5\n    ).starting(\n        '12/27/2017 16:45'  # This argument can either be parsable text or datetime object.\n    ).run(\n        my_job, 'fast', seconds=5\n    )\n# max_seconds starts from the moment go is called.  Pad for future run times accordingly.\n).go(max_seconds=60)\n```\n\n# Cron API\nThe `Cron` class has a very small api\n\n| method | Description |\n| --- | --- |\n| `.schedule()` |[**Required**] Specify the different jobs you want using `Tab` instances|\n| `.go()` | [**Required**] Start the crontab manager to run all specified tasks|\n| `.get_logger()` | A class method you can use to get an instance of the crontab logger|\n\n# Tab API with examples\nThe api for the `Tab` class is designed to be composable and readable in plain English.  It supports\nthe following \"verbs\" by invoking methods.\n\n| method | Description |\n| --- | --- |\n| `.run()` |[**Required**] Specify the function to run. |\n| `.every()` |[**Required**] Specify the interval between function calls.|\n| `.starting()` | [**Optional**] Specify an explicit time for the function calls to begin.|\n| `.lasting()` | [**Optional**] Specify how long the task will continue being iterated.|\n| `.until()` | [**Optional**] Specify an explicit time past which the iteration will stop\n| `.during()` | [**Optional**] Specify time conditions under which the function will run\n| `.excluding()` | [**Optional**] Specify time conditions under which the function will be inhibited\n\n## Run a job indefinitely\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\n\ndef my_job(name):\n    print('Running function with name={}'.format(name))\n\n\nCron().schedule(\n    Tab(name='forever').every(seconds=5).run(my_job, 'my_func'),\n).go()\n\n```\n\n## Run one job indefinitely, another for thirty seconds, and another until 1/1/2030\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\n\ndef my_job(name):\n    print('Running function with name={}'.format(name))\n\n\nCron().schedule(\n    Tab(name='forever').run(my_job, 'forever_job').every(seconds=5),\n    Tab(name='for_thirty').run(my_job, 'mortal_job').every(seconds=5).lasting(seconds=30),\n    Tab(name='real_long').run(my_job, 'long_job').every(seconds=5).until('1/1/2030'),\n).go()\n\n```\n\n## Run job every half hour from 9AM to 5PM excluding weekends\n```python\nfrom crontabs import Cron, Tab\nfrom datetime import datetime\n\ndef my_job(name):\n    # Grab an instance of the crontab logger and write to it.\n    logger = Cron.get_logger()\n    logger.info('Running function with name={}'.format(name))\n\n\ndef business_hours(timestamp):\n    return 9 <= timestamp.hour < 17\n\ndef weekends(timestamp):\n    return timestamp.weekday() > 4\n\n\n# Run a job every 30 minutes during weekdays.  Stop crontabs after it has been running for a year.\n# This will indiscriminately kill every Tab it owns at that time.\nCron().schedule(\n    Tab(\n        name='my_job'\n    ).run(\n        my_job, 'my_job'\n    ).every(\n        minutes=30\n    ).during(\n        business_hours\n    ).excluding(\n        weekends\n    )\n).go(max_seconds=3600 * 24 * 365)\n```\n\n\n# Run test suite with\n```bash\ngit clone git@github.com:robdmc/crontabs.git\ncd crontabs\npip install -e .[dev]\npy.test -s -n 8   # Might need to change the -n amount to pass\n```\n\n___\nProjects by [robdmc](https://www.linkedin.com/in/robdecarvalho).\n* [Pandashells](https://github.com/robdmc/pandashells) Pandas at the bash command line\n* [Consecution](https://github.com/robdmc/consecution) Pipeline abstraction for Python\n* [Behold](https://github.com/robdmc/behold) Helping debug large Python projects\n* [Crontabs](https://github.com/robdmc/crontabs) Simple scheduling library for Python scripts\n* [Switchenv](https://github.com/robdmc/switchenv) Manager for bash environments\n* [Gistfinder](https://github.com/robdmc/gistfinder) Fuzzy-search your gists\n"
  },
  {
    "path": "crontabs/__init__.py",
    "content": "# flake8: noqa\nfrom .version import __version__\n\nfrom .crontabs import Cron, Tab\n"
  },
  {
    "path": "crontabs/crontabs.py",
    "content": "\"\"\"\nModule for manageing crontabs interface\n\"\"\"\nimport datetime\nimport functools\nimport time\nimport traceback\nimport warnings\n\nimport daiquiri\nfrom dateutil.parser import parse\nfrom dateutil.relativedelta import relativedelta\nfrom fleming import fleming\nfrom .processes import ProcessMonitor\n\nimport logging\ndaiquiri.setup(level=logging.INFO)\n\n\nclass Cron:\n    @classmethod\n    def get_logger(self, name='crontab_log'):\n        logger = daiquiri.getLogger(name)\n        return logger\n\n    def __init__(self):\n        \"\"\"\n        A Cron object runs many \"tabs\" of asynchronous tasks.\n        \"\"\"\n        self.monitor = ProcessMonitor()\n        self._tab_list = []\n\n    def schedule(self, *tabs):\n        self._tab_list = list(tabs)\n        return self\n\n    def go(self, max_seconds=None):\n        for tab in self._tab_list:\n            target = tab._get_target()\n            self.monitor.add_subprocess(tab._name, target, tab._robust, tab._until)\n        try:\n            self.monitor.loop(max_seconds=max_seconds)\n        except KeyboardInterrupt:  # pragma: no cover\n            pass\n\n\nclass Tab:\n    _SILENCE_LOGGER = False\n\n    def __init__(self, name, robust=True, verbose=True, memory_friendly=False):\n        \"\"\"\n        Schedules a Tab entry in the cron runner\n        :param name:  Every tab must have a string name\n        :param robust:  A robust tab will be restarted if an error occures\n                        A non robust tab will not be restarted, but all other\n                        non-errored tabs should continue running\n        :param verbose: Set the verbosity of log messages.\n        :memory friendly: If set to true, each iteration will be run in separate process\n        \"\"\"\n        if not isinstance(name, str):\n            raise ValueError('Name argument must be a string')\n\n        self._name = name\n        self._robust = robust\n        self._verbose = verbose\n        self._starting = None\n        self._every_kwargs = None\n        self._func = None\n        self._func_args = None\n        self._func_kwargs = None\n        self._exclude_func = self._default_exclude_func\n        self._during_func = self._default_during_func\n        self._memory_friendly = memory_friendly\n        self._until = None\n        self._lasting_delta = None\n\n    def _default_exclude_func(self, t):\n        return False\n\n    def _default_during_func(self, t):\n        return True\n\n    def _log(self, msg):\n        if self._verbose and not self._SILENCE_LOGGER:  # pragma: no cover\n            logger = daiquiri.getLogger(self._name)\n            logger.info(msg)\n\n    def _process_date(self, datetime_or_str):\n        if isinstance(datetime_or_str, str):\n            return parse(datetime_or_str)\n        elif isinstance(datetime_or_str, datetime.datetime):\n            return datetime_or_str\n        else:\n            raise ValueError('.starting() and until() method can only take strings or datetime objects')\n\n    def starting(self, datetime_or_str):\n        \"\"\"\n        Set the starting time for the cron job.  If not specified, the starting time will always\n        be the beginning of the interval that is current when the cron is started.\n\n        :param datetime_or_str: a datetime object or a string that dateutil.parser can understand\n        :return: self\n        \"\"\"\n        self._starting = self._process_date(datetime_or_str)\n        return self\n\n    def starting_at(self, datetime_or_str):\n        warnings.warn('.starting_at() is depricated.  Use .starting() instead')\n        return self.starting(datetime_or_str)\n\n    def until(self, datetime_or_str):\n        \"\"\"\n        Run the tab until the specified time is reached.  At that point, deactivate the expired\n        tab so that it no longer runs.\n\n        :param datetime_or_str: a datetime object or a string that dateutil.parser can understand\n        :return: self\n        \"\"\"\n        self._until = self._process_date(datetime_or_str)\n        return self\n\n    def lasting(self, **kwargs):\n        \"\"\"\n        Run the tab so that it lasts this long.  The argument structure is exactly the same\n        as that of the .every() method\n        \"\"\"\n        relative_delta_kwargs = {k if k.endswith('s') else k + 's': v for (k, v) in kwargs.items()}\n        self._lasting_delta = relativedelta(**relative_delta_kwargs)\n        return self\n\n    def excluding(self, func, name=''):\n        \"\"\"\n        Pass a function that takes a timestamp for when the function should execute.\n        It inhibits running when the function returns True.\n        Optionally, add a name to the exclusion.  This name will act as an explanation\n        in the log for why the exclusion was made.\n        \"\"\"\n        self._exclude_func = func\n        self._exclude_name = name\n\n        return self\n\n    def during(self, func, name=''):\n        \"\"\"\n        Pass a function that takes a timestamp for when the function should execute.\n        It will only run if the function returns true.\n        Optionally, add a name.  This name will act as an explanation in the log for why\n        any exclusions were made outside the \"during\" specification.\n        \"\"\"\n        self._during_func = func\n        self._during_name = name\n\n        return self\n\n    def every(self, **kwargs):\n        \"\"\"\n        Specify the interval at which you want the job run.  Takes exactly one keyword argument.\n        That argument must be one named one of [second, minute, hour, day, week, month, year] or\n        their plural equivalents.\n\n        :param kwargs: Exactly one keyword argument\n        :return: self\n        \"\"\"\n        if len(kwargs) != 1:\n            raise ValueError('.every() method must be called with exactly one keyword argument')\n\n        self._every_kwargs = self._clean_kwargs(kwargs)\n\n        return self\n\n    def run(self, func, *func_args, **func__kwargs):\n        \"\"\"\n        Specify the function to run at the scheduled times\n\n        :param func:  a callable\n        :param func_args:  the args to the callable\n        :param func__kwargs: the kwargs to the callable\n        :return:\n        \"\"\"\n        self._func = func\n        self._func_args = func_args\n        self._func_kwargs = func__kwargs\n        return self\n\n    def _clean_kwargs(self, kwargs):\n        allowed_key_map = {\n            'seconds': 'second',\n            'second': 'second',\n            'minutes': 'minute',\n            'minute': 'minute',\n            'hours': 'hour',\n            'hour': 'hour',\n            'days': 'day',\n            'day': 'day',\n            'weeks': 'week',\n            'week': 'week',\n            'months': 'month',\n            'month': 'month',\n            'years': 'year',\n            'year': 'year',\n        }\n\n        kwargs = {k if k.endswith('s') else k + 's': v for (k, v) in kwargs.items()}\n\n        out_kwargs = {}\n        for key in kwargs.keys():\n            out_key = allowed_key_map.get(key.lower())\n            if out_key is None:\n                raise ValueError('Allowed time names are {}'.format(sorted(allowed_key_map.keys())))\n            out_kwargs[out_key] = kwargs[key]\n\n        return out_kwargs\n\n    def _is_uninhibited(self, time_stamp):\n        can_run = True\n        msg = 'inhibited: '\n        if self._exclude_func(time_stamp):\n            if self._exclude_name:\n                msg += self._exclude_name\n            can_run = False\n\n        if can_run and not self._during_func(time_stamp):\n            if self._during_name:\n                msg += self._during_name\n            can_run = False\n\n        if not can_run:\n            self._log(msg)\n\n        return can_run\n\n    def _loop(self, max_iter=None):\n        if not self._SILENCE_LOGGER:  # pragma: no cover don't want to clutter tests\n            logger = daiquiri.getLogger(self._name)\n            logger.info('Starting {}'.format(self._name))\n        # fleming and dateutil have arguments that just differ by ending in an \"s\"\n        fleming_kwargs = self._every_kwargs\n        relative_delta_kwargs = {}\n\n        # build the relative delta kwargs\n        for k, v in self._every_kwargs.items():\n            relative_delta_kwargs[k + 's'] = v\n\n        # Previous time is the latest interval boundary that has already happened\n        previous_time = fleming.floor(datetime.datetime.now(), **fleming_kwargs)\n\n        # keep track of iterations\n        n_iter = 0\n        # this is the infinite loop that runs the cron.  It will only be stopped when the\n        # process is killed by its monitor.\n        while True:\n            n_iter += 1\n            if max_iter is not None and n_iter > max_iter:\n                break\n            # everything is run in a try block so errors can be explicitly handled\n            try:\n                # push forward the previous/next times\n                next_time = previous_time + relativedelta(**relative_delta_kwargs)\n                previous_time = next_time\n\n                # get the current time\n                now = datetime.datetime.now()\n\n                # if our job ran longer than an interval, we will need to catch up\n                if next_time < now:\n                    continue\n\n                # sleep until the computed time to run the function\n                sleep_seconds = (next_time - now).total_seconds()\n                time.sleep(sleep_seconds)\n\n                # See what time it is on wakeup\n                timestamp = datetime.datetime.now()\n\n                # If passed until date, break out of here\n                if self._until is not None and timestamp > self._until:\n                    break\n\n                # If not inhibited, run the function\n                if self._is_uninhibited(timestamp):\n                    self._log('Running {}'.format(self._name))\n                    self._func(*self._func_args, **self._func_kwargs)\n\n            except KeyboardInterrupt:  # pragma: no cover\n                pass\n\n            except:  # noqa\n                # only raise the error if not in robust mode.\n                if self._robust:\n                    s = 'Error in tab\\n' + traceback.format_exc()\n                    logger = daiquiri.getLogger(self._name)\n                    logger.error(s)\n                else:\n                    raise\n        self._log('Finishing {}'.format(self._name))\n\n    def _get_target(self):\n        \"\"\"\n        returns a callable with no arguments designed\n        to be the target of a Subprocess\n        \"\"\"\n        if None in [self._func, self._func_kwargs, self._func_kwargs, self._every_kwargs]:\n            raise ValueError('You must call the .every() and .run() methods on every tab.')\n\n        if self._memory_friendly:  # pragma: no cover  TODO: need to find a way to test this\n            target = functools.partial(self._loop, max_iter=1)\n        else:  # pragma: no cover  TODO: need to find a way to test this\n            target = self._loop\n\n        if self._lasting_delta is not None:\n            self._until = datetime.datetime.now() + self._lasting_delta\n\n        return target\n"
  },
  {
    "path": "crontabs/processes.py",
    "content": "import traceback\n\nimport daiquiri\n\ntry:  # pragma: no cover\n    from Queue import Empty\nexcept:  # noqa  pragma: no cover\n    from queue import Empty\n\nfrom multiprocessing import Process, Queue\nimport datetime\nimport sys\n\n\nclass SubProcess:\n    def __init__(\n            self,\n            name,\n            target,\n            q_stdout,\n            q_stderr,\n            q_error,\n            robust,\n            until=None,\n            args=None,\n            kwargs=None,\n    ):\n        # set up the io queues\n        self.q_stdout = q_stdout\n        self.q_stderr = q_stderr\n        self.q_error = q_error\n\n        self._robust = robust\n        self._until = until\n\n        # Setup the name of the sub process\n        self._name = name\n\n        # Save the target of the process\n        self._target = target\n\n        # Save the args to the process\n        self._args = args or set()\n\n        # Setup a reference to the process\n        self._process = None\n\n        # Save the kwargs to the process\n        self._kwargs = kwargs or {}\n\n        self._has_logged_expiration = False\n\n    @property\n    def expired(self):\n        expired = False\n        if self._until is not None and self._until < datetime.datetime.now():\n            expired = True\n            if not self._has_logged_expiration:\n                self._has_logged_expiration = True\n                logger = daiquiri.getLogger(self._name)\n                logger.info('Process expired and will no longer run')\n        return expired\n\n    def is_alive(self):\n        return self._process is not None and self._process.is_alive()\n\n    def start(self):\n\n        self._process = Process(\n            target=wrapped_target,\n            args=[\n                self._target, self.q_stdout, self.q_stderr,\n                self.q_error, self._robust, self._name\n            ] + list(self._args),\n            kwargs=self._kwargs\n        )\n        self._process.daemon = True\n        self._process.start()\n\n\nclass IOQueue:  # pragma: no cover\n    \"\"\"\n    Okay, so here is something annoying.  If you spawn a python subprocess, you cannot\n    pipe stdout/stderr in the same way you can with the parent process.  People who\n    run this library probably want to be able to redirect output to logs.  The best way\n    I could figure out to handle this was to monkey patch stdout and stderr in the\n    subprocesses to be an instance of this class.  All this does is send write() messages\n    to a queue that is monitored by the parent process and prints to parent stdtou/stderr\n    \"\"\"\n    def __init__(self, q):\n        self._q = q\n\n    def write(self, item):\n        self._q.put(item)\n\n    def flush(self):\n        pass\n\n\ndef wrapped_target(target, q_stdout, q_stderr, q_error, robust, name, *args, **kwargs):  # pragma: no cover\n    \"\"\"\n    Wraps a target with queues replacing stdout and stderr\n    \"\"\"\n    import sys\n    sys.stdout = IOQueue(q_stdout)\n    sys.stderr = IOQueue(q_stderr)\n\n    try:\n        target(*args, **kwargs)\n\n    except:  # noqa\n        if not robust:\n            s = 'Error in tab\\n' + traceback.format_exc()\n            logger = daiquiri.getLogger(name)\n            logger.error(s)\n        else:\n            raise\n\n        if not robust:\n            q_error.put(name)\n        raise\n\n\nclass ProcessMonitor:\n    TIMEOUT_SECONDS = .05\n\n    def __init__(self):\n\n        self._subprocesses = []\n        self._is_running = False\n        self.q_stdout = Queue()\n        self.q_stderr = Queue()\n        self.q_error = Queue()\n\n    def add_subprocess(self, name, func, robust, until, *args, **kwargs):\n        sub = SubProcess(\n            name,\n            target=func,\n            q_stdout=self.q_stdout,\n            q_stderr=self.q_stderr,\n            q_error=self.q_error,\n            robust=robust,\n            until=until,\n            args=args,\n            kwargs=kwargs\n        )\n        self._subprocesses.append(sub)\n\n    def process_io_queue(self, q, stream):\n        try:\n            out = q.get(timeout=self.TIMEOUT_SECONDS)\n            out = out.strip()\n            if out:\n                stream.write(out + '\\n')\n                stream.flush()\n        except Empty:\n            pass\n\n    def process_error_queue(self, error_queue):\n        try:\n            error_name = error_queue.get(timeout=self.TIMEOUT_SECONDS)\n            if error_name:\n                error_name = error_name.strip()\n                self._subprocesses = [s for s in self._subprocesses if s._name != error_name]\n                logger = daiquiri.getLogger(error_name)\n                logger.info('Will not auto-restart because it\\'s not robust')\n\n        except Empty:\n            pass\n\n    def loop(self, max_seconds=None):\n        \"\"\"\n        Main loop for the process. This will run continuously until maxiter\n        \"\"\"\n        loop_started = datetime.datetime.now()\n\n        self._is_running = True\n        while self._is_running:\n            self.process_error_queue(self.q_error)\n\n            if max_seconds is not None:\n                if (datetime.datetime.now() - loop_started).total_seconds() > max_seconds:\n                    logger = daiquiri.getLogger('crontabs')\n                    logger.info('Crontabs reached specified timeout.  Exiting.')\n                    break\n            for subprocess in self._subprocesses:\n                if not subprocess.is_alive() and not subprocess.expired:\n                    subprocess.start()\n\n            self.process_io_queue(self.q_stdout, sys.stdout)\n            self.process_io_queue(self.q_stderr, sys.stderr)\n"
  },
  {
    "path": "crontabs/tests/__init__.py",
    "content": "# flake8: noqa\n"
  },
  {
    "path": "crontabs/tests/test_all.py",
    "content": "from collections import Counter\nfrom unittest import TestCase\nimport datetime\nimport functools\nimport sys\nimport time\n\nfrom crontabs import Cron, Tab\nfrom dateutil.parser import parse\nfrom dateutil.relativedelta import relativedelta\nimport fleming\n\nTab._SILENCE_LOGGER = True\n\n# Run tests with\n# py.test -s  crontabs/tests/test_example.py::TestSample::test_base_case\n# Or for parallel tests\n# py.test -s  --cov  -n 2\n\n\nclass ExpectedException(Exception):\n    pass\n\n\nclass PrintCatcher(object):  # pragma: no cover  This is a testing utility that doesn't need to be covered\n    def __init__(self, stream='stdout'):\n        self.text = ''\n        if stream not in {'stdout', 'stderr'}:  # pragma: no cover  this is just a testing utitlity\n            raise ValueError('stream must be either \"stdout\" or \"stderr\"')\n        self.stream = stream\n\n    def write(self, text):\n        self.text += text\n\n    def flush(self):\n        pass\n\n    def __enter__(self):\n        if self.stream == 'stdout':\n            sys.stdout = self\n        else:\n            sys.stderr = self\n        return self\n\n    def __exit__(self, *args):\n        if self.stream == 'stdout':\n            sys.stdout = sys.__stdout__\n        else:\n            sys.stderr = sys.__stderr__\n\n\ndef time_logger(name):  # pragma: no cover\n    print('{} {}'.format(name, datetime.datetime.now()))\n\n\ndef time__sleepy_logger(name):  # pragma: no cover\n    time.sleep(3)\n    print('{} {}'.format(name, datetime.datetime.now()))\n\n\ndef error_raisor(name):\n    raise ExpectedException('This exception is expected in tests. Don\\'t worry about it.')\n\n\nclass TestCrontabs(TestCase):\n\n    def test_non_robust_error(self):\n        tab = Tab(\n            'one_sec', verbose=False, robust=False\n        ).every(seconds=1).run(\n            error_raisor, 'one_sec')\n\n        with self.assertRaises(ExpectedException):\n            tab._loop(max_iter=1)\n\n    def test_robust_error(self):\n        tab = Tab(\n            'one_sec', verbose=False\n        ).every(seconds=1).run(\n            error_raisor, 'one_sec')\n        tab._loop(max_iter=1)\n\n    def test_tab_loop_sleepy(self):\n        tab = Tab(\n            'one_sec', verbose=False\n        ).every(seconds=1).run(\n            time__sleepy_logger, 'one_sec')\n        with PrintCatcher() as catcher:\n            tab._loop(max_iter=7)\n        self.assertEqual(catcher.text.count('one_sec'), 2)\n\n    def test_tab_loop_anchored(self):\n        now = datetime.datetime.now() + datetime.timedelta(seconds=1)\n        tab = Tab(\n            'one_sec', verbose=False\n        ).every(seconds=1).starting(\n            now).run(\n            time_logger, 'one_sec')\n        with PrintCatcher() as catcher:\n            tab._loop(max_iter=3)\n        self.assertEqual(catcher.text.count('one_sec'), 3)\n\n    def test_tab_loop(self):\n        tab = Tab(\n            'one_sec', verbose=False).every(seconds=1).run(\n            time_logger, 'one_sec')\n        with PrintCatcher() as catcher:\n            tab._loop(max_iter=3)\n\n        self.assertEqual(catcher.text.count('one_sec'), 3)\n\n    def test_incomplete(self):\n        with self.assertRaises(ValueError):\n            Cron().schedule(Tab('a').run(time_logger, 'bad')).go()\n\n    def test_bad_starting(self):\n        with self.assertRaises(ValueError):\n            Tab('a').starting(2.345)\n            # Cron().schedule(Tab('a').starting(2.345))\n\n    def test_bad_every(self):\n        with self.assertRaises(ValueError):\n            Tab('a').every(second=1, minute=3)\n            # Cron().schedule(Tab('a').every(second=1, minute=3))\n\n    def test_bad_interval(self):\n        with self.assertRaises(ValueError):\n            Tab('a').every(bad=11)\n            # Cron().schedule(Tab('a').every(bad=11))\n\n    def test_base_case(self):\n        cron = Cron()\n        cron.schedule(\n            Tab('two_sec', verbose=False).every(seconds=2).run(time_logger, 'two_sec'),\n            Tab('three_sec', verbose=False).every(seconds=3).run(time_logger, 'three_sec')\n        )\n        with PrintCatcher(stream='stdout') as stdout_catcher:\n            cron.go(max_seconds=6)\n\n        base_lookup = {\n            'three_sec': 3,\n            'two_sec': 2,\n        }\n\n        lines = list(stdout_catcher.text.split('\\n'))\n\n        # make sure times fall int right slots\n        for line in lines:\n            if line:\n                words = line.split()\n                name = words[0]\n                time = parse('T'.join(words[1:]))\n                self.assertEqual(time.second % base_lookup[name], 0)\n\n        # make sure the tasks were run the proper number of times\n        counter = Counter()\n        for line in lines:\n            if line:\n                counter.update({line.split()[0]: 1})\n\n        self.assertEqual(counter['two_sec'], 3)\n        self.assertEqual(counter['three_sec'], 2)\n\n    def test_anchored_case(self):\n        cron = Cron()\n        starting = datetime.datetime.now()\n        cron.schedule(\n            Tab('three_sec', verbose=False).starting(starting).every(seconds=3).run(time_logger, 'three_sec'),\n            Tab('three_sec_str', verbose=False).starting(\n                starting.isoformat()).every(seconds=3).run(time_logger, 'three_sec_str'),\n        )\n        with PrintCatcher(stream='stdout') as stdout_catcher:\n            cron.go(max_seconds=3.5)\n\n        # make sure times fall int right slots\n        lines = list(stdout_catcher.text.split('\\n'))\n        for line in lines:\n            if line:\n                words = line.split()\n                time = parse('T'.join(words[1:]))\n                elapsed = (time - starting).total_seconds()\n                self.assertTrue(elapsed < 3)\n\n    def test_excluding(self):\n        # Test base case\n        cron = Cron()\n        cron.schedule(\n            Tab('base_case', verbose=True).every(seconds=1).run(time_logger, 'base_case'),\n            Tab('d+').every(seconds=1).during(return_true).run(time_logger, 'd+'),\n            Tab('d-').every(seconds=1).during(return_false).run(time_logger, 'd-'),\n            Tab('e+').every(seconds=1).excluding(return_true).run(time_logger, 'e+'),\n            Tab('e-').every(seconds=1).excluding(return_false).run(time_logger, 'e-'),\n        )\n\n        with PrintCatcher(stream='stdout') as stdout_catcher:\n            cron.go(max_seconds=2)\n\n        self.assertTrue('d+' in stdout_catcher.text)\n        self.assertFalse('d-' in stdout_catcher.text)\n        self.assertFalse('e+' in stdout_catcher.text)\n        self.assertTrue('e-' in stdout_catcher.text)\n\n\ndef return_true(*args, **kwargs):\n    return True\n\n\ndef return_false(*args, **kwargs):\n    return False\n\n\ndef timed_error(then):\n    now = datetime.datetime.now()\n    if then + datetime.timedelta(seconds=3) < now < then + datetime.timedelta(seconds=6):\n        print('timed_error_failure')\n        raise ExpectedException('This exception is expected in tests. Don\\'t worry about it.')\n    else:\n        print('timed_error_success')\n\n\nclass TestRobustness(TestCase):\n    def test_robust_case(self):\n\n        then = datetime.datetime.now()\n\n        cron = Cron()\n        cron.schedule(\n            Tab('one_sec', verbose=False).every(seconds=1).run(time_logger, 'running_time_logger'),\n            Tab('two_sec', verbose=False, robust=True).every(seconds=1).run(functools.partial(timed_error, then))\n        )\n        with PrintCatcher(stream='stdout') as catcher:\n            cron.go(max_seconds=10)\n\n        success_count = catcher.text.count('timed_error_success')\n        failure_count = catcher.text.count('timed_error_failure')\n        time_logger_count = catcher.text.count('running_time_logger')\n        self.assertEqual(success_count, 7)\n        self.assertEqual(failure_count, 3)\n        self.assertEqual(time_logger_count, 10)\n\n    def test_non_robust_case(self):\n\n        then = datetime.datetime.now()\n\n        cron = Cron()\n        cron.schedule(\n            Tab('one_sec', verbose=False).every(seconds=1).run(time_logger, 'running_time_logger'),\n            Tab('two_sec', verbose=False, robust=False).every(seconds=1).run(functools.partial(timed_error, then))\n        )\n        with PrintCatcher(stream='stdout') as catcher:\n            cron.go(max_seconds=10)\n\n        success_count = catcher.text.count('timed_error_success')\n        failure_count = catcher.text.count('timed_error_failure')\n        time_logger_count = catcher.text.count('running_time_logger')\n        self.assertEqual(success_count, 3)\n        self.assertEqual(failure_count, 1)\n        self.assertEqual(time_logger_count, 10)\n\n\ndef func():\n    print('func_was_called')\n\n\nclass TestStartingOnNextInterval(TestCase):\n\n    def test_starts_on_next(self):\n        second = 0\n        interval_seconds = 5\n        while second % interval_seconds == 0:\n            now = datetime.datetime.now()\n            second = now.second\n\n        epoch = fleming.floor(now, second=interval_seconds)\n        then = epoch + relativedelta(seconds=interval_seconds)\n\n        cron = Cron().schedule(\n            Tab(\n                name='pusher',\n                robust=False,\n                memory_friendly=False,\n            ).run(\n                func,\n            ).starting(\n                then\n            ).every(\n                seconds=5\n            )\n        )\n\n        with PrintCatcher(stream='stdout') as catcher:\n            cron.go(max_seconds=5)\n\n        assert('func_was_called' in catcher.text)\n"
  },
  {
    "path": "crontabs/version.py",
    "content": "__version__ = '0.2.2'\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp epub latex latexpdf text man changes linkcheck doctest gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\nimport inspect\nimport os\nimport re\nimport sys\n\nfile_dir = os.path.realpath(os.path.dirname(__file__))\nsys.path.append(os.path.join(file_dir, '..'))\n\ndef get_version():\n    \"\"\"Obtain the packge version from a python file e.g. pkg/__init__.py\n    See <https://packaging.python.org/en/latest/single_source_version.html>.\n    \"\"\"\n    file_dir = os.path.realpath(os.path.dirname(__file__))\n    with open(\n            os.path.join(file_dir, '..', 'crontabs', '__init__.py')) as f:\n        txt = f.read()\n    version_match = re.search(\n        r\"\"\"^__version__ = ['\"]([^'\"]*)['\"]\"\"\", txt, re.M)\n    if version_match:\n        return version_match.group(1)\n    raise RuntimeError(\"Unable to find version string.\")\n\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#sys.path.insert(0, os.path.abspath('.'))\n\n# -- General configuration ------------------------------------------------\n\nextensions = [\n    'sphinx.ext.autodoc',\n    #'sphinx.ext.intersphinx',\n    'sphinx.ext.viewcode',\n    #'sphinxcontrib.fulltoc',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'toc'\n\n# General information about the project.\nproject = 'crontabs'\ncopyright = '2017, Rob deCarvalho'\n\n# The short X.Y version.\nversion = get_version()\n# The full version, including alpha/beta/rc tags.\nrelease = version\n\nexclude_patterns = ['_build']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\nintersphinx_mapping = {\n    'python': ('http://docs.python.org/3.4', None),\n    'django': ('http://django.readthedocs.org/en/latest/', None),\n    #'celery': ('http://celery.readthedocs.org/en/latest/', None),\n}\n\n# -- Options for HTML output ----------------------------------------------\n\nhtml_theme = 'default'\n#html_theme_path = []\n\non_rtd = os.environ.get('READTHEDOCS', None) == 'True'\nif not on_rtd:  # only import and set the theme if we're building docs locally\n    import sphinx_rtd_theme\n    html_theme = 'sphinx_rtd_theme'\n    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n# html_static_path = ['_static']\nhtml_static_path = []\n\n# Custom sidebar templates, maps document names to template names.\n#html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#html_additional_pages = {}\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\nhtml_show_sphinx = False\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\nhtml_show_copyright = True\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'crontabsdoc'\n"
  },
  {
    "path": "docs/index.rst",
    "content": "crontabs\n=============================\n\nReplace this text with content.\n\n\n"
  },
  {
    "path": "docs/ref/crontabs.rst",
    "content": ".. _ref-crontabs:\n\n\nAPI Documentation\n==================\nReplace this with api documentation\n\n\n"
  },
  {
    "path": "docs/toc.rst",
    "content": "Table of Contents\n=================\n\n.. toctree::\n   :maxdepth: 3\n\n   index\n   ref/crontabs\n"
  },
  {
    "path": "publish.py",
    "content": "import subprocess\n\nsubprocess.call('pip install wheel'.split())\nsubprocess.call('pip install twine'.split())\nsubprocess.call('rm -rf ./build'.split())\nsubprocess.call('rm -rf ./dist/'.split())\nsubprocess.call('python setup.py clean --all'.split())\nsubprocess.call('python setup.py sdist bdist_wheel'.split())\nsubprocess.call('twine upload dist/*'.split())\n"
  },
  {
    "path": "setup.cfg",
    "content": "\n[coverage:report]\nshow_missing=True\nexclude_lines =\n    # Have to re-enable the standard pragma\n    pragma: no cover\n\n    # Don't complain if tests don't hit defensive assertion code:\n    raise NotImplementedError\n\n[coverage:run]\nomit =\n    crontabs/version.py\n    crontabs/__init__.py\n    */env/*\nconcurrency = multiprocessing\n\n\n\n\n[flake8]\nmax-line-length = 120\nexclude = docs,env,*.egg\nmax-complexity = 13\nignore = E402\n\n[build_sphinx]\nsource-dir = docs/\nbuild-dir  = docs/_build\nall_files  = 1\n\n[upload_sphinx]\nupload-dir = docs/_build/html\n\n[bdist_wheel]\nuniversal = 1\n"
  },
  {
    "path": "setup.py",
    "content": "# import multiprocessing to avoid this bug (http://bugs.python.org/issue15881#msg170215)\nimport multiprocessing\nassert multiprocessing\nimport re\nfrom setuptools import setup, find_packages\n\n\ndef get_version():\n    \"\"\"\n    Extracts the version number from the version.py file.\n    \"\"\"\n    VERSION_FILE = 'crontabs/version.py'\n    mo = re.search(r'^__version__ = [\\'\"]([^\\'\"]*)[\\'\"]', open(VERSION_FILE, 'rt').read(), re.M)\n    if mo:\n        return mo.group(1)\n    else:\n        raise RuntimeError('Unable to find version string in {0}.'.format(VERSION_FILE))\n\n\ninstall_requires = [\n    'fleming',\n    'daiquiri[json]'\n]\n\ntests_require = [\n    'coverage',\n    'flake8',\n    'mock',\n    'pytest',\n    'pytest-cov',\n    'pytest-xdist',\n    'wheel',\n]\n\ndocs_require = [\n    # 'Sphinx',\n    # 'sphinx_rtd_theme'\n]\n\nextras_require = {\n    'dev': tests_require + docs_require,\n}\n\nsetup(\n    name='crontabs',\n    version=get_version(),\n    description='Simple job scheduling for python',\n    long_description='Simple job scheduling for python',\n    url='https://github.com/robdmc/crontabs',\n    author='Rob deCarvalho',\n    author_email='unlisted@unlisted.net',\n    keywords='',\n    packages=find_packages(),\n    classifiers=[\n        'Programming Language :: Python :: 2.7',\n        'Programming Language :: Python :: 3.4',\n        'Programming Language :: Python :: 3.5',\n        'Intended Audience :: Developers',\n        'License :: OSI Approved :: MIT License',\n        'Operating System :: OS Independent',\n    ],\n    license='MIT',\n    include_package_data=True,\n    test_suite='nose.collector',\n    install_requires=install_requires,\n    tests_require=tests_require,\n    extras_require=extras_require,\n    zip_safe=False,\n)\n"
  }
]