APScheduler-2.1.2/0000755000175000017500000000000012266471145012327 5ustar alexalexAPScheduler-2.1.2/MANIFEST.in0000644000175000017500000000016212266453346014067 0ustar alexalexinclude README.rst recursive-include tests *.py recursive-include examples *.py recursive-include docs *.rst *.py APScheduler-2.1.2/setup.py0000644000175000017500000000253612266460657014055 0ustar alexalex# coding: utf-8 import os.path try: from setuptools import setup extras = dict(zip_safe=False, test_suite='nose.collector', tests_require=['nose']) except ImportError: from distutils.core import setup extras = {} import apscheduler here = os.path.dirname(__file__) readme_path = os.path.join(here, 'README.rst') readme = open(readme_path).read() setup( name='APScheduler', version=apscheduler.release, description='In-process task scheduler with Cron-like capabilities', long_description=readme, author='Alex Gronholm', author_email='apscheduler@nextday.fi', url='http://pypi.python.org/pypi/APScheduler/', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3' ], keywords='scheduling cron', license='MIT', packages=('apscheduler', 'apscheduler.jobstores', 'apscheduler.triggers', 'apscheduler.triggers.cron'), ) APScheduler-2.1.2/tests/0000755000175000017500000000000012266471145013471 5ustar alexalexAPScheduler-2.1.2/tests/testutil.py0000644000175000017500000001311612266453403015717 0ustar alexalex# coding: utf-8 from datetime import date, datetime, timedelta import time import os import sys import shelve from nose.tools import eq_, raises, assert_raises # @UnresolvedImport from nose.plugins.skip import SkipTest from apscheduler.util import * class DummyClass(object): def meth(self): pass @staticmethod def staticmeth(): pass @classmethod def classmeth(cls): pass def __call__(self): pass class InnerDummyClass(object): @classmethod def innerclassmeth(cls): pass def meth(): pass @raises(ValueError) def test_asint_invalid_1(): asint('5s') @raises(ValueError) def test_asint_invalid_2(): asint('shplse') def test_asint_number(): eq_(asint('539'), 539) def test_asint_none(): eq_(asint(None), None) def test_asbool_true(): for val in (' True', 'true ', 'Yes', ' yes ', '1 '): eq_(asbool(val), True) assert asbool(True) is True def test_asbool_false(): for val in (' False', 'false ', 'No', ' no ', '0 '): eq_(asbool(val), False) assert asbool(False) is False @raises(ValueError) def test_asbool_fail(): asbool('yep') def test_convert_datetime_date(): dateval = date(2009, 8, 1) datetimeval = convert_to_datetime(dateval) correct_datetime = datetime(2009, 8, 1) assert isinstance(datetimeval, datetime) eq_(datetimeval, correct_datetime) def test_convert_datetime_passthrough(): datetimeval = datetime(2009, 8, 1, 5, 6, 12) convertedval = convert_to_datetime(datetimeval) eq_(convertedval, datetimeval) def test_convert_datetime_text1(): convertedval = convert_to_datetime('2009-8-1') eq_(convertedval, datetime(2009, 8, 1)) def test_convert_datetime_text2(): convertedval = convert_to_datetime('2009-8-1 5:16:12') eq_(convertedval, datetime(2009, 8, 1, 5, 16, 12)) def test_datestring_parse_datetime_micro(): convertedval = convert_to_datetime('2009-8-1 5:16:12.843821') eq_(convertedval, datetime(2009, 8, 1, 5, 16, 12, 843821)) @raises(TypeError) def test_convert_datetime_invalid(): convert_to_datetime(995302092123) @raises(ValueError) def test_convert_datetime_invalid_str(): convert_to_datetime('19700-12-1') def test_timedelta_seconds(): delta = timedelta(minutes=2, seconds=30) seconds = timedelta_seconds(delta) eq_(seconds, 150) def test_time_difference_positive(): earlier = datetime(2008, 9, 1, second=3) later = datetime(2008, 9, 1, second=49) eq_(time_difference(later, earlier), 46) def test_time_difference_negative(): earlier = datetime(2009, 4, 7, second=7) later = datetime(2009, 4, 7, second=56) eq_(time_difference(earlier, later), -49) class TestDSTTimeDifference(object): def setup(self): if hasattr(time, 'tzset'): os.environ['TZ'] = 'Europe/Helsinki' time.tzset() def teardown(self): if hasattr(time, 'tzset'): del os.environ['TZ'] time.tzset() def test_time_difference_daylight_1(self): earlier = datetime(2010, 3, 28, 2) later = datetime(2010, 3, 28, 4) eq_(time_difference(later, earlier), 3600) def test_time_difference_daylight_2(self): earlier = datetime(2010, 10, 31, 2) later = datetime(2010, 10, 31, 5) eq_(time_difference(later, earlier), 14400) def test_datetime_ceil_round(): dateval = datetime(2009, 4, 7, 2, 10, 16, 4000) correct_answer = datetime(2009, 4, 7, 2, 10, 17) eq_(datetime_ceil(dateval), correct_answer) def test_datetime_ceil_exact(): dateval = datetime(2009, 4, 7, 2, 10, 16) correct_answer = datetime(2009, 4, 7, 2, 10, 16) eq_(datetime_ceil(dateval), correct_answer) def test_combine_opts(): global_opts = {'someprefix.opt1': '123', 'opt2': '456', 'someprefix.opt3': '789'} local_opts = {'opt3': 'abc'} combined = combine_opts(global_opts, 'someprefix.', local_opts) eq_(combined, dict(opt1='123', opt3='abc')) def test_callable_name(): eq_(get_callable_name(test_callable_name), 'test_callable_name') eq_(get_callable_name(DummyClass.staticmeth), 'staticmeth') eq_(get_callable_name(DummyClass.classmeth), 'DummyClass.classmeth') eq_(get_callable_name(DummyClass.meth), 'meth') eq_(get_callable_name(DummyClass().meth), 'DummyClass.meth') eq_(get_callable_name(DummyClass), 'DummyClass') eq_(get_callable_name(DummyClass()), 'DummyClass') assert_raises(TypeError, get_callable_name, object()) def test_obj_to_ref(): assert_raises(ValueError, obj_to_ref, DummyClass.meth) assert_raises(ValueError, obj_to_ref, DummyClass.staticmeth) eq_(obj_to_ref(DummyClass.classmeth), 'testutil:DummyClass.classmeth') eq_(obj_to_ref(shelve.open), 'shelve:open') def test_inner_obj_to_ref(): if sys.version_info < (3, 3): raise SkipTest eq_(obj_to_ref(DummyClass.InnerDummyClass.innerclassmeth), 'testutil:DummyClass.InnerDummyClass.innerclassmeth') def test_ref_to_obj(): eq_(ref_to_obj('shelve:open'), shelve.open) assert_raises(TypeError, ref_to_obj, object()) assert_raises(ValueError, ref_to_obj, 'module') assert_raises(LookupError, ref_to_obj, 'module:blah') def test_maybe_ref(): eq_(maybe_ref('shelve:open'), shelve.open) eq_(maybe_ref(shelve.open), shelve.open) def test_to_unicode(): if sys.version_info[0] < 3: eq_(to_unicode('aaööbb'), unicode('aabb')) eq_(to_unicode(unicode('gfkj')), unicode('gfkj')) else: eq_(to_unicode('aaööbb'.encode('utf-8')), 'aabb') eq_(to_unicode('gfkj'), 'gfkj') APScheduler-2.1.2/tests/testintegration.py0000644000175000017500000001141112266453403017261 0ustar alexalexfrom time import sleep from warnings import filterwarnings, resetwarnings from tempfile import NamedTemporaryFile import os from nose.tools import eq_ from nose.plugins.skip import SkipTest from apscheduler.scheduler import Scheduler from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_MISSED try: from apscheduler.jobstores.shelve_store import ShelveJobStore except ImportError: ShelveJobStore = None try: from apscheduler.jobstores.sqlalchemy_store import SQLAlchemyJobStore except ImportError: SQLAlchemyJobStore = None try: from apscheduler.jobstores.mongodb_store import MongoDBJobStore except ImportError: MongoDBJobStore = None try: from apscheduler.jobstores.redis_store import RedisJobStore except ImportError: RedisJobStore = None def increment(vals, sleeptime): vals[0] += 1 sleep(sleeptime) class IntegrationTestBase(object): def setup(self): self.jobstore = self.make_jobstore() self.scheduler = Scheduler() self.scheduler.add_jobstore(self.jobstore, 'persistent') self.scheduler.start() def test_overlapping_runs(self): # Makes sure that "increment" is only ran once, since it will still be # running when the next appointed time hits. vals = [0] self.scheduler.add_interval_job(increment, jobstore='persistent', seconds=1, args=[vals, 2]) sleep(2.5) eq_(vals, [1]) def test_max_instances(self): vals = [0] events = [] self.scheduler.add_listener(events.append, EVENT_JOB_EXECUTED | EVENT_JOB_MISSED) self.scheduler.add_interval_job( increment, jobstore='persistent', seconds=0.3, max_instances=2, max_runs=4, args=[vals, 1]) sleep(2.4) eq_(vals, [2]) eq_(len(events), 4) eq_(events[0].code, EVENT_JOB_MISSED) eq_(events[1].code, EVENT_JOB_MISSED) eq_(events[2].code, EVENT_JOB_EXECUTED) eq_(events[3].code, EVENT_JOB_EXECUTED) class TestShelveIntegration(IntegrationTestBase): @staticmethod def make_jobstore(): if not ShelveJobStore: raise SkipTest filterwarnings('ignore', category=RuntimeWarning) f = NamedTemporaryFile(prefix='apscheduler_') f.close() resetwarnings() return ShelveJobStore(f.name) def test_overlapping_runs(self): """Shelve/test_overlapping_runs""" IntegrationTestBase.test_overlapping_runs(self) def test_max_instances(self): """Shelve/test_max_instances""" IntegrationTestBase.test_max_instances(self) def teardown(self): self.scheduler.shutdown() self.jobstore.close() if os.path.exists(self.jobstore.path): os.remove(self.jobstore.path) class TestSQLAlchemyIntegration(IntegrationTestBase): @staticmethod def make_jobstore(): if not SQLAlchemyJobStore: raise SkipTest return SQLAlchemyJobStore(url='sqlite:///example.sqlite') def test_overlapping_runs(self): """SQLAlchemy/test_overlapping_runs""" IntegrationTestBase.test_overlapping_runs(self) def test_max_instances(self): """SQLAlchemy/test_max_instances""" IntegrationTestBase.test_max_instances(self) def teardown(self): self.scheduler.shutdown() self.jobstore.close() if os.path.exists('example.sqlite'): os.remove('example.sqlite') class TestMongoDBIntegration(IntegrationTestBase): @staticmethod def make_jobstore(): if not MongoDBJobStore: raise SkipTest return MongoDBJobStore(database='apscheduler_unittest') def test_overlapping_runs(self): """MongoDB/test_overlapping_runs""" IntegrationTestBase.test_overlapping_runs(self) def test_max_instances(self): """MongoDB/test_max_instances""" IntegrationTestBase.test_max_instances(self) def teardown(self): self.scheduler.shutdown() connection = self.jobstore.collection.database.connection connection.drop_database(self.jobstore.collection.database.name) self.jobstore.close() class TestRedisIntegration(IntegrationTestBase): @staticmethod def make_jobstore(): if not RedisJobStore: raise SkipTest return RedisJobStore() def test_overlapping_runs(self): """Redis/test_overlapping_runs""" IntegrationTestBase.test_overlapping_runs(self) def test_max_instances(self): """Redis/test_max_instances""" IntegrationTestBase.test_max_instances(self) def teardown(self): self.scheduler.shutdown() self.jobstore.redis.flushdb() self.jobstore.close() APScheduler-2.1.2/tests/testtriggers.py0000644000175000017500000001660112266453403016572 0ustar alexalexfrom datetime import datetime, timedelta from nose.tools import eq_, raises from apscheduler.triggers import CronTrigger, SimpleTrigger, IntervalTrigger def test_cron_trigger_1(): trigger = CronTrigger(year='2009/2', month='1/3', day='5-13') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009/2', month='1/3', day='5-13']") start_date = datetime(2008, 12, 1) correct_next_date = datetime(2009, 1, 5) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_trigger_2(): trigger = CronTrigger(year='2009/2', month='1/3', day='5-13') start_date = datetime(2009, 10, 14) correct_next_date = datetime(2011, 1, 5) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_trigger_3(): trigger = CronTrigger(year='2009', month='2', hour='8-10') eq_(repr(trigger), "") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 2, 1, 8) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_trigger_4(): trigger = CronTrigger(year='2012', month='2', day='last') eq_(repr(trigger), "") start_date = datetime(2012, 2, 1) correct_next_date = datetime(2012, 2, 29) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_zero_value(): trigger = CronTrigger(year=2009, month=2, hour=0) eq_(repr(trigger), "") def test_cron_year_list(): trigger = CronTrigger(year='2009,2008') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009,2008']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 1, 1) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_start_date(): trigger = CronTrigger(year='2009', month='2', hour='8-10', start_date='2009-02-03 11:00:00') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', month='2', hour='8-10']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 2, 4, 8) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_weekday_overlap(): trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week='2-4') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', month='1', day='6-10', day_of_week='2-4']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 1, 7) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_weekday_nomatch(): trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week='0,6') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', month='1', day='6-10', day_of_week='0,6']") start_date = datetime(2009, 1, 1) correct_next_date = None eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_weekday_positional(): trigger = CronTrigger(year=2009, month=1, day='4th wed') eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', month='1', day='4th wed']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 1, 28) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_week_1(): trigger = CronTrigger(year=2009, month=2, week=8) eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', month='2', week='8']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 2, 16) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_week_2(): trigger = CronTrigger(year=2009, week=15, day_of_week=2) eq_(repr(trigger), "") eq_(str(trigger), "cron[year='2009', week='15', day_of_week='2']") start_date = datetime(2009, 1, 1) correct_next_date = datetime(2009, 4, 8) eq_(trigger.get_next_fire_time(start_date), correct_next_date) def test_cron_extra_coverage(): # This test has no value other than patching holes in test coverage trigger = CronTrigger(day='6,8') eq_(repr(trigger), "") eq_(str(trigger), "cron[day='6,8']") start_date = datetime(2009, 12, 31) correct_next_date = datetime(2010, 1, 6) eq_(trigger.get_next_fire_time(start_date), correct_next_date) @raises(ValueError) def test_cron_faulty_expr(): CronTrigger(year='2009-fault') def test_cron_increment_weekday(): # Makes sure that incrementing the weekday field in the process of # calculating the next matching date won't cause problems trigger = CronTrigger(hour='5-6') eq_(repr(trigger), "") eq_(str(trigger), "cron[hour='5-6']") start_date = datetime(2009, 9, 25, 7) correct_next_date = datetime(2009, 9, 26, 5) eq_(trigger.get_next_fire_time(start_date), correct_next_date) @raises(TypeError) def test_cron_bad_kwarg(): CronTrigger(second=0, third=1) def test_date_trigger_earlier(): fire_date = datetime(2009, 7, 6) trigger = SimpleTrigger(fire_date) eq_(repr(trigger), "") eq_(str(trigger), "date[2009-07-06 00:00:00]") start_date = datetime(2008, 12, 1) eq_(trigger.get_next_fire_time(start_date), fire_date) def test_date_trigger_exact(): fire_date = datetime(2009, 7, 6) trigger = SimpleTrigger(fire_date) start_date = datetime(2009, 7, 6) eq_(trigger.get_next_fire_time(start_date), fire_date) def test_date_trigger_later(): fire_date = datetime(2009, 7, 6) trigger = SimpleTrigger(fire_date) start_date = datetime(2009, 7, 7) eq_(trigger.get_next_fire_time(start_date), None) def test_date_trigger_text(): trigger = SimpleTrigger('2009-7-6') start_date = datetime(2009, 7, 6) eq_(trigger.get_next_fire_time(start_date), datetime(2009, 7, 6)) @raises(TypeError) def test_interval_invalid_interval(): IntervalTrigger('1-6') class TestInterval(object): def setUp(self): interval = timedelta(seconds=1) trigger_start_date = datetime(2009, 8, 4, second=2) self.trigger = IntervalTrigger(interval, trigger_start_date) def test_interval_repr(self): eq_(repr(self.trigger), "") eq_(str(self.trigger), "interval[0:00:01]") def test_interval_before(self): start_date = datetime(2009, 8, 4) correct_next_date = datetime(2009, 8, 4, second=2) eq_(self.trigger.get_next_fire_time(start_date), correct_next_date) def test_interval_within(self): start_date = datetime(2009, 8, 4, second=2, microsecond=1000) correct_next_date = datetime(2009, 8, 4, second=3) eq_(self.trigger.get_next_fire_time(start_date), correct_next_date) APScheduler-2.1.2/tests/testscheduler.py0000644000175000017500000003441212266453403016722 0ustar alexalexfrom datetime import datetime, timedelta from logging import StreamHandler, ERROR from threading import Thread from copy import copy import time import os from nose.tools import eq_, raises from apscheduler.jobstores.ram_store import RAMJobStore from apscheduler.scheduler import Scheduler, SchedulerAlreadyRunningError from apscheduler.job import Job from apscheduler.events import (EVENT_JOB_EXECUTED, SchedulerEvent, EVENT_SCHEDULER_START, EVENT_SCHEDULER_SHUTDOWN, EVENT_JOB_MISSED) from apscheduler import scheduler try: from StringIO import StringIO except ImportError: from io import StringIO class TestOfflineScheduler(object): def setup(self): self.scheduler = Scheduler() def teardown(self): if self.scheduler.running: self.scheduler.shutdown() @raises(KeyError) def test_jobstore_twice(self): self.scheduler.add_jobstore(RAMJobStore(), 'dummy') self.scheduler.add_jobstore(RAMJobStore(), 'dummy') def test_add_tentative_job(self): job = self.scheduler.add_date_job(lambda: None, datetime(2200, 7, 24), jobstore='dummy') assert isinstance(job, Job) eq_(self.scheduler.get_jobs(), []) def test_configure_jobstore(self): conf = {'apscheduler.jobstore.ramstore.class': 'apscheduler.jobstores.ram_store:RAMJobStore'} self.scheduler.configure(conf) self.scheduler.remove_jobstore('ramstore') def test_shutdown_offline(self): self.scheduler.shutdown() def test_configure_no_prefix(self): global_options = {'misfire_grace_time': '2', 'daemonic': 'false'} self.scheduler.configure(global_options) eq_(self.scheduler.misfire_grace_time, 1) eq_(self.scheduler.daemonic, True) def test_configure_prefix(self): global_options = {'apscheduler.misfire_grace_time': 2, 'apscheduler.daemonic': False} self.scheduler.configure(global_options) eq_(self.scheduler.misfire_grace_time, 2) eq_(self.scheduler.daemonic, False) def test_add_listener(self): val = [] self.scheduler.add_listener(val.append) event = SchedulerEvent(EVENT_SCHEDULER_START) self.scheduler._notify_listeners(event) eq_(len(val), 1) eq_(val[0], event) event = SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN) self.scheduler._notify_listeners(event) eq_(len(val), 2) eq_(val[1], event) self.scheduler.remove_listener(val.append) self.scheduler._notify_listeners(event) eq_(len(val), 2) def test_pending_jobs(self): # Tests that pending jobs are properly added to the jobs list when # the scheduler is started (and not before!) self.scheduler.add_date_job(lambda: None, datetime(9999, 9, 9)) eq_(self.scheduler.get_jobs(), []) self.scheduler.start() jobs = self.scheduler.get_jobs() eq_(len(jobs), 1) class FakeThread(object): def isAlive(self): return True def join(self): pass class FakeThreadPool(object): def submit(self, func, *args, **kwargs): func(*args, **kwargs) def shutdown(self, wait): pass class DummyException(Exception): pass original_now = datetime(2011, 4, 3, 18, 40) class FakeDateTime(datetime): _now = original_now @classmethod def now(cls): return cls._now class TestJobExecution(object): def setup(self): self.scheduler = Scheduler(threadpool=FakeThreadPool()) self.scheduler.add_jobstore(RAMJobStore(), 'default') self.scheduler._stopped = False # Make the scheduler think it's running self.scheduler._thread = FakeThread() self.logstream = StringIO() self.loghandler = StreamHandler(self.logstream) self.loghandler.setLevel(ERROR) scheduler.logger.addHandler(self.loghandler) def teardown(self): scheduler.logger.removeHandler(self.loghandler) if scheduler.datetime == FakeDateTime: scheduler.datetime = datetime FakeDateTime._now = original_now @raises(TypeError) def test_noncallable(self): date = datetime.now() + timedelta(days=1) self.scheduler.add_date_job('wontwork', date) def test_job_name(self): def my_job(): pass job = self.scheduler.add_interval_job(my_job, start_date=datetime(2010, 5, 19)) eq_(repr(job), ')>') def test_schedule_object(self): # Tests that any callable object is accepted (and not just functions) class A: def __init__(self): self.val = 0 def __call__(self): self.val += 1 a = A() job = self.scheduler.add_interval_job(a, seconds=1) self.scheduler._process_jobs(job.next_run_time) self.scheduler._process_jobs(job.next_run_time) eq_(a.val, 2) def test_schedule_method(self): # Tests that bound methods can be scheduled (at least with RAMJobStore) class A: def __init__(self): self.val = 0 def method(self): self.val += 1 a = A() job = self.scheduler.add_interval_job(a.method, seconds=1) self.scheduler._process_jobs(job.next_run_time) self.scheduler._process_jobs(job.next_run_time) eq_(a.val, 2) def test_unschedule_job(self): def increment(): vals[0] += 1 vals = [0] job = self.scheduler.add_cron_job(increment) self.scheduler._process_jobs(job.next_run_time) eq_(vals[0], 1) self.scheduler.unschedule_job(job) self.scheduler._process_jobs(job.next_run_time) eq_(vals[0], 1) def test_unschedule_func(self): def increment(): vals[0] += 1 def increment2(): vals[0] += 1 vals = [0] job1 = self.scheduler.add_cron_job(increment) job2 = self.scheduler.add_cron_job(increment2) job3 = self.scheduler.add_cron_job(increment) eq_(self.scheduler.get_jobs(), [job1, job2, job3]) self.scheduler.unschedule_func(increment) eq_(self.scheduler.get_jobs(), [job2]) @raises(KeyError) def test_unschedule_func_notfound(self): self.scheduler.unschedule_func(copy) def test_job_finished(self): def increment(): vals[0] += 1 vals = [0] job = self.scheduler.add_interval_job(increment, max_runs=1) self.scheduler._process_jobs(job.next_run_time) eq_(vals, [1]) assert job not in self.scheduler.get_jobs() def test_job_exception(self): def failure(): raise DummyException job = self.scheduler.add_date_job(failure, datetime(9999, 9, 9)) self.scheduler._process_jobs(job.next_run_time) assert 'DummyException' in self.logstream.getvalue() def test_misfire_grace_time(self): self.scheduler.misfire_grace_time = 3 job = self.scheduler.add_interval_job(lambda: None, seconds=1) eq_(job.misfire_grace_time, 3) job = self.scheduler.add_interval_job(lambda: None, seconds=1, misfire_grace_time=2) eq_(job.misfire_grace_time, 2) def test_coalesce_on(self): # Makes sure that the job is only executed once when it is scheduled # to be executed twice in a row def increment(): vals[0] += 1 vals = [0] events = [] scheduler.datetime = FakeDateTime self.scheduler.add_listener(events.append, EVENT_JOB_EXECUTED | EVENT_JOB_MISSED) job = self.scheduler.add_interval_job( increment, seconds=1, start_date=FakeDateTime.now(), coalesce=True, misfire_grace_time=2) # Turn the clock 14 seconds forward FakeDateTime._now += timedelta(seconds=2) self.scheduler._process_jobs(FakeDateTime.now()) eq_(job.runs, 1) eq_(len(events), 1) eq_(events[0].code, EVENT_JOB_EXECUTED) eq_(vals, [1]) def test_coalesce_off(self): # Makes sure that every scheduled run for the job is executed even # when they are in the past (but still within misfire_grace_time) def increment(): vals[0] += 1 vals = [0] events = [] scheduler.datetime = FakeDateTime self.scheduler.add_listener(events.append, EVENT_JOB_EXECUTED | EVENT_JOB_MISSED) job = self.scheduler.add_interval_job( increment, seconds=1, start_date=FakeDateTime.now(), coalesce=False, misfire_grace_time=2) # Turn the clock 2 seconds forward FakeDateTime._now += timedelta(seconds=2) self.scheduler._process_jobs(FakeDateTime.now()) eq_(job.runs, 3) eq_(len(events), 3) eq_(events[0].code, EVENT_JOB_EXECUTED) eq_(events[1].code, EVENT_JOB_EXECUTED) eq_(events[2].code, EVENT_JOB_EXECUTED) eq_(vals, [3]) def test_interval(self): def increment(amount): vals[0] += amount vals[1] += 1 vals = [0, 0] job = self.scheduler.add_interval_job(increment, seconds=1, args=[2]) self.scheduler._process_jobs(job.next_run_time) self.scheduler._process_jobs(job.next_run_time) eq_(vals, [4, 2]) def test_interval_schedule(self): @self.scheduler.interval_schedule(seconds=1) def increment(): vals[0] += 1 vals = [0] start = increment.job.next_run_time self.scheduler._process_jobs(start) self.scheduler._process_jobs(start + timedelta(seconds=1)) eq_(vals, [2]) def test_cron(self): def increment(amount): vals[0] += amount vals[1] += 1 vals = [0, 0] job = self.scheduler.add_cron_job(increment, args=[3]) start = job.next_run_time self.scheduler._process_jobs(start) eq_(vals, [3, 1]) self.scheduler._process_jobs(start + timedelta(seconds=1)) eq_(vals, [6, 2]) self.scheduler._process_jobs(start + timedelta(seconds=2)) eq_(vals, [9, 3]) def test_cron_schedule_1(self): @self.scheduler.cron_schedule() def increment(): vals[0] += 1 vals = [0] start = increment.job.next_run_time self.scheduler._process_jobs(start) self.scheduler._process_jobs(start + timedelta(seconds=1)) eq_(vals[0], 2) def test_cron_schedule_2(self): @self.scheduler.cron_schedule(minute='*') def increment(): vals[0] += 1 vals = [0] start = increment.job.next_run_time next_run = start + timedelta(seconds=60) eq_(increment.job.get_run_times(next_run), [start, next_run]) self.scheduler._process_jobs(start) self.scheduler._process_jobs(next_run) eq_(vals[0], 2) def test_date(self): def append_val(value): vals.append(value) vals = [] date = datetime.now() + timedelta(seconds=1) self.scheduler.add_date_job(append_val, date, kwargs={'value': 'test'}) self.scheduler._process_jobs(date) eq_(vals, ['test']) def test_print_jobs(self): out = StringIO() self.scheduler.print_jobs(out) expected = 'Jobstore default:%s'\ ' No scheduled jobs%s' % (os.linesep, os.linesep) eq_(out.getvalue(), expected) self.scheduler.add_date_job(copy, datetime(2200, 5, 19)) out = StringIO() self.scheduler.print_jobs(out) expected = 'Jobstore default:%s '\ 'copy (trigger: date[2200-05-19 00:00:00], '\ 'next run at: 2200-05-19 00:00:00)%s' % (os.linesep, os.linesep) eq_(out.getvalue(), expected) def test_jobstore(self): self.scheduler.add_jobstore(RAMJobStore(), 'dummy') job = self.scheduler.add_date_job(lambda: None, datetime(2200, 7, 24), jobstore='dummy') eq_(self.scheduler.get_jobs(), [job]) self.scheduler.remove_jobstore('dummy') eq_(self.scheduler.get_jobs(), []) @raises(KeyError) def test_remove_nonexistent_jobstore(self): self.scheduler.remove_jobstore('dummy2') def test_job_next_run_time(self): # Tests against bug #5 def increment(): vars[0] += 1 vars = [0] scheduler.datetime = FakeDateTime job = self.scheduler.add_interval_job( increment, seconds=1, misfire_grace_time=3, start_date=FakeDateTime.now()) start = job.next_run_time self.scheduler._process_jobs(start) eq_(vars, [1]) self.scheduler._process_jobs(start) eq_(vars, [1]) self.scheduler._process_jobs(start + timedelta(seconds=1)) eq_(vars, [2]) class TestRunningScheduler(object): def setup(self): self.scheduler = Scheduler() self.scheduler.start() def teardown(self): if self.scheduler.running: self.scheduler.shutdown() def test_shutdown_timeout(self): self.scheduler.shutdown() @raises(SchedulerAlreadyRunningError) def test_scheduler_double_start(self): self.scheduler.start() @raises(SchedulerAlreadyRunningError) def test_scheduler_configure_running(self): self.scheduler.configure({}) def test_scheduler_double_shutdown(self): self.scheduler.shutdown() self.scheduler.shutdown(False) class TestStandaloneScheduler(object): def setup(self): self.scheduler = Scheduler(standalone=True) self.scheduler.add_date_job(lambda: None, datetime.now() + timedelta(1)) self.thread = Thread(target=self.scheduler.start) self.thread.start() def teardown(self): self.scheduler.shutdown(True) self.thread.join(3) def test_scheduler_running(self): time.sleep(.1) eq_(self.scheduler.running, True) APScheduler-2.1.2/tests/testexpressions.py0000644000175000017500000001125612266453346017335 0ustar alexalexfrom datetime import datetime from nose.tools import eq_, raises from apscheduler.triggers.cron.expressions import * from apscheduler.triggers.cron.fields import * def test_all_expression(): field = DayOfMonthField('day', '*') eq_(repr(field), "DayOfMonthField('day', '*')") date = datetime(2009, 7, 1) eq_(field.get_next_value(date), 1) date = datetime(2009, 7, 10) eq_(field.get_next_value(date), 10) date = datetime(2009, 7, 30) eq_(field.get_next_value(date), 30) def test_all_expression_step(): field = BaseField('hour', '*/3') eq_(repr(field), "BaseField('hour', '*/3')") date = datetime(2009, 7, 1, 0) eq_(field.get_next_value(date), 0) date = datetime(2009, 7, 1, 2) eq_(field.get_next_value(date), 3) date = datetime(2009, 7, 1, 7) eq_(field.get_next_value(date), 9) @raises(ValueError) def test_all_expression_invalid(): BaseField('hour', '*/0') def test_all_expression_repr(): expr = AllExpression() eq_(repr(expr), 'AllExpression(None)') def test_all_expression_step_repr(): expr = AllExpression(2) eq_(repr(expr), "AllExpression(2)") def test_range_expression(): field = DayOfMonthField('day', '2-9') eq_(repr(field), "DayOfMonthField('day', '2-9')") date = datetime(2009, 7, 1) eq_(field.get_next_value(date), 2) date = datetime(2009, 7, 10) eq_(field.get_next_value(date), None) date = datetime(2009, 7, 5) eq_(field.get_next_value(date), 5) def test_range_expression_step(): field = DayOfMonthField('day', '2-9/3') eq_(repr(field), "DayOfMonthField('day', '2-9/3')") date = datetime(2009, 7, 1) eq_(field.get_next_value(date), 2) date = datetime(2009, 7, 3) eq_(field.get_next_value(date), 5) date = datetime(2009, 7, 9) eq_(field.get_next_value(date), None) def test_range_expression_single(): field = DayOfMonthField('day', 9) eq_(repr(field), "DayOfMonthField('day', '9')") date = datetime(2009, 7, 1) eq_(field.get_next_value(date), 9) date = datetime(2009, 7, 9) eq_(field.get_next_value(date), 9) date = datetime(2009, 7, 10) eq_(field.get_next_value(date), None) @raises(ValueError) def test_range_expression_invalid(): DayOfMonthField('day', '5-3') def test_range_expression_repr(): expr = RangeExpression(3, 7) eq_(repr(expr), 'RangeExpression(3, 7)') def test_range_expression_single_repr(): expr = RangeExpression(4) eq_(repr(expr), 'RangeExpression(4)') def test_range_expression_step_repr(): expr = RangeExpression(3, 7, 2) eq_(repr(expr), 'RangeExpression(3, 7, 2)') def test_weekday_single(): field = DayOfWeekField('day_of_week', 'WED') eq_(repr(field), "DayOfWeekField('day_of_week', 'wed')") date = datetime(2008, 2, 4) eq_(field.get_next_value(date), 2) def test_weekday_range(): field = DayOfWeekField('day_of_week', 'TUE-SAT') eq_(repr(field), "DayOfWeekField('day_of_week', 'tue-sat')") date = datetime(2008, 2, 7) eq_(field.get_next_value(date), 3) def test_weekday_pos_1(): expr = WeekdayPositionExpression('1st', 'Fri') eq_(str(expr), '1st fri') date = datetime(2008, 2, 1) eq_(expr.get_next_value(date, 'day'), 1) def test_weekday_pos_2(): expr = WeekdayPositionExpression('2nd', 'wed') eq_(str(expr), '2nd wed') date = datetime(2008, 2, 1) eq_(expr.get_next_value(date, 'day'), 13) def test_weekday_pos_3(): expr = WeekdayPositionExpression('last', 'fri') eq_(str(expr), 'last fri') date = datetime(2008, 2, 1) eq_(expr.get_next_value(date, 'day'), 29) @raises(ValueError) def test_day_of_week_invalid_pos(): WeekdayPositionExpression('6th', 'fri') @raises(ValueError) def test_day_of_week_invalid_name(): WeekdayPositionExpression('1st', 'moh') def test_weekday_position_expression_repr(): expr = WeekdayPositionExpression('2nd', 'FRI') eq_(repr(expr), "WeekdayPositionExpression('2nd', 'fri')") @raises(ValueError) def test_day_of_week_invalid_first(): WeekdayRangeExpression('moh', 'fri') @raises(ValueError) def test_day_of_week_invalid_last(): WeekdayRangeExpression('mon', 'fre') def test_weekday_range_expression_repr(): expr = WeekdayRangeExpression('tue', 'SUN') eq_(repr(expr), "WeekdayRangeExpression('tue', 'sun')") def test_weekday_range_expression_single_repr(): expr = WeekdayRangeExpression('thu') eq_(repr(expr), "WeekdayRangeExpression('thu')") def test_last_day_of_month_expression(): expr = LastDayOfMonthExpression() date = datetime(2012, 2, 1) eq_(expr.get_next_value(date, 'day'), 29) def test_last_day_of_month_expression_invalid(): expr = LastDayOfMonthExpression() eq_(repr(expr), "LastDayOfMonthExpression()") APScheduler-2.1.2/tests/testjobstores.py0000644000175000017500000001370112266453403016754 0ustar alexalexfrom datetime import datetime from warnings import filterwarnings, resetwarnings from tempfile import NamedTemporaryFile import os from nose.tools import eq_, assert_raises, raises # @UnresolvedImport from nose.plugins.skip import SkipTest from apscheduler.jobstores.ram_store import RAMJobStore from apscheduler.jobstores.base import JobStore from apscheduler.triggers import SimpleTrigger from apscheduler.job import Job try: from apscheduler.jobstores.shelve_store import ShelveJobStore except ImportError: ShelveJobStore = None try: from apscheduler.jobstores.sqlalchemy_store import SQLAlchemyJobStore except ImportError: SQLAlchemyJobStore = None try: from apscheduler.jobstores.mongodb_store import MongoDBJobStore except ImportError: MongoDBJobStore = None try: from apscheduler.jobstores.redis_store import RedisJobStore except ImportError: RedisJobStore = None def dummy_job(): pass def dummy_job2(): pass def dummy_job3(): pass class JobStoreTestBase(object): def setup(self): self.trigger_date = datetime(2999, 1, 1) self.earlier_date = datetime(2998, 12, 31) self.trigger = SimpleTrigger(self.trigger_date) self.job = Job(self.trigger, dummy_job, [], {}, 1, False) self.job.next_run_time = self.trigger_date def test_jobstore_add_update_remove(self): eq_(self.jobstore.jobs, []) self.jobstore.add_job(self.job) eq_(self.jobstore.jobs, [self.job]) eq_(self.jobstore.jobs[0], self.job) eq_(self.jobstore.jobs[0].runs, 0) self.job.runs += 1 self.jobstore.update_job(self.job) self.jobstore.load_jobs() eq_(len(self.jobstore.jobs), 1) eq_(self.jobstore.jobs, [self.job]) eq_(self.jobstore.jobs[0].runs, 1) self.jobstore.remove_job(self.job) eq_(self.jobstore.jobs, []) self.jobstore.load_jobs() eq_(self.jobstore.jobs, []) class PersistentJobstoreTestBase(JobStoreTestBase): def test_one_job_fails_to_load(self): global dummy_job2, dummy_job_temp job1 = Job(self.trigger, dummy_job, [], {}, 1, False) job2 = Job(self.trigger, dummy_job2, [], {}, 1, False) job3 = Job(self.trigger, dummy_job3, [], {}, 1, False) for job in job1, job2, job3: job.next_run_time = self.trigger_date self.jobstore.add_job(job) dummy_job_temp = dummy_job2 del dummy_job2 try: self.jobstore.load_jobs() eq_(len(self.jobstore.jobs), 2) finally: dummy_job2 = dummy_job_temp del dummy_job_temp class TestRAMJobStore(JobStoreTestBase): @classmethod def setup_class(cls): cls.jobstore = RAMJobStore() def test_repr(self): eq_(repr(self.jobstore), '') class TestShelveJobStore(PersistentJobstoreTestBase): @classmethod def setup_class(cls): if not ShelveJobStore: raise SkipTest filterwarnings('ignore', category=RuntimeWarning) f = NamedTemporaryFile(prefix='apscheduler_') f.close() resetwarnings() cls.jobstore = ShelveJobStore(f.name) @classmethod def teardown_class(cls): cls.jobstore.close() if os.path.exists(cls.jobstore.path): os.remove(cls.jobstore.path) def test_repr(self): eq_(repr(self.jobstore), '' % self.jobstore.path) class TestSQLAlchemyJobStore1(PersistentJobstoreTestBase): @classmethod def setup_class(cls): if not SQLAlchemyJobStore: raise SkipTest cls.jobstore = SQLAlchemyJobStore(url='sqlite:///') @classmethod def teardown_class(cls): cls.jobstore.close() def test_repr(self): eq_(repr(self.jobstore), '') class TestSQLAlchemyJobStore2(PersistentJobstoreTestBase): @classmethod def setup_class(cls): if not SQLAlchemyJobStore: raise SkipTest from sqlalchemy import create_engine engine = create_engine('sqlite:///') cls.jobstore = SQLAlchemyJobStore(engine=engine) @classmethod def teardown_class(cls): cls.jobstore.close() def test_repr(self): eq_(repr(self.jobstore), '') class TestMongoDBJobStore(PersistentJobstoreTestBase): @classmethod def setup_class(cls): if not MongoDBJobStore: raise SkipTest cls.jobstore = MongoDBJobStore(database='apscheduler_unittest') @classmethod def teardown_class(cls): connection = cls.jobstore.collection.database.connection connection.drop_database(cls.jobstore.collection.database.name) cls.jobstore.close() def test_repr(self): eq_(repr(self.jobstore), "") class TestRedisJobStore(PersistentJobstoreTestBase): @classmethod def setup_class(cls): if not RedisJobStore: raise SkipTest cls.jobstore = RedisJobStore() @classmethod def teardown_class(cls): cls.jobstore.redis.flushdb() cls.jobstore.close() def test_repr(self): eq_(repr(self.jobstore), "") @raises(ValueError) def test_sqlalchemy_invalid_args(): if not SQLAlchemyJobStore: raise SkipTest SQLAlchemyJobStore() def test_sqlalchemy_alternate_tablename(): if not SQLAlchemyJobStore: raise SkipTest store = SQLAlchemyJobStore('sqlite:///', tablename='test_table') eq_(store.jobs_t.name, 'test_table') def test_unimplemented_job_store(): class DummyJobStore(JobStore): pass jobstore = DummyJobStore() assert_raises(NotImplementedError, jobstore.add_job, None) assert_raises(NotImplementedError, jobstore.update_job, None) assert_raises(NotImplementedError, jobstore.remove_job, None) assert_raises(NotImplementedError, jobstore.load_jobs) APScheduler-2.1.2/tests/testjob.py0000644000175000017500000001202312266453403015510 0ustar alexalexfrom datetime import datetime, timedelta from threading import Lock from nose.tools import eq_, raises, assert_raises # @UnresolvedImport from apscheduler.job import Job, MaxInstancesReachedError from apscheduler.triggers.simple import SimpleTrigger from apscheduler.triggers.interval import IntervalTrigger lock_type = type(Lock()) def dummyfunc(): pass class TestJob(object): RUNTIME = datetime(2010, 12, 13, 0, 8, 0) def setup(self): self.trigger = SimpleTrigger(self.RUNTIME) self.job = Job(self.trigger, dummyfunc, [], {}, 1, False) def test_compute_next_run_time(self): self.job.compute_next_run_time( self.RUNTIME - timedelta(microseconds=1)) eq_(self.job.next_run_time, self.RUNTIME) self.job.compute_next_run_time(self.RUNTIME) eq_(self.job.next_run_time, self.RUNTIME) self.job.compute_next_run_time( self.RUNTIME + timedelta(microseconds=1)) eq_(self.job.next_run_time, None) def test_compute_run_times(self): expected_times = [self.RUNTIME + timedelta(seconds=1), self.RUNTIME + timedelta(seconds=2)] self.job.trigger = IntervalTrigger(timedelta(seconds=1), self.RUNTIME) self.job.compute_next_run_time(expected_times[0]) eq_(self.job.next_run_time, expected_times[0]) run_times = self.job.get_run_times(self.RUNTIME) eq_(run_times, []) run_times = self.job.get_run_times(expected_times[0]) eq_(run_times, [expected_times[0]]) run_times = self.job.get_run_times(expected_times[1]) eq_(run_times, expected_times) def test_max_runs(self): self.job.max_runs = 1 self.job.runs += 1 self.job.compute_next_run_time(self.RUNTIME) eq_(self.job.next_run_time, None) def test_eq_num(self): # Just increasing coverage here assert not self.job == 'dummyfunc' def test_getstate(self): state = self.job.__getstate__() eq_(state, dict(trigger=self.trigger, func_ref='testjob:dummyfunc', name='dummyfunc', args=[], kwargs={}, misfire_grace_time=1, coalesce=False, max_runs=None, max_instances=1, runs=0)) def test_setstate(self): trigger = SimpleTrigger('2010-12-14 13:05:00') state = dict(trigger=trigger, name='testjob.dummyfunc', func_ref='testjob:dummyfunc', args=[], kwargs={}, misfire_grace_time=2, max_runs=2, coalesce=True, max_instances=2, runs=1) self.job.__setstate__(state) eq_(self.job.trigger, trigger) eq_(self.job.func, dummyfunc) eq_(self.job.max_runs, 2) eq_(self.job.coalesce, True) eq_(self.job.max_instances, 2) eq_(self.job.runs, 1) assert not hasattr(self.job, 'func_ref') assert isinstance(self.job._lock, lock_type) def test_jobs_equal(self): assert self.job == self.job job2 = Job(SimpleTrigger(self.RUNTIME), lambda: None, [], {}, 1, False) assert self.job != job2 job2.id = self.job.id = 123 eq_(self.job, job2) assert self.job != 'bleh' def test_instances(self): self.job.max_instances = 2 eq_(self.job.instances, 0) self.job.add_instance() eq_(self.job.instances, 1) self.job.add_instance() eq_(self.job.instances, 2) assert_raises(MaxInstancesReachedError, self.job.add_instance) self.job.remove_instance() eq_(self.job.instances, 1) self.job.remove_instance() eq_(self.job.instances, 0) assert_raises(AssertionError, self.job.remove_instance) def test_repr(self): self.job.compute_next_run_time(self.RUNTIME) eq_(repr(self.job), ")>") eq_(str(self.job), "dummyfunc (trigger: date[2010-12-13 00:08:00], " "next run at: 2010-12-13 00:08:00)") @raises(ValueError) def test_create_job_no_trigger(): Job(None, lambda: None, [], {}, 1, False) @raises(TypeError) def test_create_job_invalid_func(): Job(SimpleTrigger(datetime.now()), 'bleh', [], {}, 1, False) @raises(TypeError) def test_create_job_invalid_args(): Job(SimpleTrigger(datetime.now()), lambda: None, None, {}, 1, False) @raises(TypeError) def test_create_job_invalid_kwargs(): Job(SimpleTrigger(datetime.now()), lambda: None, [], None, 1, False) @raises(ValueError) def test_create_job_invalid_misfire(): Job(SimpleTrigger(datetime.now()), lambda: None, [], {}, 0, False) @raises(ValueError) def test_create_job_invalid_maxruns(): Job(SimpleTrigger(datetime.now()), lambda: None, [], {}, 1, False, max_runs=0) @raises(ValueError) def test_create_job_invalid_maxinstances(): Job(SimpleTrigger(datetime.now()), lambda: None, [], {}, 1, False, max_instances=0) APScheduler-2.1.2/tests/testthreadpool.py0000644000175000017500000000260012266453346017105 0ustar alexalexfrom threading import Event from time import sleep from nose.tools import eq_, assert_raises # @UnresolvedImport from apscheduler.threadpool import ThreadPool def test_threadpool(): pool = ThreadPool(core_threads=2, keepalive=0) event1 = Event() event2 = Event() event3 = Event() pool.submit(event1.set) pool.submit(event2.set) pool.submit(event3.set) event1.wait(1) event2.wait(1) event3.wait(1) assert event1.isSet() assert event2.isSet() assert event3.isSet() sleep(0.3) eq_(repr(pool), '' % id(pool)) pool.shutdown() eq_(repr(pool), '' % id(pool)) # Make sure double shutdown is ok pool.shutdown() # Make sure one can't submit tasks to a thread pool that has been shut down assert_raises(RuntimeError, pool.submit, event1.set) def test_threadpool_maxthreads(): pool = ThreadPool(core_threads=2, max_threads=1) eq_(pool.max_threads, 2) pool = ThreadPool(core_threads=2, max_threads=3) eq_(pool.max_threads, 3) pool = ThreadPool(core_threads=0, max_threads=0) eq_(pool.max_threads, 1) def test_threadpool_nocore(): pool = ThreadPool(keepalive=0) event = Event() pool.submit(event.set) event.wait(1) assert event.isSet() sleep(1) eq_(repr(pool), '' % id(pool)) APScheduler-2.1.2/PKG-INFO0000644000175000017500000000626412266471145013434 0ustar alexalexMetadata-Version: 1.0 Name: APScheduler Version: 2.1.2 Summary: In-process task scheduler with Cron-like capabilities Home-page: http://pypi.python.org/pypi/APScheduler/ Author: Alex Gronholm Author-email: apscheduler@nextday.fi License: MIT Description: Advanced Python Scheduler (APScheduler) is a light but powerful in-process task scheduler that lets you schedule jobs (functions or any python callables) to be executed at times of your choosing. This can be a far better alternative to externally run cron scripts for long-running applications (e.g. web applications), as it is platform neutral and can directly access your application's variables and functions. The development of APScheduler was heavily influenced by the `Quartz `_ task scheduler written in Java. APScheduler provides most of the major features that Quartz does, but it also provides features not present in Quartz (such as multiple job stores). Features ======== * No (hard) external dependencies * Thread-safe API * Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 2.2) * Configurable scheduling mechanisms (triggers): * Cron-like scheduling * Delayed scheduling of single run jobs (like the UNIX "at" command) * Interval-based (run a job at specified time intervals) * Multiple, simultaneously active job stores: * RAM * File-based simple database (shelve) * `SQLAlchemy `_ (any supported RDBMS works) * `MongoDB `_ * `Redis `_ Documentation ============= Documentation can be found `here `_. Source ====== The source can be browsed at `Bitbucket `_. Reporting bugs ============== A `bug tracker `_ is provided by bitbucket.org. Getting help ============ If you have problems or other questions, you can either: * Ask on the `APScheduler Google group `_, or * Ask on the ``#apscheduler`` channel on `Freenode IRC `_ Keywords: scheduling cron Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 APScheduler-2.1.2/examples/0000755000175000017500000000000012266471145014145 5ustar alexalexAPScheduler-2.1.2/examples/threaded.py0000644000175000017500000000113012266453403016267 0ustar alexalex""" Basic example showing how the scheduler integrates with the application it's running alongside with. """ from datetime import datetime import time from apscheduler.scheduler import Scheduler def tick(): print('Tick! The time is: %s' % datetime.now()) if __name__ == '__main__': scheduler = Scheduler() scheduler.add_interval_job(tick, seconds=3) print('Press Ctrl+C to exit') scheduler.start() # This is here to simulate application activity (which keeps the main # thread alive). while True: print('This is the main thread.') time.sleep(2) APScheduler-2.1.2/examples/interval.py0000644000175000017500000000076512266453403016350 0ustar alexalex""" Basic example showing how to start the scheduler and schedule a job that executes on 3 second intervals. """ from datetime import datetime from apscheduler.scheduler import Scheduler def tick(): print('Tick! The time is: %s' % datetime.now()) if __name__ == '__main__': scheduler = Scheduler(standalone=True) scheduler.add_interval_job(tick, seconds=3) print('Press Ctrl+C to exit') try: scheduler.start() except (KeyboardInterrupt, SystemExit): pass APScheduler-2.1.2/examples/persistent.py0000644000175000017500000000173212266453403016717 0ustar alexalex""" This example demonstrates the use of persistent job stores. On each run, it adds a new alarm that fires after ten seconds. You can exit the program, restart it and observe that any previous alarms that have not fired yet are still active. """ from datetime import datetime, timedelta from apscheduler.scheduler import Scheduler from apscheduler.jobstores.shelve_store import ShelveJobStore def alarm(time): print('Alarm! This alarm was scheduled at %s.' % time) if __name__ == '__main__': scheduler = Scheduler(standalone=True) scheduler.add_jobstore(ShelveJobStore('example.db'), 'shelve') alarm_time = datetime.now() + timedelta(seconds=10) scheduler.add_date_job(alarm, alarm_time, name='alarm', jobstore='shelve', args=[datetime.now()]) print('To clear the alarms, delete the example.db file.') print('Press Ctrl+C to exit') try: scheduler.start() except (KeyboardInterrupt, SystemExit): pass APScheduler-2.1.2/docs/0000755000175000017500000000000012266471145013257 5ustar alexalexAPScheduler-2.1.2/docs/dateschedule.rst0000644000175000017500000000231012266453403016434 0ustar alexalexSimple date-based scheduling ============================ This is the simplest possible method of scheduling a job. It schedules a job to be executed once at the specified time. This is the in-process equivalent to the UNIX "at" command. :: from datetime import date from apscheduler.scheduler import Scheduler # Start the scheduler sched = Scheduler() sched.start() # Define the function that is to be executed def my_job(text): print text # The job will be executed on November 6th, 2009 exec_date = date(2009, 11, 6) # Store the job in a variable in case we want to cancel it job = sched.add_date_job(my_job, exec_date, ['text']) We could be more specific with the scheduling too:: from datetime import datetime # The job will be executed on November 6th, 2009 at 16:30:05 job = sched.add_date_job(my_job, datetime(2009, 11, 6, 16, 30, 5), ['text']) You can even specify a date as text, with or without the time part:: job = sched.add_date_job(my_job, '2009-11-06 16:30:05', ['text']) # Even down to the microsecond level, if you really want to! job = sched.add_date_job(my_job, '2009-11-06 16:30:05.720400', ['text']) APScheduler-2.1.2/docs/conf.py0000644000175000017500000001447012266453346014567 0ustar alexalex# -*- coding: utf-8 -*- # # APScheduler documentation build configuration file, created by # sphinx-quickstart on Fri Jul 31 02:56:30 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import apscheduler # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'APScheduler' copyright = u'Alex Grönholm' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = apscheduler.version # The full version, including alpha/beta/rc tags. release = apscheduler.release # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'APSchedulerdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'APScheduler.tex', u'APScheduler Documentation', u'Alex Grönholm', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True intersphinx_mapping = {'python': ('http://docs.python.org/', None)} APScheduler-2.1.2/docs/extending.rst0000644000175000017500000000363212266453346016005 0ustar alexalexExtending APScheduler ===================== This document is meant to explain how to add extra functionality to APScheduler, such as custom triggers or job stores. Writing and using custom triggers --------------------------------- Triggers determine the times when the jobs should be run. APScheduler comes with three built-in triggers -- :class:`~apscheduler.triggers.simple.SimpleTrigger`, :class:`~apscheduler.triggers.interval.IntervalTrigger` and :class:`~apscheduler.triggers.cron.CronTrigger`. You don't normally use these directly, since the scheduler has shortcut methods for these built-in triggers. If you need to use some specialized scheduling algorithm, you can implement that as a custom trigger class. The only method a trigger class has to implement is ``get_next_fire_time``. This method receives a starting date (a :class:`~datetime.datetime` object) as its sole argument. It should return the next time the trigger will fire (starting from and including the given time), according to whatever scheduling logic you wish to implement. If no such datetime can be computed, it should return ``None``. To schedule a job using your custom trigger, you can either extends the :class:`~apscheduler.scheduler.Scheduler` class to include your own shortcuts, or use the generic :meth:`~apscheduler.scheduler.Scheduler.add_job` method to add your jobs. Writing and using custom job stores ----------------------------------- Job store classes should preferably inherit from :class:`apscheduler.jobstores.base.JobStore`. This class provides stubbed out methods which any implementation should override. These methods also contain useful documentation regarding the responsibilities of a job store. It is recommended that you look at the existing job store implementations for examples. To use your job store, you must add it to the scheduler as normal:: jobstore = MyJobStore() scheduler.add_jobstore(jobstore, 'mystore') APScheduler-2.1.2/docs/intervalschedule.rst0000644000175000017500000000320712266453403017351 0ustar alexalexInterval-based scheduling ========================= This method schedules jobs to be run on selected intervals. The execution of the job starts after the given delay, or on ``start_date`` if specified. After that, the job will be executed again after the specified delay. The ``start_date`` parameter can be given as a date/datetime object or text. See the :doc:`Date-based scheduling section ` for more examples on that. :: from datetime import datetime from apscheduler.scheduler import Scheduler # Start the scheduler sched = Scheduler() sched.start() def job_function(): print "Hello World" # Schedule job_function to be called every two hours sched.add_interval_job(job_function, hours=2) # The same as before, but start after a certain time point sched.add_interval_job(job_function, hours=2, start_date='2010-10-10 09:30') Decorator syntax ---------------- As a convenience, there is an alternative syntax for using interval-based schedules. The :meth:`~apscheduler.scheduler.Scheduler.interval_schedule` decorator can be attached to any function, and has the same syntax as :meth:`~apscheduler.scheduler.Scheduler.add_interval_job`, except for the ``func`` parameter, obviously. :: from apscheduler.scheduler import Scheduler # Start the scheduler sched = Scheduler() sched.start() # Schedule job_function to be called every two hours @sched.interval_schedule(hours=2) def job_function(): print "Hello World" If you need to unschedule the decorated functions, you can do it this way:: scheduler.unschedule_job(job_function.job) APScheduler-2.1.2/docs/modules/0000755000175000017500000000000012266471145014727 5ustar alexalexAPScheduler-2.1.2/docs/modules/scheduler.rst0000644000175000017500000000033212266453346017440 0ustar alexalex:mod:`apscheduler.scheduler` ============================ .. automodule:: apscheduler.scheduler Module Contents --------------- .. autoclass:: Scheduler :members: .. autoexception:: SchedulerAlreadyRunningError APScheduler-2.1.2/docs/modules/events.rst0000644000175000017500000000037412266453346016774 0ustar alexalex:mod:`apscheduler.events` ============================ .. automodule:: apscheduler.events Module Contents --------------- .. autoclass:: SchedulerEvent :members: .. autoclass:: JobStoreEvent :members: .. autoclass:: JobEvent :members: APScheduler-2.1.2/docs/modules/jobstores/0000755000175000017500000000000012266471145016741 5ustar alexalexAPScheduler-2.1.2/docs/modules/jobstores/ram.rst0000644000175000017500000000032112266453346020251 0ustar alexalex:mod:`apscheduler.jobstores.ram_store` ============================================= .. automodule:: apscheduler.jobstores.ram_store Module Contents --------------- .. autoclass:: RAMJobStore :members: APScheduler-2.1.2/docs/modules/jobstores/sqlalchemy.rst0000644000175000017500000000034612266453346021643 0ustar alexalex:mod:`apscheduler.jobstores.sqlalchemy_store` ============================================= .. automodule:: apscheduler.jobstores.sqlalchemy_store Module Contents --------------- .. autoclass:: SQLAlchemyJobStore :members: APScheduler-2.1.2/docs/modules/jobstores/mongodb.rst0000644000175000017500000000033512266453346021124 0ustar alexalex:mod:`apscheduler.jobstores.mongodb_store` ============================================= .. automodule:: apscheduler.jobstores.mongodb_store Module Contents --------------- .. autoclass:: MongoDBJobStore :members: APScheduler-2.1.2/docs/modules/jobstores/shelve.rst0000644000175000017500000000032612266453346020765 0ustar alexalex:mod:`apscheduler.jobstores.shelve_store` ========================================= .. automodule:: apscheduler.jobstores.shelve_store Module Contents --------------- .. autoclass:: ShelveJobStore :members: APScheduler-2.1.2/docs/modules/jobstores/redis.rst0000644000175000017500000000032712266453346020606 0ustar alexalex:mod:`apscheduler.jobstores.redis_store` ============================================= .. automodule:: apscheduler.jobstores.redis_store Module Contents --------------- .. autoclass:: RedisJobStore :members: APScheduler-2.1.2/docs/modules/job.rst0000644000175000017500000000020412266453346016232 0ustar alexalex:mod:`apscheduler.job` ====================== .. automodule:: apscheduler.job Module Contents --------------- .. autoclass:: Job APScheduler-2.1.2/docs/cronschedule.rst0000644000175000017500000001037112266453403016466 0ustar alexalexCron-style scheduling ===================== This is the most powerful scheduling method available in APScheduler. You can specify a variety of different expressions on each field, and when determining the next execution time, it finds the earliest possible time that satisfies the conditions in every field. This behavior resembles the "Cron" utility found in most UNIX-like operating systems. You can also specify the starting date for the cron-style schedule through the ``start_date`` parameter, which can be given as a date/datetime object or text. See the :doc:`Date-based scheduling section ` for examples on that. Unlike with crontab expressions, you can omit fields that you don't need. Fields greater than the least significant explicitly defined field default to ``*`` while lesser fields default to their minimum values except for ``week`` and ``day_of_week`` which default to ``*``. For example, if you specify only ``day=1, minute=20``, then the job will execute on the first day of every month on every year at 20 minutes of every hour. The code examples below should further illustrate this behavior. .. Note:: The behavior for omitted fields was changed in APScheduler 2.0. Omitted fields previously always defaulted to ``*``. Available fields ---------------- =============== ====================================================== Field Description =============== ====================================================== ``year`` 4-digit year number ``month`` month number (1-12) ``day`` day of the month (1-31) ``week`` ISO week number (1-53) ``day_of_week`` number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) ``hour`` hour (0-23) ``minute`` minute (0-59) ``second`` second (0-59) =============== ====================================================== .. Note:: The first weekday is always **monday**. Expression types ---------------- The following table lists all the available expressions applicable in cron-style schedules. ============ ========= ====================================================== Expression Field Description ============ ========= ====================================================== ``*`` any Fire on every value ``*/a`` any Fire every ``a`` values, starting from the minimum ``a-b`` any Fire on any value within the ``a-b`` range (a must be smaller than b) ``a-b/c`` any Fire every ``c`` values within the ``a-b`` range ``xth y`` day Fire on the ``x`` -th occurrence of weekday ``y`` within the month ``last x`` day Fire on the last occurrence of weekday ``x`` within the month ``last`` day Fire on the last day within the month ``x,y,z`` any Fire on any matching expression; can combine any number of any of the above expressions ============ ========= ====================================================== Example 1 --------- :: from apscheduler.scheduler import Scheduler # Start the scheduler sched = Scheduler() sched.start() def job_function(): print "Hello World" # Schedules job_function to be run on the third Friday # of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00 sched.add_cron_job(job_function, month='6-8,11-12', day='3rd fri', hour='0-3') Example 2 --------- :: # Initialization similar as above, the backup function defined elsewhere # Schedule a backup to run once from Monday to Friday at 5:30 (am) sched.add_cron_job(backup, day_of_week='mon-fri', hour=5, minute=30) Decorator syntax ---------------- As a convenience, there is an alternative syntax for using cron-style schedules. The :meth:`~apscheduler.scheduler.Scheduler.cron_schedule` decorator can be attached to any function, and has the same syntax as :meth:`~apscheduler.scheduler.Scheduler.add_cron_job`, except for the ``func`` parameter, obviously. :: @sched.cron_schedule(day='last sun') def some_decorated_task(): print "I am printed at 00:00:00 on the last Sunday of every month!" If you need to unschedule the decorated functions, you can do it this way:: scheduler.unschedule_job(job_function.job) APScheduler-2.1.2/docs/migration.rst0000644000175000017500000000267312266453403016007 0ustar alexalexMigrating from APScheduler v1.x to 2.0 ====================================== There have been some API changes since the 1.x series. This document explains the changes made to v2.0 that are incompatible with the v1.x API. API changes ----------- * The behavior of cron scheduling with regards to default values for omitted fields has been made more intuitive -- omitted fields lower than the least significant explicitly defined field will default to their minimum values except for the week number and weekday fields * SchedulerShutdownError has been removed -- jobs are now added tentatively and scheduled for real when/if the scheduler is restarted * Scheduler.is_job_active() has been removed -- use ``job in scheduler.get_jobs()`` instead * dump_jobs() is now print_jobs() and prints directly to the given file or sys.stdout if none is given * The ``repeat`` parameter was removed from :meth:`~apscheduler.scheduler.Scheduler.add_interval_job` and :meth:`~apscheduler.scheduler.Scheduler.interval_schedule` in favor of the universal ``max_runs`` option * :meth:`~apscheduler.scheduler.Scheduler.unschedule_func` now raises a KeyError if the given function is not scheduled * The semantics of :meth:`~apscheduler.scheduler.Scheduler.shutdown` have changed -- the method no longer accepts a numeric argument, but two booleans Configuration changes --------------------- * The scheduler can no longer be reconfigured while it's running APScheduler-2.1.2/docs/index.rst0000644000175000017500000004273112266460703015125 0ustar alexalexAdvanced Python Scheduler ========================= .. contents:: Introduction ------------ Advanced Python Scheduler (APScheduler) is a light but powerful in-process task scheduler that lets you schedule functions (or any other python callables) to be executed at times of your choosing. This can be a far better alternative to externally run cron scripts for long-running applications (e.g. web applications), as it is platform neutral and can directly access your application's variables and functions. The development of APScheduler was heavily influenced by the `Quartz `_ task scheduler written in Java. APScheduler provides most of the major features that Quartz does, but it also provides features not present in Quartz (such as multiple job stores). Features -------- * No (hard) external dependencies * Thread-safe API * Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 2.2) * Configurable scheduling mechanisms (triggers): * Cron-like scheduling * Delayed scheduling of single run jobs (like the UNIX "at" command) * Interval-based (run a job at specified time intervals) * Multiple, simultaneously active job stores: * RAM * File-based simple database (:py:mod:`shelve`) * `SQLAlchemy `_ (any supported RDBMS works) * `MongoDB `_ * `Redis `_ Usage ===== Installing APScheduler ---------------------- The preferred installation method is by using `pip `_:: $ pip install apscheduler or `easy_install `_:: $ easy_install apscheduler If that doesn't work, you can manually `download the APScheduler distribution `_ from PyPI, extract and then install it:: $ python setup.py install Starting the scheduler ---------------------- To start the scheduler with default settings:: from apscheduler.scheduler import Scheduler sched = Scheduler() sched.start() The constructor takes as its first, optional parameter a dictionary of "global" options to facilitate configuration from .ini files. All APScheduler options given in the global configuration must begin with "apscheduler." to avoid name clashes with other software. The constructor also takes options as keyword arguments (without the prefix). You can also configure the scheduler after its instantiation, if necessary. This is handy if you use the decorators for scheduling and must have a Scheduler instance available from the very beginning:: from apscheduler.scheduler import Scheduler sched = Scheduler() @sched.interval_schedule(hours=3) def some_job(): print "Decorated job" sched.configure(options_from_ini_file) sched.start() Scheduling jobs --------------- The simplest way to schedule jobs using the built-in triggers is to use one of the shortcut methods provided by the scheduler: .. toctree:: :maxdepth: 1 dateschedule intervalschedule cronschedule These shortcuts cover the vast majority of use cases. However, if you need to use a custom trigger, you need to use the :meth:`~apscheduler.scheduler.Scheduler.add_job` method. When a scheduled job is triggered, it is handed over to the thread pool for execution. You can request a job to be added to a specific job store by giving the target job store's alias in the ``jobstore`` option to :meth:`~apscheduler.scheduler.Scheduler.add_job` or any of the above shortcut methods. You can schedule jobs on the scheduler **at any time**. If the scheduler is not running when the job is added, the job will be scheduled `tentatively` and its first run time will only be computed when the scheduler starts. Jobs will not run retroactively in such cases. .. warning:: Scheduling new jobs from existing jobs is not currently reliable. This will likely be fixed in the next major release. .. _job_options: Job options ----------- The following options can be given as keyword arguments to :meth:`~apscheduler.scheduler.Scheduler.add_job` or one of the shortcut methods, including the decorators. ===================== ========================================================= Option Definition ===================== ========================================================= name Name of the job (informative, does not have to be unique) misfire_grace_time Time in seconds that the job is allowed to miss the the designated run time before being considered to have misfired (see :ref:`coalescing`) (overrides the global scheduler setting) coalesce Run once instead of many times if the scheduler determines that the job should be run more than once in succession (see :ref:`coalescing`) (overrides the global scheduler setting) max_runs Maximum number of times this job is allowed to be triggered before being removed max_instances Maximum number of concurrently running instances allowed for this job (see :ref:`_max_instances`) ===================== ========================================================= Shutting down the scheduler --------------------------- To shut down the scheduler:: sched.shutdown() By default, the scheduler shuts down its thread pool and waits until all currently executing jobs are finished. For a faster exit you can do:: sched.shutdown(wait=False) This will still shut down the thread pool but does not wait for any running tasks to complete. Also, if you gave the scheduler a thread pool that you want to manage elsewhere, you probably want to skip the thread pool shutdown altogether:: sched.shutdown(shutdown_threadpool=False) This implies ``wait=False``, since there is no way to wait for the scheduler's tasks to finish without shutting down the thread pool. A neat trick to automatically shut down the scheduler is to use an :py:mod:`atexit` hook for that:: import atexit sched = Scheduler(daemon=True) atexit.register(lambda: sched.shutdown(wait=False)) # Proceed with starting the actual application Scheduler configuration options ------------------------------- ======================= ========== ============================================== Directive Default Definition ======================= ========== ============================================== misfire_grace_time 1 Maximum time in seconds for the job execution to be allowed to delay before it is considered a misfire (see :ref:`coalescing`) coalesce False Roll several pending executions of jobs into one (see :ref:`coalescing`) standalone False If set to ``True``, :meth:`~apscheduler.scheduler.Scheduler.start` will run the main loop in the calling thread until no more jobs are scheduled. See :ref:`modes` for more information. daemonic True Controls whether the scheduler thread is daemonic or not. This option has no effect when ``standalone`` is ``True``. If set to ``False``, then the scheduler must be shut down explicitly when the program is about to finish, or it will prevent the program from terminating. If set to ``True``, the scheduler will automatically terminate with the application, but may cause an exception to be raised on exit. Jobs are always executed in non-daemonic threads. threadpool (built-in) Instance of a :pep:`3148` compliant thread pool or a dot-notation (``x.y.z:varname``) reference to one threadpool.core_threads 0 Maximum number of persistent threads in the pool threadpool.max_threads 20 Maximum number of total threads in the pool threadpool.keepalive 1 Seconds to keep non-core worker threads waiting for new tasks jobstore.X.class Class of the jobstore named X (specified as module.name:classname) jobstore.X.Y Constructor option Y of jobstore X ======================= ========== ============================================== .. _modes: Operating modes: embedded vs standalone --------------------------------------- The scheduler has two operating modes: standalone and embedded. In embedded mode, it will spawn its own thread when :meth:`~apscheduler.scheduler.Scheduler.start` is called. In standalone mode, it will run directly in the calling thread and will block until there are no more pending jobs. The embedded mode is suitable for running alongside some application that requires scheduling capabilities. The standalone mode, on the other hand, can be used as a handy cross-platform cron replacement for executing Python code. A typical usage of the standalone mode is to have a script that only adds the jobs to the scheduler and then calls :meth:`~apscheduler.scheduler.Scheduler.start` on the scheduler. All of the examples in the examples directory demonstrate usage of the standalone mode, with the exception of ``threaded.py`` which demonstrates the embedded mode (where the "application" just prints a line every 2 seconds). Job stores ---------- APScheduler keeps all the scheduled jobs in *job stores*. Job stores are configurable adapters to some back-end that may or may not support persisting job configurations on disk, database or something else. Job stores are added to the scheduler and identified by their aliases. The alias ``default`` is special in that if the user does not explicitly specify a job store alias when scheduling a job, it goes to the ``default`` job store. If there is no job store in the scheduler by that name when the scheduler is started, a new job store of type :class:`~apscheduler.jobstores.ram_store.RAMJobStore` is created to serve as the default. The other built-in job stores are: * :class:`~apscheduler.jobstores.shelve_store.ShelveJobStore` * :class:`~apscheduler.jobstores.sqlalchemy_store.SQLAlchemyJobStore` * :class:`~apscheduler.jobstores.mongodb_store.MongoDBJobStore` * :class:`~apscheduler.jobstores.redis_store.RedisJobStore` Job stores can be added either through configuration options or the :meth:`~apscheduler.scheduler.Scheduler.add_jobstore` method. The following are therefore equal:: config = {'apscheduler.jobstores.file.class': 'apscheduler.jobstores.shelve_store:ShelveJobStore', 'apscheduler.jobstores.file.path': '/tmp/dbfile'} sched = Scheduler(config) and:: from apscheduler.jobstores.shelve_store import ShelveJobStore sched = Scheduler() sched.add_jobstore(ShelveJobStore('/tmp/dbfile'), 'file') The example configuration above results in the scheduler having two job stores -- one :class:`~apscheduler.jobstores.ram_store.RAMJobStore` and one :class:`~apscheduler.jobstores.shelve_store.ShelveJobStore`. Job persistency --------------- The built-in job stores (other than :class:`~apscheduler.jobstores.ram_store.RAMJobStore`) store jobs in a durable manner. This means that when you schedule jobs in them, shut down the scheduler, restart it and readd the job store in question, it will load the previously scheduled jobs automatically. Persistent job stores store a reference to the target callable in text form and serialize the arguments using pickle. This unfortunately adds some restrictions: * You cannot schedule static methods, inner functions or lambdas. * You cannot update the objects given as arguments to the callable. Technically you *can* update the state of the argument objects, but those changes are never persisted back to the job store. .. note:: None of these restrictions apply to ``RAMJobStore``. .. _max_instances: Limiting the number of concurrently executing instances of a job ---------------------------------------------------------------- By default, no two instances of the same job will be run concurrently. This means that if the job is about to be run but the previous run hasn't finished yet, then the latest run is considered a misfire. It is possible to set the maximum number of instances for a particular job that the scheduler will let run concurrently, by using the ``max_instances`` keyword argument when adding the job. .. _coalescing: Missed job executions and coalescing ------------------------------------ Sometimes the scheduler may be unable to execute a scheduled job at the time it was scheduled to run. The most common case is when a job is scheduled in a persistent job store and the scheduler is shut down and restarted after the job was supposed to execute. When this happens, the job is considered to have "misfired". The scheduler will then check each missed execution time against the job's ``misfire_grace_time`` option (which can be set on per-job basis or globally in the scheduler) to see if the execution should still be triggered. This can lead into the job being executed several times in succession. If this behavior is undesirable for your particular use case, it is possible to use `coalescing` to roll all these missed executions into one. In other words, if coalescing is enabled for the job and the scheduler sees one or more queued executions for the job, it will only trigger it once. The "bypassed" runs of the job are not considered misfires nor do they count towards any maximum run count of the job. Scheduler events ---------------- It is possible to attach event listeners to the scheduler. Scheduler events are fired on certain occasions, and may carry additional information in them concerning the details of that particular event. It is possible to listen to only particular types of events by giving the appropriate ``mask`` argument to :meth:`~apscheduler.scheduler.Scheduler.add_listener`, OR'ing the different constants together. The listener callable is called with one argument, the event object. The type of the event object is tied to the event code as shown below: ========================== ============== ========================================== Constant Event class Triggered when... ========================== ============== ========================================== EVENT_SCHEDULER_START SchedulerEvent The scheduler is started EVENT_SCHEDULER_SHUTDOWN SchedulerEvent The scheduler is shut down EVENT_JOBSTORE_ADDED JobStoreEvent A job store is added to the scheduler EVENT_JOBSTORE_REMOVED JobStoreEvent A job store is removed from the scheduler EVENT_JOBSTORE_JOB_ADDED JobStoreEvent A job is added to a job store EVENT_JOBSTORE_JOB_REMOVED JobStoreEvent A job is removed from a job store EVENT_JOB_EXECUTED JobEvent A job is executed successfully EVENT_JOB_ERROR JobEvent A job raised an exception during execution EVENT_JOB_MISSED JobEvent A job's execution time is missed ========================== ============== ========================================== See the documentation for the :mod:`~apscheduler.events` module for specifics on the available event attributes. Example:: def my_listener(event): if event.exception: print 'The job crashed :(' else: print 'The job worked :)' scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) Getting a list of scheduled jobs -------------------------------- If you want to see which jobs are currently added in the scheduler, you can simply do:: sched.print_jobs() This will print a human-readable listing of scheduled jobs, their triggering mechanisms and the next time they will fire. If you supply a file-like object as an argument to this method, it will output the results in that file. To get a machine processable list of the scheduled jobs, you can use the :meth:`~apscheduler.scheduler.Scheduler.get_jobs` scheduler method. It will return a list of :class:`~apscheduler.job.Job` instances. Extending APScheduler ===================== It is possible to extend APScheduler to support alternative job stores and triggers. See the :doc:`Extending APScheduler ` document for details. FAQ === Q: Why do my processes hang instead of exiting when they are finished? A: A scheduled job may still be executing. APScheduler's thread pool is wired to wait for the job threads to exit before allowing the interpreter to exit to avoid unpredictable behavior caused by the shutdown procedures of the Python interpreter. A more thorough explanation `can be found here `_. Reporting bugs ============== A `bug tracker `_ is provided by bitbucket.org. Getting help ============ If you have problems or other questions, you can either: * Ask on the `APScheduler Google group `_, or * Ask on the ``#apscheduler`` channel on `Freenode IRC `_ .. include:: ../CHANGES.rst APScheduler-2.1.2/apscheduler/0000755000175000017500000000000012266471145014626 5ustar alexalexAPScheduler-2.1.2/apscheduler/util.py0000644000175000017500000001514012266453403016153 0ustar alexalex""" This module contains several handy functions primarily meant for internal use. """ from datetime import date, datetime, timedelta from time import mktime import re import sys __all__ = ('asint', 'asbool', 'convert_to_datetime', 'timedelta_seconds', 'time_difference', 'datetime_ceil', 'combine_opts', 'get_callable_name', 'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'to_unicode', 'iteritems', 'itervalues', 'xrange') def asint(text): """ Safely converts a string to an integer, returning None if the string is None. :type text: str :rtype: int """ if text is not None: return int(text) def asbool(obj): """ Interprets an object as a boolean value. :rtype: bool """ if isinstance(obj, str): obj = obj.strip().lower() if obj in ('true', 'yes', 'on', 'y', 't', '1'): return True if obj in ('false', 'no', 'off', 'n', 'f', '0'): return False raise ValueError('Unable to interpret value "%s" as boolean' % obj) return bool(obj) _DATE_REGEX = re.compile( r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' r'(?: (?P\d{1,2}):(?P\d{1,2}):(?P\d{1,2})' r'(?:\.(?P\d{1,6}))?)?') def convert_to_datetime(input): """ Converts the given object to a datetime object, if possible. If an actual datetime object is passed, it is returned unmodified. If the input is a string, it is parsed as a datetime. Date strings are accepted in three different forms: date only (Y-m-d), date with time (Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). :rtype: datetime """ if isinstance(input, datetime): return input elif isinstance(input, date): return datetime.fromordinal(input.toordinal()) elif isinstance(input, basestring): m = _DATE_REGEX.match(input) if not m: raise ValueError('Invalid date string') values = [(k, int(v or 0)) for k, v in m.groupdict().items()] values = dict(values) return datetime(**values) raise TypeError('Unsupported input type: %s' % type(input)) def timedelta_seconds(delta): """ Converts the given timedelta to seconds. :type delta: timedelta :rtype: float """ return delta.days * 24 * 60 * 60 + delta.seconds + \ delta.microseconds / 1000000.0 def time_difference(date1, date2): """ Returns the time difference in seconds between the given two datetime objects. The difference is calculated as: date1 - date2. :param date1: the later datetime :type date1: datetime :param date2: the earlier datetime :type date2: datetime :rtype: float """ later = mktime(date1.timetuple()) + date1.microsecond / 1000000.0 earlier = mktime(date2.timetuple()) + date2.microsecond / 1000000.0 return later - earlier def datetime_ceil(dateval): """ Rounds the given datetime object upwards. :type dateval: datetime """ if dateval.microsecond > 0: return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond) return dateval def combine_opts(global_config, prefix, local_config={}): """ Returns a subdictionary from keys and values of ``global_config`` where the key starts with the given prefix, combined with options from local_config. The keys in the subdictionary have the prefix removed. :type global_config: dict :type prefix: str :type local_config: dict :rtype: dict """ prefixlen = len(prefix) subconf = {} for key, value in global_config.items(): if key.startswith(prefix): key = key[prefixlen:] subconf[key] = value subconf.update(local_config) return subconf def get_callable_name(func): """ Returns the best available display name for the given function/callable. """ f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None) if f_self and hasattr(func, '__name__'): if isinstance(f_self, type): # class method clsname = getattr(f_self, '__qualname__', None) or f_self.__name__ return '%s.%s' % (clsname, func.__name__) # bound method return '%s.%s' % (f_self.__class__.__name__, func.__name__) if hasattr(func, '__call__'): if hasattr(func, '__name__'): # function, unbound method or a class with a __call__ method return func.__name__ # instance of a class with a __call__ method return func.__class__.__name__ raise TypeError('Unable to determine a name for %s -- ' 'maybe it is not a callable?' % repr(func)) def obj_to_ref(obj): """ Returns the path to the given object. """ ref = '%s:%s' % (obj.__module__, get_callable_name(obj)) try: obj2 = ref_to_obj(ref) if obj != obj2: raise ValueError except Exception: raise ValueError('Cannot determine the reference to %s' % repr(obj)) return ref def ref_to_obj(ref): """ Returns the object pointed to by ``ref``. """ if not isinstance(ref, basestring): raise TypeError('References must be strings') if not ':' in ref: raise ValueError('Invalid reference') modulename, rest = ref.split(':', 1) try: obj = __import__(modulename) except ImportError: raise LookupError('Error resolving reference %s: ' 'could not import module' % ref) try: for name in modulename.split('.')[1:] + rest.split('.'): obj = getattr(obj, name) return obj except Exception: raise LookupError('Error resolving reference %s: ' 'error looking up object' % ref) def maybe_ref(ref): """ Returns the object that the given reference points to, if it is indeed a reference. If it is not a reference, the object is returned as-is. """ if not isinstance(ref, str): return ref return ref_to_obj(ref) def to_unicode(string, encoding='ascii'): """ Safely converts a string to a unicode representation on any Python version. """ if hasattr(string, 'decode'): return string.decode(encoding, 'ignore') return string # pragma: nocover if sys.version_info < (3, 0): # pragma: nocover iteritems = lambda d: d.iteritems() itervalues = lambda d: d.itervalues() xrange = xrange basestring = basestring else: # pragma: nocover iteritems = lambda d: d.items() itervalues = lambda d: d.values() xrange = range basestring = str APScheduler-2.1.2/apscheduler/threadpool.py0000644000175000017500000000761612266453403017350 0ustar alexalex""" Generic thread pool class. Modeled after Java's ThreadPoolExecutor. Please note that this ThreadPool does *not* fully implement the PEP 3148 ThreadPool! """ from threading import Thread, Lock, currentThread from weakref import ref import logging import atexit try: from queue import Queue, Empty except ImportError: from Queue import Queue, Empty logger = logging.getLogger(__name__) _threadpools = set() # Worker threads are daemonic in order to let the interpreter exit without # an explicit shutdown of the thread pool. The following trick is necessary # to allow worker threads to finish cleanly. def _shutdown_all(): for pool_ref in tuple(_threadpools): pool = pool_ref() if pool: pool.shutdown() atexit.register(_shutdown_all) class ThreadPool(object): def __init__(self, core_threads=0, max_threads=20, keepalive=1): """ :param core_threads: maximum number of persistent threads in the pool :param max_threads: maximum number of total threads in the pool :param thread_class: callable that creates a Thread object :param keepalive: seconds to keep non-core worker threads waiting for new tasks """ self.core_threads = core_threads self.max_threads = max(max_threads, core_threads, 1) self.keepalive = keepalive self._queue = Queue() self._threads_lock = Lock() self._threads = set() self._shutdown = False _threadpools.add(ref(self)) logger.info('Started thread pool with %d core threads and %s maximum ' 'threads', core_threads, max_threads or 'unlimited') def _adjust_threadcount(self): self._threads_lock.acquire() try: if self.num_threads < self.max_threads: self._add_thread(self.num_threads < self.core_threads) finally: self._threads_lock.release() def _add_thread(self, core): t = Thread(target=self._run_jobs, args=(core,)) t.setDaemon(True) t.start() self._threads.add(t) def _run_jobs(self, core): logger.debug('Started worker thread') block = True timeout = None if not core: block = self.keepalive > 0 timeout = self.keepalive while True: try: func, args, kwargs = self._queue.get(block, timeout) except Empty: break if self._shutdown: break try: func(*args, **kwargs) except: logger.exception('Error in worker thread') self._threads_lock.acquire() self._threads.remove(currentThread()) self._threads_lock.release() logger.debug('Exiting worker thread') @property def num_threads(self): return len(self._threads) def submit(self, func, *args, **kwargs): if self._shutdown: raise RuntimeError('Cannot schedule new tasks after shutdown') self._queue.put((func, args, kwargs)) self._adjust_threadcount() def shutdown(self, wait=True): if self._shutdown: return logging.info('Shutting down thread pool') self._shutdown = True _threadpools.remove(ref(self)) self._threads_lock.acquire() for _ in range(self.num_threads): self._queue.put((None, None, None)) self._threads_lock.release() if wait: self._threads_lock.acquire() threads = tuple(self._threads) self._threads_lock.release() for thread in threads: thread.join() def __repr__(self): if self.max_threads: threadcount = '%d/%d' % (self.num_threads, self.max_threads) else: threadcount = '%d' % self.num_threads return '' % (id(self), threadcount) APScheduler-2.1.2/apscheduler/triggers/0000755000175000017500000000000012266471145016454 5ustar alexalexAPScheduler-2.1.2/apscheduler/triggers/simple.py0000644000175000017500000000074212266453403020317 0ustar alexalexfrom apscheduler.util import convert_to_datetime class SimpleTrigger(object): def __init__(self, run_date): self.run_date = convert_to_datetime(run_date) def get_next_fire_time(self, start_date): if self.run_date >= start_date: return self.run_date def __str__(self): return 'date[%s]' % str(self.run_date) def __repr__(self): return '<%s (run_date=%s)>' % ( self.__class__.__name__, repr(self.run_date)) APScheduler-2.1.2/apscheduler/triggers/interval.py0000644000175000017500000000255412266453403020655 0ustar alexalexfrom datetime import datetime, timedelta from math import ceil from apscheduler.util import convert_to_datetime, timedelta_seconds class IntervalTrigger(object): def __init__(self, interval, start_date=None): if not isinstance(interval, timedelta): raise TypeError('interval must be a timedelta') if start_date: start_date = convert_to_datetime(start_date) self.interval = interval self.interval_length = timedelta_seconds(self.interval) if self.interval_length == 0: self.interval = timedelta(seconds=1) self.interval_length = 1 if start_date is None: self.start_date = datetime.now() + self.interval else: self.start_date = convert_to_datetime(start_date) def get_next_fire_time(self, start_date): if start_date < self.start_date: return self.start_date timediff_seconds = timedelta_seconds(start_date - self.start_date) next_interval_num = int(ceil(timediff_seconds / self.interval_length)) return self.start_date + self.interval * next_interval_num def __str__(self): return 'interval[%s]' % str(self.interval) def __repr__(self): return "<%s (interval=%s, start_date=%s)>" % ( self.__class__.__name__, repr(self.interval), repr(self.start_date)) APScheduler-2.1.2/apscheduler/triggers/__init__.py0000644000175000017500000000024212266453403020560 0ustar alexalexfrom apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.simple import SimpleTrigger APScheduler-2.1.2/apscheduler/triggers/cron/0000755000175000017500000000000012266471145017415 5ustar alexalexAPScheduler-2.1.2/apscheduler/triggers/cron/expressions.py0000644000175000017500000001407412266453403022354 0ustar alexalex""" This module contains the expressions applicable for CronTrigger's fields. """ from calendar import monthrange import re from apscheduler.util import asint __all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression', 'WeekdayPositionExpression', 'LastDayOfMonthExpression') WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] class AllExpression(object): value_re = re.compile(r'\*(?:/(?P\d+))?$') def __init__(self, step=None): self.step = asint(step) if self.step == 0: raise ValueError('Increment must be higher than 0') def get_next_value(self, date, field): start = field.get_value(date) minval = field.get_min(date) maxval = field.get_max(date) start = max(start, minval) if not self.step: next = start else: distance_to_next = (self.step - (start - minval)) % self.step next = start + distance_to_next if next <= maxval: return next def __str__(self): if self.step: return '*/%d' % self.step return '*' def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.step) class RangeExpression(AllExpression): value_re = re.compile( r'(?P\d+)(?:-(?P\d+))?(?:/(?P\d+))?$') def __init__(self, first, last=None, step=None): AllExpression.__init__(self, step) first = asint(first) last = asint(last) if last is None and step is None: last = first if last is not None and first > last: raise ValueError('The minimum value in a range must not be ' 'higher than the maximum') self.first = first self.last = last def get_next_value(self, date, field): start = field.get_value(date) minval = field.get_min(date) maxval = field.get_max(date) # Apply range limits minval = max(minval, self.first) if self.last is not None: maxval = min(maxval, self.last) start = max(start, minval) if not self.step: next = start else: distance_to_next = (self.step - (start - minval)) % self.step next = start + distance_to_next if next <= maxval: return next def __str__(self): if self.last != self.first and self.last is not None: range = '%d-%d' % (self.first, self.last) else: range = str(self.first) if self.step: return '%s/%d' % (range, self.step) return range def __repr__(self): args = [str(self.first)] if self.last != self.first and self.last is not None or self.step: args.append(str(self.last)) if self.step: args.append(str(self.step)) return "%s(%s)" % (self.__class__.__name__, ', '.join(args)) class WeekdayRangeExpression(RangeExpression): value_re = re.compile(r'(?P[a-z]+)(?:-(?P[a-z]+))?', re.IGNORECASE) def __init__(self, first, last=None): try: first_num = WEEKDAYS.index(first.lower()) except ValueError: raise ValueError('Invalid weekday name "%s"' % first) if last: try: last_num = WEEKDAYS.index(last.lower()) except ValueError: raise ValueError('Invalid weekday name "%s"' % last) else: last_num = None RangeExpression.__init__(self, first_num, last_num) def __str__(self): if self.last != self.first and self.last is not None: return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last]) return WEEKDAYS[self.first] def __repr__(self): args = ["'%s'" % WEEKDAYS[self.first]] if self.last != self.first and self.last is not None: args.append("'%s'" % WEEKDAYS[self.last]) return "%s(%s)" % (self.__class__.__name__, ', '.join(args)) class WeekdayPositionExpression(AllExpression): options = ['1st', '2nd', '3rd', '4th', '5th', 'last'] value_re = re.compile(r'(?P%s) +(?P(?:\d+|\w+))' % '|'.join(options), re.IGNORECASE) def __init__(self, option_name, weekday_name): try: self.option_num = self.options.index(option_name.lower()) except ValueError: raise ValueError('Invalid weekday position "%s"' % option_name) try: self.weekday = WEEKDAYS.index(weekday_name.lower()) except ValueError: raise ValueError('Invalid weekday name "%s"' % weekday_name) def get_next_value(self, date, field): # Figure out the weekday of the month's first day and the number # of days in that month first_day_wday, last_day = monthrange(date.year, date.month) # Calculate which day of the month is the first of the target weekdays first_hit_day = self.weekday - first_day_wday + 1 if first_hit_day <= 0: first_hit_day += 7 # Calculate what day of the month the target weekday would be if self.option_num < 5: target_day = first_hit_day + self.option_num * 7 else: target_day = first_hit_day + ((last_day - first_hit_day) / 7) * 7 if target_day <= last_day and target_day >= date.day: return target_day def __str__(self): return '%s %s' % (self.options[self.option_num], WEEKDAYS[self.weekday]) def __repr__(self): return "%s('%s', '%s')" % (self.__class__.__name__, self.options[self.option_num], WEEKDAYS[self.weekday]) class LastDayOfMonthExpression(AllExpression): value_re = re.compile(r'last', re.IGNORECASE) def __init__(self): pass def get_next_value(self, date, field): return monthrange(date.year, date.month)[1] def __str__(self): return 'last' def __repr__(self): return "%s()" % self.__class__.__name__ APScheduler-2.1.2/apscheduler/triggers/cron/fields.py0000644000175000017500000000576212266453403021244 0ustar alexalex""" Fields represent CronTrigger options which map to :class:`~datetime.datetime` fields. """ from calendar import monthrange from apscheduler.triggers.cron.expressions import * __all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField', 'WeekField', 'DayOfMonthField', 'DayOfWeekField') MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1, 'day_of_week': 0, 'hour': 0, 'minute': 0, 'second': 0} MAX_VALUES = {'year': 2 ** 63, 'month': 12, 'day:': 31, 'week': 53, 'day_of_week': 6, 'hour': 23, 'minute': 59, 'second': 59} DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*', 'day_of_week': '*', 'hour': 0, 'minute': 0, 'second': 0} class BaseField(object): REAL = True COMPILERS = [AllExpression, RangeExpression] def __init__(self, name, exprs, is_default=False): self.name = name self.is_default = is_default self.compile_expressions(exprs) def get_min(self, dateval): return MIN_VALUES[self.name] def get_max(self, dateval): return MAX_VALUES[self.name] def get_value(self, dateval): return getattr(dateval, self.name) def get_next_value(self, dateval): smallest = None for expr in self.expressions: value = expr.get_next_value(dateval, self) if smallest is None or (value is not None and value < smallest): smallest = value return smallest def compile_expressions(self, exprs): self.expressions = [] # Split a comma-separated expression list, if any exprs = str(exprs).strip() if ',' in exprs: for expr in exprs.split(','): self.compile_expression(expr) else: self.compile_expression(exprs) def compile_expression(self, expr): for compiler in self.COMPILERS: match = compiler.value_re.match(expr) if match: compiled_expr = compiler(**match.groupdict()) self.expressions.append(compiled_expr) return raise ValueError('Unrecognized expression "%s" for field "%s"' % (expr, self.name)) def __str__(self): expr_strings = (str(e) for e in self.expressions) return ','.join(expr_strings) def __repr__(self): return "%s('%s', '%s')" % (self.__class__.__name__, self.name, str(self)) class WeekField(BaseField): REAL = False def get_value(self, dateval): return dateval.isocalendar()[1] class DayOfMonthField(BaseField): COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression, LastDayOfMonthExpression] def get_max(self, dateval): return monthrange(dateval.year, dateval.month)[1] class DayOfWeekField(BaseField): REAL = False COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression] def get_value(self, dateval): return dateval.weekday() APScheduler-2.1.2/apscheduler/triggers/cron/__init__.py0000644000175000017500000001207612266453403021531 0ustar alexalexfrom datetime import date, datetime from apscheduler.triggers.cron.fields import * from apscheduler.util import datetime_ceil, convert_to_datetime, iteritems class CronTrigger(object): FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour', 'minute', 'second') FIELDS_MAP = {'year': BaseField, 'month': BaseField, 'week': WeekField, 'day': DayOfMonthField, 'day_of_week': DayOfWeekField, 'hour': BaseField, 'minute': BaseField, 'second': BaseField} def __init__(self, **values): self.start_date = values.pop('start_date', None) if self.start_date: self.start_date = convert_to_datetime(self.start_date) # Check field names and yank out all None valued fields for key, value in list(iteritems(values)): if key not in self.FIELD_NAMES: raise TypeError('Invalid field name: %s' % key) if value is None: del values[key] self.fields = [] assign_defaults = False for field_name in self.FIELD_NAMES: if field_name in values: exprs = values.pop(field_name) is_default = False assign_defaults = not values elif assign_defaults: exprs = DEFAULT_VALUES[field_name] is_default = True else: exprs = '*' is_default = True field_class = self.FIELDS_MAP[field_name] field = field_class(field_name, exprs, is_default) self.fields.append(field) def _increment_field_value(self, dateval, fieldnum): """ Increments the designated field and resets all less significant fields to their minimum values. :type dateval: datetime :type fieldnum: int :type amount: int :rtype: tuple :return: a tuple containing the new date, and the number of the field that was actually incremented """ i = 0 values = {} while i < len(self.fields): field = self.fields[i] if not field.REAL: if i == fieldnum: fieldnum -= 1 i -= 1 else: i += 1 continue if i < fieldnum: values[field.name] = field.get_value(dateval) i += 1 elif i > fieldnum: values[field.name] = field.get_min(dateval) i += 1 else: value = field.get_value(dateval) maxval = field.get_max(dateval) if value == maxval: fieldnum -= 1 i -= 1 else: values[field.name] = value + 1 i += 1 return datetime(**values), fieldnum def _set_field_value(self, dateval, fieldnum, new_value): values = {} for i, field in enumerate(self.fields): if field.REAL: if i < fieldnum: values[field.name] = field.get_value(dateval) elif i > fieldnum: values[field.name] = field.get_min(dateval) else: values[field.name] = new_value return datetime(**values) def get_next_fire_time(self, start_date): if self.start_date: start_date = max(start_date, self.start_date) next_date = datetime_ceil(start_date) fieldnum = 0 while 0 <= fieldnum < len(self.fields): field = self.fields[fieldnum] curr_value = field.get_value(next_date) next_value = field.get_next_value(next_date) if next_value is None: # No valid value was found next_date, fieldnum = self._increment_field_value( next_date, fieldnum - 1) elif next_value > curr_value: # A valid, but higher than the starting value, was found if field.REAL: next_date = self._set_field_value( next_date, fieldnum, next_value) fieldnum += 1 else: next_date, fieldnum = self._increment_field_value( next_date, fieldnum) else: # A valid value was found, no changes necessary fieldnum += 1 if fieldnum >= 0: return next_date def __str__(self): options = ["%s='%s'" % (f.name, str(f)) for f in self.fields if not f.is_default] return 'cron[%s]' % (', '.join(options)) def __repr__(self): options = ["%s='%s'" % (f.name, str(f)) for f in self.fields if not f.is_default] if self.start_date: options.append("start_date='%s'" % self.start_date.isoformat(' ')) return '<%s (%s)>' % (self.__class__.__name__, ', '.join(options)) APScheduler-2.1.2/apscheduler/__init__.py0000644000175000017500000000017712266456627016754 0ustar alexalexversion_info = (2, 1, 2) version = '.'.join(str(n) for n in version_info[:3]) release = '.'.join(str(n) for n in version_info) APScheduler-2.1.2/apscheduler/job.py0000644000175000017500000001140112266453403015744 0ustar alexalex""" Jobs represent scheduled tasks. """ from threading import Lock from datetime import timedelta from apscheduler.util import to_unicode, ref_to_obj, get_callable_name,\ obj_to_ref class MaxInstancesReachedError(Exception): pass class Job(object): """ Encapsulates the actual Job along with its metadata. Job instances are created by the scheduler when adding jobs, and should not be directly instantiated. These options can be set when adding jobs to the scheduler (see :ref:`job_options`). :var trigger: trigger that determines the execution times :var func: callable to call when the trigger is triggered :var args: list of positional arguments to call func with :var kwargs: dict of keyword arguments to call func with :var name: name of the job :var misfire_grace_time: seconds after the designated run time that the job is still allowed to be run :var coalesce: run once instead of many times if the scheduler determines that the job should be run more than once in succession :var max_runs: maximum number of times this job is allowed to be triggered :var max_instances: maximum number of concurrently running instances allowed for this job :var runs: number of times this job has been triggered :var instances: number of concurrently running instances of this job """ id = None next_run_time = None def __init__(self, trigger, func, args, kwargs, misfire_grace_time, coalesce, name=None, max_runs=None, max_instances=1): if not trigger: raise ValueError('The trigger must not be None') if not hasattr(func, '__call__'): raise TypeError('func must be callable') if not hasattr(args, '__getitem__'): raise TypeError('args must be a list-like object') if not hasattr(kwargs, '__getitem__'): raise TypeError('kwargs must be a dict-like object') if misfire_grace_time <= 0: raise ValueError('misfire_grace_time must be a positive value') if max_runs is not None and max_runs <= 0: raise ValueError('max_runs must be a positive value') if max_instances <= 0: raise ValueError('max_instances must be a positive value') self._lock = Lock() self.trigger = trigger self.func = func self.args = args self.kwargs = kwargs self.name = to_unicode(name or get_callable_name(func)) self.misfire_grace_time = misfire_grace_time self.coalesce = coalesce self.max_runs = max_runs self.max_instances = max_instances self.runs = 0 self.instances = 0 def compute_next_run_time(self, now): if self.runs == self.max_runs: self.next_run_time = None else: self.next_run_time = self.trigger.get_next_fire_time(now) return self.next_run_time def get_run_times(self, now): """ Computes the scheduled run times between ``next_run_time`` and ``now``. """ run_times = [] run_time = self.next_run_time increment = timedelta(microseconds=1) while ((not self.max_runs or self.runs < self.max_runs) and run_time and run_time <= now): run_times.append(run_time) run_time = self.trigger.get_next_fire_time(run_time + increment) return run_times def add_instance(self): self._lock.acquire() try: if self.instances == self.max_instances: raise MaxInstancesReachedError self.instances += 1 finally: self._lock.release() def remove_instance(self): self._lock.acquire() try: assert self.instances > 0, 'Already at 0 instances' self.instances -= 1 finally: self._lock.release() def __getstate__(self): # Prevents the unwanted pickling of transient or unpicklable variables state = self.__dict__.copy() state.pop('instances', None) state.pop('func', None) state.pop('_lock', None) state['func_ref'] = obj_to_ref(self.func) return state def __setstate__(self, state): state['instances'] = 0 state['func'] = ref_to_obj(state.pop('func_ref')) state['_lock'] = Lock() self.__dict__ = state def __eq__(self, other): if isinstance(other, Job): return self.id is not None and other.id == self.id or self is other return NotImplemented def __repr__(self): return '' % (self.name, repr(self.trigger)) def __str__(self): return '%s (trigger: %s, next run at: %s)' % ( self.name, str(self.trigger), str(self.next_run_time)) APScheduler-2.1.2/apscheduler/jobstores/0000755000175000017500000000000012266471145016640 5ustar alexalexAPScheduler-2.1.2/apscheduler/jobstores/redis_store.py0000644000175000017500000000537712266453403021545 0ustar alexalex""" Stores jobs in a Redis database. """ from uuid import uuid4 from datetime import datetime import logging from apscheduler.jobstores.base import JobStore from apscheduler.job import Job try: import cPickle as pickle except ImportError: # pragma: nocover import pickle try: from redis import StrictRedis except ImportError: # pragma: nocover raise ImportError('RedisJobStore requires redis installed') try: long = long except NameError: long = int logger = logging.getLogger(__name__) class RedisJobStore(JobStore): def __init__(self, db=0, key_prefix='jobs.', pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): self.jobs = [] self.pickle_protocol = pickle_protocol self.key_prefix = key_prefix if db is None: raise ValueError('The "db" parameter must not be empty') if not key_prefix: raise ValueError('The "key_prefix" parameter must not be empty') self.redis = StrictRedis(db=db, **connect_args) def add_job(self, job): job.id = str(uuid4()) job_state = job.__getstate__() job_dict = { 'job_state': pickle.dumps(job_state, self.pickle_protocol), 'runs': '0', 'next_run_time': job_state.pop('next_run_time').isoformat()} self.redis.hmset(self.key_prefix + job.id, job_dict) self.jobs.append(job) def remove_job(self, job): self.redis.delete(self.key_prefix + job.id) self.jobs.remove(job) def load_jobs(self): jobs = [] keys = self.redis.keys(self.key_prefix + '*') pipeline = self.redis.pipeline() for key in keys: pipeline.hgetall(key) results = pipeline.execute() for job_dict in results: job_state = {} try: job = Job.__new__(Job) job_state = pickle.loads(job_dict['job_state'.encode()]) job_state['runs'] = long(job_dict['runs'.encode()]) dateval = job_dict['next_run_time'.encode()].decode() job_state['next_run_time'] = datetime.strptime( dateval, '%Y-%m-%dT%H:%M:%S') job.__setstate__(job_state) jobs.append(job) except Exception: job_name = job_state.get('name', '(unknown)') logger.exception('Unable to restore job "%s"', job_name) self.jobs = jobs def update_job(self, job): attrs = { 'next_run_time': job.next_run_time.isoformat(), 'runs': job.runs} self.redis.hmset(self.key_prefix + job.id, attrs) def close(self): self.redis.connection_pool.disconnect() def __repr__(self): return '<%s>' % self.__class__.__name__ APScheduler-2.1.2/apscheduler/jobstores/mongodb_store.py0000644000175000017500000000552712266453403022061 0ustar alexalex""" Stores jobs in a MongoDB database. """ import logging from apscheduler.jobstores.base import JobStore from apscheduler.job import Job try: import cPickle as pickle except ImportError: # pragma: nocover import pickle try: from bson.binary import Binary from pymongo.connection import Connection except ImportError: # pragma: nocover raise ImportError('MongoDBJobStore requires PyMongo installed') logger = logging.getLogger(__name__) class MongoDBJobStore(JobStore): def __init__(self, database='apscheduler', collection='jobs', connection=None, pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): self.jobs = [] self.pickle_protocol = pickle_protocol if not database: raise ValueError('The "database" parameter must not be empty') if not collection: raise ValueError('The "collection" parameter must not be empty') if connection: self.connection = connection else: self.connection = Connection(**connect_args) self.collection = self.connection[database][collection] def add_job(self, job): job_dict = job.__getstate__() job_dict['trigger'] = Binary(pickle.dumps(job.trigger, self.pickle_protocol)) job_dict['args'] = Binary(pickle.dumps(job.args, self.pickle_protocol)) job_dict['kwargs'] = Binary(pickle.dumps(job.kwargs, self.pickle_protocol)) job.id = self.collection.insert(job_dict) self.jobs.append(job) def remove_job(self, job): self.collection.remove(job.id) self.jobs.remove(job) def load_jobs(self): jobs = [] for job_dict in self.collection.find(): try: job = Job.__new__(Job) job_dict['id'] = job_dict.pop('_id') job_dict['trigger'] = pickle.loads(job_dict['trigger']) job_dict['args'] = pickle.loads(job_dict['args']) job_dict['kwargs'] = pickle.loads(job_dict['kwargs']) job.__setstate__(job_dict) jobs.append(job) except Exception: job_name = job_dict.get('name', '(unknown)') logger.exception('Unable to restore job "%s"', job_name) self.jobs = jobs def update_job(self, job): spec = {'_id': job.id} document = {'$set': {'next_run_time': job.next_run_time}, '$inc': {'runs': 1}} self.collection.update(spec, document) def close(self): self.connection.disconnect() def __repr__(self): connection = self.collection.database.connection return '<%s (connection=%s)>' % (self.__class__.__name__, connection) APScheduler-2.1.2/apscheduler/jobstores/__init__.py0000644000175000017500000000000012266453346020742 0ustar alexalexAPScheduler-2.1.2/apscheduler/jobstores/base.py0000644000175000017500000000130612266453346020127 0ustar alexalex""" Abstract base class that provides the interface needed by all job stores. Job store methods are also documented here. """ class JobStore(object): def add_job(self, job): """Adds the given job from this store.""" raise NotImplementedError def update_job(self, job): """Persists the running state of the given job.""" raise NotImplementedError def remove_job(self, job): """Removes the given jobs from this store.""" raise NotImplementedError def load_jobs(self): """Loads jobs from this store into memory.""" raise NotImplementedError def close(self): """Frees any resources still bound to this job store.""" APScheduler-2.1.2/apscheduler/jobstores/shelve_store.py0000644000175000017500000000366612266453613021727 0ustar alexalex""" Stores jobs in a file governed by the :mod:`shelve` module. """ import shelve import pickle import random import logging from apscheduler.jobstores.base import JobStore from apscheduler.job import Job from apscheduler.util import itervalues logger = logging.getLogger(__name__) class ShelveJobStore(JobStore): MAX_ID = 1000000 def __init__(self, path, pickle_protocol=pickle.HIGHEST_PROTOCOL): self.jobs = [] self.path = path self.pickle_protocol = pickle_protocol self._open_store() def _open_store(self): self.store = shelve.open(self.path, 'c', self.pickle_protocol) def _generate_id(self): id = None while not id: id = str(random.randint(1, self.MAX_ID)) if not id in self.store: return id def add_job(self, job): job.id = self._generate_id() self.store[job.id] = job.__getstate__() self.store.close() self._open_store() self.jobs.append(job) def update_job(self, job): job_dict = self.store[job.id] job_dict['next_run_time'] = job.next_run_time job_dict['runs'] = job.runs self.store[job.id] = job_dict self.store.close() self._open_store() def remove_job(self, job): del self.store[job.id] self.store.close() self._open_store() self.jobs.remove(job) def load_jobs(self): jobs = [] for job_dict in itervalues(self.store): try: job = Job.__new__(Job) job.__setstate__(job_dict) jobs.append(job) except Exception: job_name = job_dict.get('name', '(unknown)') logger.exception('Unable to restore job "%s"', job_name) self.jobs = jobs def close(self): self.store.close() def __repr__(self): return '<%s (path=%s)>' % (self.__class__.__name__, self.path) APScheduler-2.1.2/apscheduler/jobstores/ram_store.py0000644000175000017500000000074012266453403021203 0ustar alexalex""" Stores jobs in an array in RAM. Provides no persistence support. """ from apscheduler.jobstores.base import JobStore class RAMJobStore(JobStore): def __init__(self): self.jobs = [] def add_job(self, job): self.jobs.append(job) def update_job(self, job): pass def remove_job(self, job): self.jobs.remove(job) def load_jobs(self): pass def __repr__(self): return '<%s>' % (self.__class__.__name__) APScheduler-2.1.2/apscheduler/jobstores/sqlalchemy_store.py0000644000175000017500000000616112266453403022571 0ustar alexalex""" Stores jobs in a database table using SQLAlchemy. """ import pickle import logging import sqlalchemy from apscheduler.jobstores.base import JobStore from apscheduler.job import Job try: from sqlalchemy import * except ImportError: # pragma: nocover raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed') logger = logging.getLogger(__name__) class SQLAlchemyJobStore(JobStore): def __init__(self, url=None, engine=None, tablename='apscheduler_jobs', metadata=None, pickle_protocol=pickle.HIGHEST_PROTOCOL): self.jobs = [] self.pickle_protocol = pickle_protocol if engine: self.engine = engine elif url: self.engine = create_engine(url) else: raise ValueError('Need either "engine" or "url" defined') if sqlalchemy.__version__ < '0.7': pickle_coltype = PickleType(pickle_protocol, mutable=False) else: pickle_coltype = PickleType(pickle_protocol) self.jobs_t = Table( tablename, metadata or MetaData(), Column('id', Integer, Sequence(tablename + '_id_seq', optional=True), primary_key=True), Column('trigger', pickle_coltype, nullable=False), Column('func_ref', String(1024), nullable=False), Column('args', pickle_coltype, nullable=False), Column('kwargs', pickle_coltype, nullable=False), Column('name', Unicode(1024)), Column('misfire_grace_time', Integer, nullable=False), Column('coalesce', Boolean, nullable=False), Column('max_runs', Integer), Column('max_instances', Integer), Column('next_run_time', DateTime, nullable=False), Column('runs', BigInteger)) self.jobs_t.create(self.engine, True) def add_job(self, job): job_dict = job.__getstate__() result = self.engine.execute(self.jobs_t.insert().values(**job_dict)) job.id = result.inserted_primary_key[0] self.jobs.append(job) def remove_job(self, job): delete = self.jobs_t.delete().where(self.jobs_t.c.id == job.id) self.engine.execute(delete) self.jobs.remove(job) def load_jobs(self): jobs = [] for row in self.engine.execute(select([self.jobs_t])): try: job = Job.__new__(Job) job_dict = dict(row.items()) job.__setstate__(job_dict) jobs.append(job) except Exception: job_name = job_dict.get('name', '(unknown)') logger.exception('Unable to restore job "%s"', job_name) self.jobs = jobs def update_job(self, job): job_dict = job.__getstate__() update = self.jobs_t.update().where(self.jobs_t.c.id == job.id).\ values(next_run_time=job_dict['next_run_time'], runs=job_dict['runs']) self.engine.execute(update) def close(self): self.engine.dispose() def __repr__(self): return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url) APScheduler-2.1.2/apscheduler/events.py0000644000175000017500000000474112266453403016507 0ustar alexalex__all__ = ('EVENT_SCHEDULER_START', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_JOBSTORE_JOB_ADDED', 'EVENT_JOBSTORE_JOB_REMOVED', 'EVENT_JOB_EXECUTED', 'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_ALL', 'SchedulerEvent', 'JobStoreEvent', 'JobEvent') EVENT_SCHEDULER_START = 1 # The scheduler was started EVENT_SCHEDULER_SHUTDOWN = 2 # The scheduler was shut down EVENT_JOBSTORE_ADDED = 4 # A job store was added to the scheduler EVENT_JOBSTORE_REMOVED = 8 # A job store was removed from the scheduler EVENT_JOBSTORE_JOB_ADDED = 16 # A job was added to a job store EVENT_JOBSTORE_JOB_REMOVED = 32 # A job was removed from a job store EVENT_JOB_EXECUTED = 64 # A job was executed successfully EVENT_JOB_ERROR = 128 # A job raised an exception during execution EVENT_JOB_MISSED = 256 # A job's execution was missed EVENT_ALL = (EVENT_SCHEDULER_START | EVENT_SCHEDULER_SHUTDOWN | EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED | EVENT_JOBSTORE_JOB_ADDED | EVENT_JOBSTORE_JOB_REMOVED | EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED) class SchedulerEvent(object): """ An event that concerns the scheduler itself. :var code: the type code of this event """ def __init__(self, code): self.code = code class JobStoreEvent(SchedulerEvent): """ An event that concerns job stores. :var alias: the alias of the job store involved :var job: the new job if a job was added """ def __init__(self, code, alias, job=None): SchedulerEvent.__init__(self, code) self.alias = alias if job: self.job = job class JobEvent(SchedulerEvent): """ An event that concerns the execution of individual jobs. :var job: the job instance in question :var scheduled_run_time: the time when the job was scheduled to be run :var retval: the return value of the successfully executed job :var exception: the exception raised by the job :var traceback: the traceback object associated with the exception """ def __init__(self, code, job, scheduled_run_time, retval=None, exception=None, traceback=None): SchedulerEvent.__init__(self, code) self.job = job self.scheduled_run_time = scheduled_run_time self.retval = retval self.exception = exception self.traceback = traceback APScheduler-2.1.2/apscheduler/scheduler.py0000644000175000017500000005546612266456364017204 0ustar alexalex""" This module is the main part of the library. It houses the Scheduler class and related exceptions. """ from threading import Thread, Event, Lock from datetime import datetime, timedelta from logging import getLogger import os import sys from apscheduler.util import * from apscheduler.triggers import SimpleTrigger, IntervalTrigger, CronTrigger from apscheduler.jobstores.ram_store import RAMJobStore from apscheduler.job import Job, MaxInstancesReachedError from apscheduler.events import * from apscheduler.threadpool import ThreadPool logger = getLogger(__name__) class SchedulerAlreadyRunningError(Exception): """ Raised when attempting to start or configure the scheduler when it's already running. """ def __str__(self): return 'Scheduler is already running' class Scheduler(object): """ This class is responsible for scheduling jobs and triggering their execution. """ _stopped = True _thread = None def __init__(self, gconfig={}, **options): self._wakeup = Event() self._jobstores = {} self._jobstores_lock = Lock() self._listeners = [] self._listeners_lock = Lock() self._pending_jobs = [] self.configure(gconfig, **options) def configure(self, gconfig={}, **options): """ Reconfigures the scheduler with the given options. Can only be done when the scheduler isn't running. """ if self.running: raise SchedulerAlreadyRunningError # Set general options config = combine_opts(gconfig, 'apscheduler.', options) self.misfire_grace_time = int(config.pop('misfire_grace_time', 1)) self.coalesce = asbool(config.pop('coalesce', True)) self.daemonic = asbool(config.pop('daemonic', True)) self.standalone = asbool(config.pop('standalone', False)) # Configure the thread pool if 'threadpool' in config: self._threadpool = maybe_ref(config['threadpool']) else: threadpool_opts = combine_opts(config, 'threadpool.') self._threadpool = ThreadPool(**threadpool_opts) # Configure job stores jobstore_opts = combine_opts(config, 'jobstore.') jobstores = {} for key, value in jobstore_opts.items(): store_name, option = key.split('.', 1) opts_dict = jobstores.setdefault(store_name, {}) opts_dict[option] = value for alias, opts in jobstores.items(): classname = opts.pop('class') cls = maybe_ref(classname) jobstore = cls(**opts) self.add_jobstore(jobstore, alias, True) def start(self): """ Starts the scheduler in a new thread. In threaded mode (the default), this method will return immediately after starting the scheduler thread. In standalone mode, this method will block until there are no more scheduled jobs. """ if self.running: raise SchedulerAlreadyRunningError # Create a RAMJobStore as the default if there is no default job store if not 'default' in self._jobstores: self.add_jobstore(RAMJobStore(), 'default', True) # Schedule all pending jobs for job, jobstore in self._pending_jobs: self._real_add_job(job, jobstore, False) del self._pending_jobs[:] self._stopped = False if self.standalone: self._main_loop() else: self._thread = Thread(target=self._main_loop, name='APScheduler') self._thread.setDaemon(self.daemonic) self._thread.start() def shutdown(self, wait=True, shutdown_threadpool=True, close_jobstores=True): """ Shuts down the scheduler and terminates the thread. Does not interrupt any currently running jobs. :param wait: ``True`` to wait until all currently executing jobs have finished (if ``shutdown_threadpool`` is also ``True``) :param shutdown_threadpool: ``True`` to shut down the thread pool :param close_jobstores: ``True`` to close all job stores after shutdown """ if not self.running: return self._stopped = True self._wakeup.set() # Shut down the thread pool if shutdown_threadpool: self._threadpool.shutdown(wait) # Wait until the scheduler thread terminates if self._thread: self._thread.join() # Close all job stores if close_jobstores: for jobstore in itervalues(self._jobstores): jobstore.close() @property def running(self): thread_alive = self._thread and self._thread.isAlive() standalone = getattr(self, 'standalone', False) return not self._stopped and (standalone or thread_alive) def add_jobstore(self, jobstore, alias, quiet=False): """ Adds a job store to this scheduler. :param jobstore: job store to be added :param alias: alias for the job store :param quiet: True to suppress scheduler thread wakeup :type jobstore: instance of :class:`~apscheduler.jobstores.base.JobStore` :type alias: str """ self._jobstores_lock.acquire() try: if alias in self._jobstores: raise KeyError('Alias "%s" is already in use' % alias) self._jobstores[alias] = jobstore jobstore.load_jobs() finally: self._jobstores_lock.release() # Notify listeners that a new job store has been added self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_ADDED, alias)) # Notify the scheduler so it can scan the new job store for jobs if not quiet: self._wakeup.set() def remove_jobstore(self, alias, close=True): """ Removes the job store by the given alias from this scheduler. :param close: ``True`` to close the job store after removing it :type alias: str """ self._jobstores_lock.acquire() try: jobstore = self._jobstores.pop(alias) if not jobstore: raise KeyError('No such job store: %s' % alias) finally: self._jobstores_lock.release() # Close the job store if requested if close: jobstore.close() # Notify listeners that a job store has been removed self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_REMOVED, alias)) def add_listener(self, callback, mask=EVENT_ALL): """ Adds a listener for scheduler events. When a matching event occurs, ``callback`` is executed with the event object as its sole argument. If the ``mask`` parameter is not provided, the callback will receive events of all types. :param callback: any callable that takes one argument :param mask: bitmask that indicates which events should be listened to """ self._listeners_lock.acquire() try: self._listeners.append((callback, mask)) finally: self._listeners_lock.release() def remove_listener(self, callback): """ Removes a previously added event listener. """ self._listeners_lock.acquire() try: for i, (cb, _) in enumerate(self._listeners): if callback == cb: del self._listeners[i] finally: self._listeners_lock.release() def _notify_listeners(self, event): self._listeners_lock.acquire() try: listeners = tuple(self._listeners) finally: self._listeners_lock.release() for cb, mask in listeners: if event.code & mask: try: cb(event) except: logger.exception('Error notifying listener') def _real_add_job(self, job, jobstore, wakeup): job.compute_next_run_time(datetime.now()) if not job.next_run_time: raise ValueError('Not adding job since it would never be run') self._jobstores_lock.acquire() try: try: store = self._jobstores[jobstore] except KeyError: raise KeyError('No such job store: %s' % jobstore) store.add_job(job) finally: self._jobstores_lock.release() # Notify listeners that a new job has been added event = JobStoreEvent(EVENT_JOBSTORE_JOB_ADDED, jobstore, job) self._notify_listeners(event) logger.info('Added job "%s" to job store "%s"', job, jobstore) # Notify the scheduler about the new job if wakeup: self._wakeup.set() def add_job(self, trigger, func, args, kwargs, jobstore='default', **options): """ Adds the given job to the job list and notifies the scheduler thread. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). :param trigger: trigger that determines when ``func`` is called :param func: callable to run at the given time :param args: list of positional arguments to call func with :param kwargs: dict of keyword arguments to call func with :param jobstore: alias of the job store to store the job in :rtype: :class:`~apscheduler.job.Job` """ job = Job(trigger, func, args or [], kwargs or {}, options.pop('misfire_grace_time', self.misfire_grace_time), options.pop('coalesce', self.coalesce), **options) if not self.running: self._pending_jobs.append((job, jobstore)) logger.info('Adding job tentatively -- it will be properly ' 'scheduled when the scheduler starts') else: self._real_add_job(job, jobstore, True) return job def _remove_job(self, job, alias, jobstore): jobstore.remove_job(job) # Notify listeners that a job has been removed event = JobStoreEvent(EVENT_JOBSTORE_JOB_REMOVED, alias, job) self._notify_listeners(event) logger.info('Removed job "%s"', job) def add_date_job(self, func, date, args=None, kwargs=None, **options): """ Schedules a job to be completed on a specific date and time. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). :param func: callable to run at the given time :param date: the date/time to run the job at :param name: name of the job :param jobstore: stored the job in the named (or given) job store :param misfire_grace_time: seconds after the designated run time that the job is still allowed to be run :type date: :class:`datetime.date` :rtype: :class:`~apscheduler.job.Job` """ trigger = SimpleTrigger(date) return self.add_job(trigger, func, args, kwargs, **options) def add_interval_job(self, func, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None, args=None, kwargs=None, **options): """ Schedules a job to be completed on specified intervals. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). :param func: callable to run :param weeks: number of weeks to wait :param days: number of days to wait :param hours: number of hours to wait :param minutes: number of minutes to wait :param seconds: number of seconds to wait :param start_date: when to first execute the job and start the counter (default is after the given interval) :param args: list of positional arguments to call func with :param kwargs: dict of keyword arguments to call func with :param name: name of the job :param jobstore: alias of the job store to add the job to :param misfire_grace_time: seconds after the designated run time that the job is still allowed to be run :rtype: :class:`~apscheduler.job.Job` """ interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds) trigger = IntervalTrigger(interval, start_date) return self.add_job(trigger, func, args, kwargs, **options) def add_cron_job(self, func, year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None, second=None, start_date=None, args=None, kwargs=None, **options): """ Schedules a job to be completed on times that match the given expressions. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). :param func: callable to run :param year: year to run on :param month: month to run on :param day: day of month to run on :param week: week of the year to run on :param day_of_week: weekday to run on (0 = Monday) :param hour: hour to run on :param second: second to run on :param args: list of positional arguments to call func with :param kwargs: dict of keyword arguments to call func with :param name: name of the job :param jobstore: alias of the job store to add the job to :param misfire_grace_time: seconds after the designated run time that the job is still allowed to be run :return: the scheduled job :rtype: :class:`~apscheduler.job.Job` """ trigger = CronTrigger(year=year, month=month, day=day, week=week, day_of_week=day_of_week, hour=hour, minute=minute, second=second, start_date=start_date) return self.add_job(trigger, func, args, kwargs, **options) def cron_schedule(self, **options): """ Decorator version of :meth:`add_cron_job`. This decorator does not wrap its host function. Unscheduling decorated functions is possible by passing the ``job`` attribute of the scheduled function to :meth:`unschedule_job`. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). """ def inner(func): func.job = self.add_cron_job(func, **options) return func return inner def interval_schedule(self, **options): """ Decorator version of :meth:`add_interval_job`. This decorator does not wrap its host function. Unscheduling decorated functions is possible by passing the ``job`` attribute of the scheduled function to :meth:`unschedule_job`. Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class (see :ref:`job_options`). """ def inner(func): func.job = self.add_interval_job(func, **options) return func return inner def get_jobs(self): """ Returns a list of all scheduled jobs. :return: list of :class:`~apscheduler.job.Job` objects """ self._jobstores_lock.acquire() try: jobs = [] for jobstore in itervalues(self._jobstores): jobs.extend(jobstore.jobs) return jobs finally: self._jobstores_lock.release() def unschedule_job(self, job): """ Removes a job, preventing it from being run any more. """ self._jobstores_lock.acquire() try: for alias, jobstore in iteritems(self._jobstores): if job in list(jobstore.jobs): self._remove_job(job, alias, jobstore) return finally: self._jobstores_lock.release() raise KeyError('Job "%s" is not scheduled in any job store' % job) def unschedule_func(self, func): """ Removes all jobs that would execute the given function. """ found = False self._jobstores_lock.acquire() try: for alias, jobstore in iteritems(self._jobstores): for job in list(jobstore.jobs): if job.func == func: self._remove_job(job, alias, jobstore) found = True finally: self._jobstores_lock.release() if not found: raise KeyError('The given function is not scheduled in this ' 'scheduler') def print_jobs(self, out=None): """ Prints out a textual listing of all jobs currently scheduled on this scheduler. :param out: a file-like object to print to (defaults to **sys.stdout** if nothing is given) """ out = out or sys.stdout job_strs = [] self._jobstores_lock.acquire() try: for alias, jobstore in iteritems(self._jobstores): job_strs.append('Jobstore %s:' % alias) if jobstore.jobs: for job in jobstore.jobs: job_strs.append(' %s' % job) else: job_strs.append(' No scheduled jobs') finally: self._jobstores_lock.release() out.write(os.linesep.join(job_strs) + os.linesep) def _run_job(self, job, run_times): """ Acts as a harness that runs the actual job code in a thread. """ for run_time in run_times: # See if the job missed its run time window, and handle possible # misfires accordingly difference = datetime.now() - run_time grace_time = timedelta(seconds=job.misfire_grace_time) if difference > grace_time: # Notify listeners about a missed run event = JobEvent(EVENT_JOB_MISSED, job, run_time) self._notify_listeners(event) logger.warning('Run time of job "%s" was missed by %s', job, difference) else: try: job.add_instance() except MaxInstancesReachedError: event = JobEvent(EVENT_JOB_MISSED, job, run_time) self._notify_listeners(event) logger.warning('Execution of job "%s" skipped: ' 'maximum number of running instances ' 'reached (%d)', job, job.max_instances) break logger.info('Running job "%s" (scheduled at %s)', job, run_time) try: retval = job.func(*job.args, **job.kwargs) except: # Notify listeners about the exception exc, tb = sys.exc_info()[1:] event = JobEvent(EVENT_JOB_ERROR, job, run_time, exception=exc, traceback=tb) self._notify_listeners(event) logger.exception('Job "%s" raised an exception', job) else: # Notify listeners about successful execution event = JobEvent(EVENT_JOB_EXECUTED, job, run_time, retval=retval) self._notify_listeners(event) logger.info('Job "%s" executed successfully', job) job.remove_instance() # If coalescing is enabled, don't attempt any further runs if job.coalesce: break def _process_jobs(self, now): """ Iterates through jobs in every jobstore, starts pending jobs and figures out the next wakeup time. """ next_wakeup_time = None self._jobstores_lock.acquire() try: for alias, jobstore in iteritems(self._jobstores): for job in tuple(jobstore.jobs): run_times = job.get_run_times(now) if run_times: self._threadpool.submit(self._run_job, job, run_times) # Increase the job's run count if job.coalesce: job.runs += 1 else: job.runs += len(run_times) # Update the job, but don't keep finished jobs around if job.compute_next_run_time( now + timedelta(microseconds=1)): jobstore.update_job(job) else: self._remove_job(job, alias, jobstore) if not next_wakeup_time: next_wakeup_time = job.next_run_time elif job.next_run_time: next_wakeup_time = min(next_wakeup_time, job.next_run_time) return next_wakeup_time finally: self._jobstores_lock.release() def _main_loop(self): """Executes jobs on schedule.""" logger.info('Scheduler started') self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_START)) self._wakeup.clear() while not self._stopped: logger.debug('Looking for jobs to run') now = datetime.now() next_wakeup_time = self._process_jobs(now) # Sleep until the next job is scheduled to be run, # a new job is added or the scheduler is stopped if next_wakeup_time is not None: wait_seconds = time_difference(next_wakeup_time, now) logger.debug('Next wakeup is due at %s (in %f seconds)', next_wakeup_time, wait_seconds) try: self._wakeup.wait(wait_seconds) except IOError: # Catch errno 514 on some Linux kernels pass self._wakeup.clear() elif self.standalone: logger.debug('No jobs left; shutting down scheduler') self.shutdown() break else: logger.debug('No jobs; waiting until a job is added') try: self._wakeup.wait() except IOError: # Catch errno 514 on some Linux kernels pass self._wakeup.clear() logger.info('Scheduler has been shut down') self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN)) APScheduler-2.1.2/README.rst0000644000175000017500000000364412266460672014030 0ustar alexalexAdvanced Python Scheduler (APScheduler) is a light but powerful in-process task scheduler that lets you schedule jobs (functions or any python callables) to be executed at times of your choosing. This can be a far better alternative to externally run cron scripts for long-running applications (e.g. web applications), as it is platform neutral and can directly access your application's variables and functions. The development of APScheduler was heavily influenced by the `Quartz `_ task scheduler written in Java. APScheduler provides most of the major features that Quartz does, but it also provides features not present in Quartz (such as multiple job stores). Features ======== * No (hard) external dependencies * Thread-safe API * Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 2.2) * Configurable scheduling mechanisms (triggers): * Cron-like scheduling * Delayed scheduling of single run jobs (like the UNIX "at" command) * Interval-based (run a job at specified time intervals) * Multiple, simultaneously active job stores: * RAM * File-based simple database (shelve) * `SQLAlchemy `_ (any supported RDBMS works) * `MongoDB `_ * `Redis `_ Documentation ============= Documentation can be found `here `_. Source ====== The source can be browsed at `Bitbucket `_. Reporting bugs ============== A `bug tracker `_ is provided by bitbucket.org. Getting help ============ If you have problems or other questions, you can either: * Ask on the `APScheduler Google group `_, or * Ask on the ``#apscheduler`` channel on `Freenode IRC `_ APScheduler-2.1.2/setup.cfg0000644000175000017500000000027112266471145014150 0ustar alexalex[wheel] universal = 1 [build_sphinx] build-dir = docs/_build source-dir = docs [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [upload_docs] upload-dir = docs/_build/html APScheduler-2.1.2/APScheduler.egg-info/0000755000175000017500000000000012266471145016160 5ustar alexalexAPScheduler-2.1.2/APScheduler.egg-info/SOURCES.txt0000644000175000017500000000257512266471145020055 0ustar alexalexMANIFEST.in README.rst setup.cfg setup.py APScheduler.egg-info/PKG-INFO APScheduler.egg-info/SOURCES.txt APScheduler.egg-info/dependency_links.txt APScheduler.egg-info/top_level.txt apscheduler/__init__.py apscheduler/events.py apscheduler/job.py apscheduler/scheduler.py apscheduler/threadpool.py apscheduler/util.py apscheduler/jobstores/__init__.py apscheduler/jobstores/base.py apscheduler/jobstores/mongodb_store.py apscheduler/jobstores/ram_store.py apscheduler/jobstores/redis_store.py apscheduler/jobstores/shelve_store.py apscheduler/jobstores/sqlalchemy_store.py apscheduler/triggers/__init__.py apscheduler/triggers/interval.py apscheduler/triggers/simple.py apscheduler/triggers/cron/__init__.py apscheduler/triggers/cron/expressions.py apscheduler/triggers/cron/fields.py docs/conf.py docs/cronschedule.rst docs/dateschedule.rst docs/extending.rst docs/index.rst docs/intervalschedule.rst docs/migration.rst docs/modules/events.rst docs/modules/job.rst docs/modules/scheduler.rst docs/modules/jobstores/mongodb.rst docs/modules/jobstores/ram.rst docs/modules/jobstores/redis.rst docs/modules/jobstores/shelve.rst docs/modules/jobstores/sqlalchemy.rst examples/interval.py examples/persistent.py examples/threaded.py tests/testexpressions.py tests/testintegration.py tests/testjob.py tests/testjobstores.py tests/testscheduler.py tests/testthreadpool.py tests/testtriggers.py tests/testutil.pyAPScheduler-2.1.2/APScheduler.egg-info/PKG-INFO0000644000175000017500000000626412266471144017264 0ustar alexalexMetadata-Version: 1.0 Name: APScheduler Version: 2.1.2 Summary: In-process task scheduler with Cron-like capabilities Home-page: http://pypi.python.org/pypi/APScheduler/ Author: Alex Gronholm Author-email: apscheduler@nextday.fi License: MIT Description: Advanced Python Scheduler (APScheduler) is a light but powerful in-process task scheduler that lets you schedule jobs (functions or any python callables) to be executed at times of your choosing. This can be a far better alternative to externally run cron scripts for long-running applications (e.g. web applications), as it is platform neutral and can directly access your application's variables and functions. The development of APScheduler was heavily influenced by the `Quartz `_ task scheduler written in Java. APScheduler provides most of the major features that Quartz does, but it also provides features not present in Quartz (such as multiple job stores). Features ======== * No (hard) external dependencies * Thread-safe API * Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 2.2) * Configurable scheduling mechanisms (triggers): * Cron-like scheduling * Delayed scheduling of single run jobs (like the UNIX "at" command) * Interval-based (run a job at specified time intervals) * Multiple, simultaneously active job stores: * RAM * File-based simple database (shelve) * `SQLAlchemy `_ (any supported RDBMS works) * `MongoDB `_ * `Redis `_ Documentation ============= Documentation can be found `here `_. Source ====== The source can be browsed at `Bitbucket `_. Reporting bugs ============== A `bug tracker `_ is provided by bitbucket.org. Getting help ============ If you have problems or other questions, you can either: * Ask on the `APScheduler Google group `_, or * Ask on the ``#apscheduler`` channel on `Freenode IRC `_ Keywords: scheduling cron Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 APScheduler-2.1.2/APScheduler.egg-info/top_level.txt0000644000175000017500000000001412266471144020704 0ustar alexalexapscheduler APScheduler-2.1.2/APScheduler.egg-info/dependency_links.txt0000644000175000017500000000000112266471144022225 0ustar alexalex