schedule-0.3.2/0000755000076500000240000000000012545360135013673 5ustar danielstaff00000000000000schedule-0.3.2/HISTORY.rst0000644000076500000240000000603512545356570015602 0ustar danielstaff00000000000000.. :changelog: History ------- 0.3.2 (2015-07-02) ++++++++++++++++++ - Fixed issues where scheduling a job with a functools.partial as the job function fails. Thanks @dylwhich. - Fixed an issue where scheduling a job to run every >= 2 days would cause the initial execution to happen one day early. Thanks @WoLfulus for identifying this and providing a fix. - Added a FAQ item to describe how to schedule a job that runs only once. 0.3.1 (2014-09-03) ++++++++++++++++++ - Fixed an issue with unicode handling in setup.py that was causing trouble on Python 3 and Debian (https://github.com/dbader/schedule/issues/27). Thanks to @waghanza for reporting it. - Added an FAQ item to describe how to deal with job functions that throw exceptions. Thanks @mplewis. 0.3.0 (2014-06-14) ++++++++++++++++++ - Added support for scheduling jobs on specific weekdays. Example: ``schedule.every().tuesday.do(job)`` or ``schedule.every().wednesday.at("13:15").do(job)`` (Thanks @abultman.) - Run tests against Python 2.7 and 3.4. Python 3.3 should continue to work but we're not actively testing it on CI anymore. 0.2.1 (2013-11-20) ++++++++++++++++++ - Fixed history (no code changes). 0.2.0 (2013-11-09) ++++++++++++++++++ - This release introduces two new features in a backwards compatible way: - Allow jobs to cancel repeated execution: Jobs can be cancelled by calling ``schedule.cancel_job()`` or by returning ``schedule.CancelJob`` from the job function. (Thanks to @cfrco and @matrixise.) - Updated ``at_time()`` to allow running jobs at a particular time every hour. Example: ``every().hour.at(':15').do(job)`` will run ``job`` 15 minutes after every full hour. (Thanks @mattss.) - Refactored unit tests to mock ``datetime`` in a cleaner way. (Thanks @matts.) 0.1.11 (2013-07-30) +++++++++++++++++++ - Fixed an issue with ``next_run()`` throwing a ``ValueError`` exception when the job queue is empty. Thanks to @dpagano for pointing this out and thanks to @mrhwick for quickly providing a fix. 0.1.10 (2013-06-07) +++++++++++++++++++ - Fixed issue with ``at_time`` jobs not running on the same day the job is created (Thanks to @mattss) 0.1.9 (2013-05-27) ++++++++++++++++++ - Added ``schedule.next_run()`` - Added ``schedule.idle_seconds()`` - Args passed into ``do()`` are forwarded to the job function at call time - Increased test coverage to 100% 0.1.8 (2013-05-21) ++++++++++++++++++ - Changed default ``delay_seconds`` for ``schedule.run_all()`` to 0 (from 60) - Increased test coverage 0.1.7 (2013-05-20) ++++++++++++++++++ - API change: renamed ``schedule.run_all_jobs()`` to ``schedule.run_all()`` - API change: renamed ``schedule.run_pending_jobs()`` to ``schedule.run_pending()`` - API change: renamed ``schedule.clear_all_jobs()`` to ``schedule.clear()`` - Added ``schedule.jobs`` 0.1.6 (2013-05-20) ++++++++++++++++++ - Fix packaging - README fixes 0.1.4 (2013-05-20) ++++++++++++++++++ - API change: renamed ``schedule.tick()`` to ``schedule.run_pending_jobs()`` - Updated README and ``setup.py`` packaging 0.1.0 (2013-05-19) ++++++++++++++++++ - Initial release schedule-0.3.2/LICENSE.txt0000644000076500000240000000211312146147272015515 0ustar danielstaff00000000000000The MIT License (MIT) Copyright (c) 2013 Daniel Bader (http://dbader.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. schedule-0.3.2/PKG-INFO0000644000076500000240000002001712545360135014770 0ustar danielstaff00000000000000Metadata-Version: 1.1 Name: schedule Version: 0.3.2 Summary: Job scheduling for humans. Home-page: https://github.com/dbader/schedule Author: Daniel Bader Author-email: mail@dbader.org License: The MIT License (MIT) Copyright (c) 2013 Daniel Bader (http://dbader.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Download-URL: https://github.com/dbader/schedule/tarball/0.3.2 Description: schedule ======== .. image:: https://api.travis-ci.org/dbader/schedule.svg :target: https://travis-ci.org/dbader/schedule .. image:: https://coveralls.io/repos/dbader/schedule/badge.svg :target: https://coveralls.io/r/dbader/schedule .. image:: https://img.shields.io/pypi/v/schedule.svg :target: https://pypi.python.org/pypi/schedule .. image:: https://img.shields.io/pypi/dm/schedule.svg :target: https://pypi.python.org/pypi/schedule Python job scheduling for humans. An in-process scheduler for periodic jobs that uses the builder pattern for configuration. Schedule lets you run Python functions (or any other callable) periodically at pre-determined intervals using a simple, human-friendly syntax. Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ (`Google cache `_) and the `clockwork `_ Ruby module. Features -------- - A simple to use API for scheduling jobs. - Very lightweight and no external dependencies. - Excellent test coverage. - Tested on Python 2.7 and 3.4 Usage ----- .. code-block:: bash $ pip install schedule .. code-block:: python import schedule import time def job(): print("I'm working...") schedule.every(10).minutes.do(job) schedule.every().hour.do(job) schedule.every().day.at("10:30").do(job) schedule.every().monday.do(job) schedule.every().wednesday.at("13:15").do(job) while True: schedule.run_pending() time.sleep(1) FAQ --- In lieu of a full documentation (coming soon) check out this set of `frequently asked questions `_ for solutions to some common questions. Meta ---- Daniel Bader - `@dbader_org `_ - mail@dbader.org Distributed under the MIT license. See ``LICENSE.txt`` for more information. https://github.com/dbader/schedule .. :changelog: History ------- 0.3.2 (2015-07-02) ++++++++++++++++++ - Fixed issues where scheduling a job with a functools.partial as the job function fails. Thanks @dylwhich. - Fixed an issue where scheduling a job to run every >= 2 days would cause the initial execution to happen one day early. Thanks @WoLfulus for identifying this and providing a fix. - Added a FAQ item to describe how to schedule a job that runs only once. 0.3.1 (2014-09-03) ++++++++++++++++++ - Fixed an issue with unicode handling in setup.py that was causing trouble on Python 3 and Debian (https://github.com/dbader/schedule/issues/27). Thanks to @waghanza for reporting it. - Added an FAQ item to describe how to deal with job functions that throw exceptions. Thanks @mplewis. 0.3.0 (2014-06-14) ++++++++++++++++++ - Added support for scheduling jobs on specific weekdays. Example: ``schedule.every().tuesday.do(job)`` or ``schedule.every().wednesday.at("13:15").do(job)`` (Thanks @abultman.) - Run tests against Python 2.7 and 3.4. Python 3.3 should continue to work but we're not actively testing it on CI anymore. 0.2.1 (2013-11-20) ++++++++++++++++++ - Fixed history (no code changes). 0.2.0 (2013-11-09) ++++++++++++++++++ - This release introduces two new features in a backwards compatible way: - Allow jobs to cancel repeated execution: Jobs can be cancelled by calling ``schedule.cancel_job()`` or by returning ``schedule.CancelJob`` from the job function. (Thanks to @cfrco and @matrixise.) - Updated ``at_time()`` to allow running jobs at a particular time every hour. Example: ``every().hour.at(':15').do(job)`` will run ``job`` 15 minutes after every full hour. (Thanks @mattss.) - Refactored unit tests to mock ``datetime`` in a cleaner way. (Thanks @matts.) 0.1.11 (2013-07-30) +++++++++++++++++++ - Fixed an issue with ``next_run()`` throwing a ``ValueError`` exception when the job queue is empty. Thanks to @dpagano for pointing this out and thanks to @mrhwick for quickly providing a fix. 0.1.10 (2013-06-07) +++++++++++++++++++ - Fixed issue with ``at_time`` jobs not running on the same day the job is created (Thanks to @mattss) 0.1.9 (2013-05-27) ++++++++++++++++++ - Added ``schedule.next_run()`` - Added ``schedule.idle_seconds()`` - Args passed into ``do()`` are forwarded to the job function at call time - Increased test coverage to 100% 0.1.8 (2013-05-21) ++++++++++++++++++ - Changed default ``delay_seconds`` for ``schedule.run_all()`` to 0 (from 60) - Increased test coverage 0.1.7 (2013-05-20) ++++++++++++++++++ - API change: renamed ``schedule.run_all_jobs()`` to ``schedule.run_all()`` - API change: renamed ``schedule.run_pending_jobs()`` to ``schedule.run_pending()`` - API change: renamed ``schedule.clear_all_jobs()`` to ``schedule.clear()`` - Added ``schedule.jobs`` 0.1.6 (2013-05-20) ++++++++++++++++++ - Fix packaging - README fixes 0.1.4 (2013-05-20) ++++++++++++++++++ - API change: renamed ``schedule.tick()`` to ``schedule.run_pending_jobs()`` - Updated README and ``setup.py`` packaging 0.1.0 (2013-05-19) ++++++++++++++++++ - Initial release Keywords: schedule,periodic,jobs,scheduling,clockwork,cron Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Natural Language :: English schedule-0.3.2/README.rst0000644000076500000240000000415512545356570015377 0ustar danielstaff00000000000000schedule ======== .. image:: https://api.travis-ci.org/dbader/schedule.svg :target: https://travis-ci.org/dbader/schedule .. image:: https://coveralls.io/repos/dbader/schedule/badge.svg :target: https://coveralls.io/r/dbader/schedule .. image:: https://img.shields.io/pypi/v/schedule.svg :target: https://pypi.python.org/pypi/schedule .. image:: https://img.shields.io/pypi/dm/schedule.svg :target: https://pypi.python.org/pypi/schedule Python job scheduling for humans. An in-process scheduler for periodic jobs that uses the builder pattern for configuration. Schedule lets you run Python functions (or any other callable) periodically at pre-determined intervals using a simple, human-friendly syntax. Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ (`Google cache `_) and the `clockwork `_ Ruby module. Features -------- - A simple to use API for scheduling jobs. - Very lightweight and no external dependencies. - Excellent test coverage. - Tested on Python 2.7 and 3.4 Usage ----- .. code-block:: bash $ pip install schedule .. code-block:: python import schedule import time def job(): print("I'm working...") schedule.every(10).minutes.do(job) schedule.every().hour.do(job) schedule.every().day.at("10:30").do(job) schedule.every().monday.do(job) schedule.every().wednesday.at("13:15").do(job) while True: schedule.run_pending() time.sleep(1) FAQ --- In lieu of a full documentation (coming soon) check out this set of `frequently asked questions `_ for solutions to some common questions. Meta ---- Daniel Bader - `@dbader_org `_ - mail@dbader.org Distributed under the MIT license. See ``LICENSE.txt`` for more information. https://github.com/dbader/schedule schedule-0.3.2/schedule/0000755000076500000240000000000012545360135015467 5ustar danielstaff00000000000000schedule-0.3.2/schedule/__init__.py0000644000076500000240000003010512545356570017607 0ustar danielstaff00000000000000""" Python job scheduling for humans. An in-process scheduler for periodic jobs that uses the builder pattern for configuration. Schedule lets you run Python functions (or any other callable) periodically at pre-determined intervals using a simple, human-friendly syntax. Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the "clockwork" Ruby module [2][3]. Features: - A simple to use API for scheduling jobs. - Very lightweight and no external dependencies. - Excellent test coverage. - Works with Python 2.7 and 3.3 Usage: >>> import schedule >>> import time >>> def job(message='stuff'): >>> print("I'm working on:", message) >>> schedule.every(10).minutes.do(job) >>> schedule.every().hour.do(job, message='things') >>> schedule.every().day.at("10:30").do(job) >>> while True: >>> schedule.run_pending() >>> time.sleep(1) [1] http://adam.heroku.com/past/2010/4/13/rethinking_cron/ [2] https://github.com/tomykaira/clockwork [3] http://adam.heroku.com/past/2010/6/30/replace_cron_with_clockwork/ """ import datetime import functools import logging import time logger = logging.getLogger('schedule') class CancelJob(object): pass class Scheduler(object): def __init__(self): self.jobs = [] def run_pending(self): """Run all jobs that are scheduled to run. Please note that it is *intended behavior that tick() does not run missed jobs*. For example, if you've registered a job that should run every minute and you only call tick() in one hour increments then your job won't be run 60 times in between but only once. """ runnable_jobs = (job for job in self.jobs if job.should_run) for job in sorted(runnable_jobs): self._run_job(job) def run_all(self, delay_seconds=0): """Run all jobs regardless if they are scheduled to run or not. A delay of `delay` seconds is added between each job. This helps distribute system load generated by the jobs more evenly over time.""" logger.info('Running *all* %i jobs with %is delay inbetween', len(self.jobs), delay_seconds) for job in self.jobs: self._run_job(job) time.sleep(delay_seconds) def clear(self): """Deletes all scheduled jobs.""" del self.jobs[:] def cancel_job(self, job): """Delete a scheduled job.""" try: self.jobs.remove(job) except ValueError: pass def every(self, interval=1): """Schedule a new periodic job.""" job = Job(interval) self.jobs.append(job) return job def _run_job(self, job): ret = job.run() if isinstance(ret, CancelJob) or ret is CancelJob: self.cancel_job(job) @property def next_run(self): """Datetime when the next job should run.""" if not self.jobs: return None return min(self.jobs).next_run @property def idle_seconds(self): """Number of seconds until `next_run`.""" return (self.next_run - datetime.datetime.now()).total_seconds() class Job(object): """A periodic job as used by `Scheduler`.""" def __init__(self, interval): self.interval = interval # pause interval * unit between runs self.job_func = None # the job job_func to run self.unit = None # time units, e.g. 'minutes', 'hours', ... self.at_time = None # optional time at which this job runs self.last_run = None # datetime of the last run self.next_run = None # datetime of the next run self.period = None # timedelta between runs, only valid for self.start_day = None # Specific day of the week to start on def __lt__(self, other): """PeriodicJobs are sortable based on the scheduled time they run next.""" return self.next_run < other.next_run def __repr__(self): def format_time(t): return t.strftime('%Y-%m-%d %H:%M:%S') if t else '[never]' timestats = '(last run: %s, next run: %s)' % ( format_time(self.last_run), format_time(self.next_run)) if hasattr(self.job_func, '__name__'): job_func_name = self.job_func.__name__ else: job_func_name = repr(self.job_func) args = [repr(x) for x in self.job_func.args] kwargs = ['%s=%s' % (k, repr(v)) for k, v in self.job_func.keywords.items()] call_repr = job_func_name + '(' + ', '.join(args + kwargs) + ')' if self.at_time is not None: return 'Every %s %s at %s do %s %s' % ( self.interval, self.unit[:-1] if self.interval == 1 else self.unit, self.at_time, call_repr, timestats) else: return 'Every %s %s do %s %s' % ( self.interval, self.unit[:-1] if self.interval == 1 else self.unit, call_repr, timestats) @property def second(self): assert self.interval == 1 return self.seconds @property def seconds(self): self.unit = 'seconds' return self @property def minute(self): assert self.interval == 1 return self.minutes @property def minutes(self): self.unit = 'minutes' return self @property def hour(self): assert self.interval == 1 return self.hours @property def hours(self): self.unit = 'hours' return self @property def day(self): assert self.interval == 1 return self.days @property def days(self): self.unit = 'days' return self @property def week(self): assert self.interval == 1 return self.weeks @property def monday(self): assert self.interval == 1 self.start_day = 'monday' return self.weeks @property def tuesday(self): assert self.interval == 1 self.start_day = 'tuesday' return self.weeks @property def wednesday(self): assert self.interval == 1 self.start_day = 'wednesday' return self.weeks @property def thursday(self): assert self.interval == 1 self.start_day = 'thursday' return self.weeks @property def friday(self): assert self.interval == 1 self.start_day = 'friday' return self.weeks @property def saturday(self): assert self.interval == 1 self.start_day = 'saturday' return self.weeks @property def sunday(self): assert self.interval == 1 self.start_day = 'sunday' return self.weeks @property def weeks(self): self.unit = 'weeks' return self def at(self, time_str): """Schedule the job every day at a specific time. Calling this is only valid for jobs scheduled to run every N day(s). """ assert self.unit in ('days', 'hours') or self.start_day hour, minute = [t for t in time_str.split(':')] minute = int(minute) if self.unit == 'days' or self.start_day: hour = int(hour) assert 0 <= hour <= 23 elif self.unit == 'hours': hour = 0 assert 0 <= minute <= 59 self.at_time = datetime.time(hour, minute) return self def do(self, job_func, *args, **kwargs): """Specifies the job_func that should be called every time the job runs. Any additional arguments are passed on to job_func when the job runs. """ self.job_func = functools.partial(job_func, *args, **kwargs) try: functools.update_wrapper(self.job_func, job_func) except AttributeError: # job_funcs already wrapped by functools.partial won't have # __name__, __module__ or __doc__ and the update_wrapper() # call will fail. pass self._schedule_next_run() return self @property def should_run(self): """True if the job should be run now.""" return datetime.datetime.now() >= self.next_run def run(self): """Run the job and immediately reschedule it.""" logger.info('Running job %s', self) ret = self.job_func() self.last_run = datetime.datetime.now() self._schedule_next_run() return ret def _schedule_next_run(self): """Compute the instant when this job should run next.""" # Allow *, ** magic temporarily: # pylint: disable=W0142 assert self.unit in ('seconds', 'minutes', 'hours', 'days', 'weeks') self.period = datetime.timedelta(**{self.unit: self.interval}) self.next_run = datetime.datetime.now() + self.period if self.start_day is not None: assert self.unit == 'weeks' weekdays = ( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ) assert self.start_day in weekdays weekday = weekdays.index(self.start_day) days_ahead = weekday - self.next_run.weekday() if days_ahead <= 0: # Target day already happened this week days_ahead += 7 self.next_run += datetime.timedelta(days_ahead) - self.period if self.at_time is not None: assert self.unit in ('days', 'hours') or self.start_day is not None kwargs = { 'minute': self.at_time.minute, 'second': self.at_time.second, 'microsecond': 0 } if self.unit == 'days' or self.start_day is not None: kwargs['hour'] = self.at_time.hour self.next_run = self.next_run.replace(**kwargs) # If we are running for the first time, make sure we run # at the specified time *today* (or *this hour*) as well if not self.last_run: now = datetime.datetime.now() if (self.unit == 'days' and self.at_time > now.time() and self.interval == 1): self.next_run = self.next_run - datetime.timedelta(days=1) elif self.unit == 'hours' and self.at_time.minute > now.minute: self.next_run = self.next_run - datetime.timedelta(hours=1) if self.start_day is not None and self.at_time is not None: # Let's see if we will still make that time we specified today if (self.next_run - datetime.datetime.now()).days >= 7: self.next_run -= self.period # The following methods are shortcuts for not having to # create a Scheduler instance: default_scheduler = Scheduler() jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()? def every(interval=1): """Schedule a new periodic job.""" return default_scheduler.every(interval) def run_pending(): """Run all jobs that are scheduled to run. Please note that it is *intended behavior that run_pending() does not run missed jobs*. For example, if you've registered a job that should run every minute and you only call run_pending() in one hour increments then your job won't be run 60 times in between but only once. """ default_scheduler.run_pending() def run_all(delay_seconds=0): """Run all jobs regardless if they are scheduled to run or not. A delay of `delay` seconds is added between each job. This can help to distribute the system load generated by the jobs more evenly over time.""" default_scheduler.run_all(delay_seconds=delay_seconds) def clear(): """Deletes all scheduled jobs.""" default_scheduler.clear() def cancel_job(job): """Delete a scheduled job.""" default_scheduler.cancel_job(job) def next_run(): """Datetime when the next job should run.""" return default_scheduler.next_run def idle_seconds(): """Number of seconds until `next_run`.""" return default_scheduler.idle_seconds schedule-0.3.2/setup.cfg0000644000076500000240000000005112347135412015506 0ustar danielstaff00000000000000[metadata] description-file = README.rst schedule-0.3.2/setup.py0000644000076500000240000000234612545356645015425 0ustar danielstaff00000000000000import codecs import os import sys from distutils.core import setup if sys.argv[-1] == 'publish': os.system('python setup.py sdist upload -r pypi') sys.exit() SCHEDULE_VERSION = '0.3.2' SCHEDULE_DOWNLOAD_URL = ( 'https://github.com/dbader/schedule/tarball/' + SCHEDULE_VERSION ) def read_file(filename): """ Read a utf8 encoded text file and return its contents. """ with codecs.open(filename, 'r', 'utf8') as f: return f.read() setup( name='schedule', packages=['schedule'], version=SCHEDULE_VERSION, description='Job scheduling for humans.', long_description=( read_file('README.rst') + '\n\n' + read_file('HISTORY.rst') ), license=read_file('LICENSE.txt'), author='Daniel Bader', author_email='mail@dbader.org', url='https://github.com/dbader/schedule', download_url=SCHEDULE_DOWNLOAD_URL, keywords=[ 'schedule', 'periodic', 'jobs', 'scheduling', 'clockwork', 'cron' ], classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Natural Language :: English', ], ) schedule-0.3.2/test_schedule.py0000644000076500000240000002245312545356570017116 0ustar danielstaff00000000000000"""Unit tests for schedule.py""" import datetime import functools import mock import unittest # Silence "missing docstring", "method could be a function", # "class already defined", and "too many public methods" messages: # pylint: disable-msg=R0201,C0111,E0102,R0904,R0901 import schedule from schedule import every def make_mock_job(name=None): job = mock.Mock() job.__name__ = name or 'job' return job class mock_datetime(object): """ Monkey-patch datetime for predictable results """ def __init__(self, year, month, day, hour, minute): self.year = year self.month = month self.day = day self.hour = hour self.minute = minute def __enter__(self): class MockDate(datetime.datetime): @classmethod def today(cls): return cls(self.year, self.month, self.day) @classmethod def now(cls): return cls(self.year, self.month, self.day, self.hour, self.minute) self.original_datetime = datetime.datetime datetime.datetime = MockDate def __exit__(self, *args, **kwargs): datetime.datetime = self.original_datetime class SchedulerTests(unittest.TestCase): def setUp(self): schedule.clear() def test_time_units(self): assert every().seconds.unit == 'seconds' assert every().minutes.unit == 'minutes' assert every().hours.unit == 'hours' assert every().days.unit == 'days' assert every().weeks.unit == 'weeks' def test_singular_time_units_match_plural_units(self): assert every().second.unit == every().seconds.unit assert every().minute.unit == every().minutes.unit assert every().hour.unit == every().hours.unit assert every().day.unit == every().days.unit assert every().week.unit == every().weeks.unit def test_at_time(self): mock_job = make_mock_job() assert every().day.at('10:30').do(mock_job).next_run.hour == 10 assert every().day.at('10:30').do(mock_job).next_run.minute == 30 def test_at_time_hour(self): with mock_datetime(2010, 1, 6, 12, 20): mock_job = make_mock_job() assert every().hour.at(':30').do(mock_job).next_run.hour == 12 assert every().hour.at(':30').do(mock_job).next_run.minute == 30 assert every().hour.at(':10').do(mock_job).next_run.hour == 13 assert every().hour.at(':10').do(mock_job).next_run.minute == 10 assert every().hour.at(':00').do(mock_job).next_run.hour == 13 assert every().hour.at(':00').do(mock_job).next_run.minute == 0 def test_next_run_time(self): with mock_datetime(2010, 1, 6, 12, 15): mock_job = make_mock_job() assert schedule.next_run() is None assert every().minute.do(mock_job).next_run.minute == 16 assert every(5).minutes.do(mock_job).next_run.minute == 20 assert every().hour.do(mock_job).next_run.hour == 13 assert every().day.do(mock_job).next_run.day == 7 assert every().day.at('09:00').do(mock_job).next_run.day == 7 assert every().day.at('12:30').do(mock_job).next_run.day == 6 assert every().week.do(mock_job).next_run.day == 13 assert every().monday.do(mock_job).next_run.day == 11 assert every().tuesday.do(mock_job).next_run.day == 12 assert every().wednesday.do(mock_job).next_run.day == 13 assert every().thursday.do(mock_job).next_run.day == 7 assert every().friday.do(mock_job).next_run.day == 8 assert every().saturday.do(mock_job).next_run.day == 9 assert every().sunday.do(mock_job).next_run.day == 10 def test_run_all(self): mock_job = make_mock_job() every().minute.do(mock_job) every().hour.do(mock_job) every().day.at('11:00').do(mock_job) schedule.run_all() assert mock_job.call_count == 3 def test_job_func_args_are_passed_on(self): mock_job = make_mock_job() every().second.do(mock_job, 1, 2, 'three', foo=23, bar={}) schedule.run_all() mock_job.assert_called_once_with(1, 2, 'three', foo=23, bar={}) def test_to_string(self): def job_fun(): pass s = str(every().minute.do(job_fun, 'foo', bar=23)) assert 'job_fun' in s assert 'foo' in s assert 'bar=23' in s def test_to_string_lambda_job_func(self): assert len(str(every().minute.do(lambda: 1))) > 1 assert len(str(every().day.at('10:30').do(lambda: 1))) > 1 def test_to_string_functools_partial_job_func(self): def job_fun(arg): pass job_fun = functools.partial(job_fun, 'foo') job_repr = repr(every().minute.do(job_fun, bar=True, somekey=23)) assert 'functools.partial' in job_repr assert 'bar=True' in job_repr assert 'somekey=23' in job_repr def test_run_pending(self): """Check that run_pending() runs pending jobs. We do this by overriding datetime.datetime with mock objects that represent increasing system times. Please note that it is *intended behavior that run_pending() does not run missed jobs*. For example, if you've registered a job that should run every minute and you only call run_pending() in one hour increments then your job won't be run 60 times in between but only once. """ mock_job = make_mock_job() with mock_datetime(2010, 1, 6, 12, 15): every().minute.do(mock_job) every().hour.do(mock_job) every().day.do(mock_job) every().sunday.do(mock_job) schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 6, 12, 16): schedule.run_pending() assert mock_job.call_count == 1 with mock_datetime(2010, 1, 6, 13, 16): mock_job.reset_mock() schedule.run_pending() assert mock_job.call_count == 2 with mock_datetime(2010, 1, 7, 13, 16): mock_job.reset_mock() schedule.run_pending() assert mock_job.call_count == 3 with mock_datetime(2010, 1, 10, 13, 16): mock_job.reset_mock() schedule.run_pending() assert mock_job.call_count == 4 def test_run_every_weekday_at_specific_time_today(self): mock_job = make_mock_job() with mock_datetime(2010, 1, 6, 13, 16): every().wednesday.at('14:12').do(mock_job) schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 6, 14, 16): schedule.run_pending() assert mock_job.call_count == 1 def test_run_every_weekday_at_specific_time_past_today(self): mock_job = make_mock_job() with mock_datetime(2010, 1, 6, 13, 16): every().wednesday.at('13:15').do(mock_job) schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 13, 13, 14): schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 13, 13, 16): schedule.run_pending() assert mock_job.call_count == 1 def test_run_every_n_days_at_specific_time(self): mock_job = make_mock_job() with mock_datetime(2010, 1, 6, 11, 29): every(2).days.at('11:30').do(mock_job) schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 6, 11, 31): schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 7, 11, 31): schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 8, 11, 29): schedule.run_pending() assert mock_job.call_count == 0 with mock_datetime(2010, 1, 8, 11, 31): schedule.run_pending() assert mock_job.call_count == 1 with mock_datetime(2010, 1, 10, 11, 31): schedule.run_pending() assert mock_job.call_count == 2 def test_next_run_property(self): original_datetime = datetime.datetime with mock_datetime(2010, 1, 6, 13, 16): hourly_job = make_mock_job('hourly') daily_job = make_mock_job('daily') every().day.do(daily_job) every().hour.do(hourly_job) assert len(schedule.jobs) == 2 # Make sure the hourly job is first assert schedule.next_run() == original_datetime(2010, 1, 6, 14, 16) assert schedule.idle_seconds() == 60 * 60 def test_cancel_job(self): def stop_job(): return schedule.CancelJob mock_job = make_mock_job() every().second.do(stop_job) mj = every().second.do(mock_job) assert len(schedule.jobs) == 2 schedule.run_all() assert len(schedule.jobs) == 1 assert schedule.jobs[0] == mj schedule.cancel_job('Not a job') assert len(schedule.jobs) == 1 schedule.default_scheduler.cancel_job('Not a job') assert len(schedule.jobs) == 1 schedule.cancel_job(mj) assert len(schedule.jobs) == 0