summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst4
-rw-r--r--apscheduler/job.py35
-rw-r--r--apscheduler/scheduler.py206
-rw-r--r--apscheduler/triggers/__init__.py3
-rw-r--r--apscheduler/triggers/cron/__init__.py28
-rw-r--r--apscheduler/triggers/date.py (renamed from apscheduler/triggers/simple.py)7
-rw-r--r--apscheduler/triggers/interval.py17
-rw-r--r--docs/cronschedule.rst12
-rw-r--r--docs/dateschedule.rst8
-rw-r--r--docs/index.rst6
-rw-r--r--docs/intervalschedule.rst12
-rw-r--r--docs/migration.rst22
-rw-r--r--examples/interval.py2
-rw-r--r--examples/persistent.py3
-rw-r--r--examples/reference.py2
-rw-r--r--examples/threaded.py2
-rw-r--r--setup.py20
-rw-r--r--tests/testintegration.py6
-rw-r--r--tests/testjob.py52
-rw-r--r--tests/testjobstores.py12
-rw-r--r--tests/testscheduler.py57
-rw-r--r--tests/testtriggers.py20
22 files changed, 230 insertions, 306 deletions
diff --git a/README.rst b/README.rst
index 96b6525..98702c9 100644
--- a/README.rst
+++ b/README.rst
@@ -15,9 +15,9 @@ provides features not present in Quartz (such as multiple job stores).
Features
========
-* No (hard) external dependencies
+* No (hard) external dependencies, except for setuptools/distribute
* Thread-safe API
-* Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 1.9)
+* Excellent test coverage (tested on CPython 2.6 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 1.9)
* Configurable scheduling mechanisms (triggers):
* Cron-like scheduling
diff --git a/apscheduler/job.py b/apscheduler/job.py
index adc7586..a025983 100644
--- a/apscheduler/job.py
+++ b/apscheduler/job.py
@@ -5,8 +5,7 @@ Jobs represent scheduled tasks.
from threading import Lock
from datetime import timedelta
-from apscheduler.util import (to_unicode, ref_to_obj, obj_to_ref,
- get_callable_name)
+from apscheduler.util import to_unicode, ref_to_obj, obj_to_ref, get_callable_name
class MaxInstancesReachedError(Exception):
@@ -16,40 +15,12 @@ class MaxInstancesReachedError(Exception):
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
+ and should not be directly instantiated.
"""
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(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')
-
+ def __init__(self, trigger, func, args, kwargs, misfire_grace_time, coalesce, name, max_runs, max_instances):
if isinstance(func, str):
self.func = ref_to_obj(func)
self.func_ref = func
diff --git a/apscheduler/scheduler.py b/apscheduler/scheduler.py
index 599073a..5117fc9 100644
--- a/apscheduler/scheduler.py
+++ b/apscheduler/scheduler.py
@@ -5,11 +5,13 @@ This module is the main part of the library. It houses the Scheduler class and r
from threading import Thread, Event, Lock
from datetime import datetime, timedelta
from logging import getLogger
+from collections import Mapping, Iterable
import os
import sys
+from pkg_resources import iter_entry_points
+
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 *
@@ -34,6 +36,7 @@ class Scheduler(object):
_stopped = False
_thread = None
+ _plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.triggers'))
def __init__(self, gconfig={}, **options):
self._wakeup = Event()
@@ -42,6 +45,7 @@ class Scheduler(object):
self._listeners = []
self._listeners_lock = Lock()
self._pending_jobs = []
+ self._triggers = {}
self.configure(gconfig, **options)
def configure(self, gconfig={}, **options):
@@ -215,16 +219,14 @@ class Scheduler(object):
logger.exception('Error notifying listener')
def _real_add_job(self, job, jobstore, wakeup):
+ # Recalculate the next run time
job.compute_next_run_time(datetime.now())
- if not job.next_run_time:
- raise ValueError('Not adding job since it would never be run')
- with self._jobstores_lock:
- try:
- store = self._jobstores[jobstore]
- except KeyError:
- raise KeyError('No such job store: %s' % jobstore)
- store.add_job(job)
+ # Add the job to the given job store
+ store = self._jobstores.get(jobstore)
+ if not store:
+ raise KeyError('No such job store: %s' % jobstore)
+ store.add_job(job)
# Notify listeners that a new job has been added
event = JobStoreEvent(EVENT_JOBSTORE_JOB_ADDED, jobstore, job)
@@ -236,8 +238,8 @@ class Scheduler(object):
if wakeup:
self._wakeup.set()
- def add_job(self, trigger, func, args, kwargs, jobstore='default',
- **options):
+ def add_job(self, func, trigger, trigger_args=(), args=None, kwargs=None, misfire_grace_time=None, coalesce=None,
+ name=None, max_runs=None, max_instances=1, jobstore='default'):
"""
Adds the given job to the job list and notifies the scheduler thread.
@@ -245,26 +247,90 @@ class Scheduler(object):
``package.module:some.object`` format, where the first half (separated by ``:``) is an importable module and the
second half is a reference to the callable object, relative to the module.
- Any extra keyword arguments are passed along to the constructor of the :class:`~apscheduler.job.Job` class
- (see :ref:`job_options`).
+ The ``trigger`` argument can either be:
+
+ # the plugin name of the trigger (e.g. "cron"), in which case you should provide ``trigger_args`` as well
+ # an instance of the trigger
:param trigger: trigger that determines when ``func`` is called
+ :param trigger_args: arguments given to the constructor of the trigger class
:param func: callable (or a textual reference to one) 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
+ :param name: name of the job
+ :param misfire_grace_time: seconds after the designated run time that the job is still allowed to be run
+ :param coalesce: run once instead of many times if the scheduler determines that the job should be run more than once
+ in succession
+ :param max_runs: maximum number of times this job is allowed to be triggered
+ :param max_instances: maximum number of concurrently running instances allowed for this job
: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)
+ # Argument sanity checking
+ if args is not None and (not isinstance(args, Iterable) and not isinstance(args, str)):
+ raise TypeError('args must be an iterable')
+ if kwargs is not None and not isinstance(kwargs, Mapping):
+ raise TypeError('kwargs must be a dict-like object')
+ if misfire_grace_time is not None and 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')
+
+ # If trigger is a string, resolve it to a class, possibly by loading an entry point if necessary
+ if isinstance(trigger, str):
+ try:
+ trigger_cls = self._triggers[trigger]
+ except KeyError:
+ if trigger in self._plugins:
+ trigger_cls = self._triggers[trigger] = self._plugins[trigger].load()
+ if not callable(getattr(trigger_cls, 'get_next_fire_time')):
+ raise TypeError('The trigger entry point does not point to a trigger class')
+ else:
+ raise KeyError('No trigger by the name "%s" was found' % trigger)
+
+ if isinstance(trigger_args, Mapping):
+ trigger = trigger_cls(**trigger_args)
+ elif isinstance(trigger_args, Iterable):
+ trigger = trigger_cls(*trigger_args)
+ else:
+ raise ValueError('trigger_args must either be a dict-like object or an iterable')
+ elif not callable(getattr(trigger, 'get_next_fire_time')):
+ raise TypeError('Expected a trigger instance, got %s instead' % trigger.__class__.__name__)
+
+ # Replace with scheduler level defaults if values are missing
+ if misfire_grace_time is None:
+ misfire_grace_time = self.misfire_grace_time
+ if coalesce is None:
+ coalesce = self.coalesce
+
+ args = tuple(args) if args is not None else ()
+ kwargs = dict(kwargs) if kwargs is not None else {}
+ job = Job(trigger, func, args, kwargs, misfire_grace_time, coalesce, name, max_runs, max_instances)
+
+ # Ensure that dead-on-arrival jobs are never added
+ if job.compute_next_run_time(datetime.now()) is None:
+ raise ValueError('Not adding job since it would never be run')
+
+ # Don't really add jobs to job stores before the scheduler is up and running
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 scheduled_job(self, trigger, trigger_args=(), args=None, kwargs=None, jobstore='default',
+ misfire_grace_time=None, coalesce=None, name=None, max_runs=None, max_instances=1):
+ """A decorator version of :meth:`add_job`."""
+ def inner(func):
+ func.job = self.add_job(func, trigger, trigger_args, args, kwargs, misfire_grace_time, coalesce,
+ name, max_runs, max_instances, jobstore)
+ return func
+ return inner
+
def _remove_job(self, job, alias, jobstore):
jobstore.remove_job(job)
@@ -274,114 +340,6 @@ class Scheduler(object):
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.
diff --git a/apscheduler/triggers/__init__.py b/apscheduler/triggers/__init__.py
index 74a9788..e69de29 100644
--- a/apscheduler/triggers/__init__.py
+++ b/apscheduler/triggers/__init__.py
@@ -1,3 +0,0 @@
-from apscheduler.triggers.cron import CronTrigger
-from apscheduler.triggers.interval import IntervalTrigger
-from apscheduler.triggers.simple import SimpleTrigger
diff --git a/apscheduler/triggers/cron/__init__.py b/apscheduler/triggers/cron/__init__.py
index f216fae..2c4559e 100644
--- a/apscheduler/triggers/cron/__init__.py
+++ b/apscheduler/triggers/cron/__init__.py
@@ -17,18 +17,24 @@ class CronTrigger(object):
'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]
+ def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None,
+ second=None, start_date=None):
+ """
+ Triggers when current time matches all specified time constraints, emulating the UNIX cron scheduler.
+
+ :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 start_date: earliest possible date/time to trigger on
+ """
+ self.start_date = convert_to_datetime(start_date) if start_date else None
+ values = dict((key, value) for (key, value) in iteritems(locals())
+ if key in self.FIELD_NAMES and value is not None)
self.fields = []
assign_defaults = False
for field_name in self.FIELD_NAMES:
diff --git a/apscheduler/triggers/simple.py b/apscheduler/triggers/date.py
index ea61b3f..33a30f9 100644
--- a/apscheduler/triggers/simple.py
+++ b/apscheduler/triggers/date.py
@@ -1,8 +1,13 @@
from apscheduler.util import convert_to_datetime
-class SimpleTrigger(object):
+class DateTrigger(object):
def __init__(self, run_date):
+ """
+ Triggers once on the given datetime.
+
+ :param run_date: the date/time to run the job at
+ """
self.run_date = convert_to_datetime(run_date)
def get_next_fire_time(self, start_date):
diff --git a/apscheduler/triggers/interval.py b/apscheduler/triggers/interval.py
index e7dfc17..5c3206a 100644
--- a/apscheduler/triggers/interval.py
+++ b/apscheduler/triggers/interval.py
@@ -5,11 +5,18 @@ 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')
-
- self.interval = interval
+ def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None):
+ """
+ Triggers on specified intervals.
+
+ :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)
+ """
+ self.interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)
self.interval_length = timedelta_seconds(self.interval)
if self.interval_length == 0:
self.interval = timedelta(seconds=1)
diff --git a/docs/cronschedule.rst b/docs/cronschedule.rst
index b381be0..6af2547 100644
--- a/docs/cronschedule.rst
+++ b/docs/cronschedule.rst
@@ -84,7 +84,7 @@ Example 1
# 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')
+ sched.add_job(job_function, 'cron', {'month': '6-8,11-12', 'day': '3rd fri', 'hour': '0-3'})
Example 2
@@ -95,21 +95,15 @@ 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)
+ sched.add_job(backup, 'cron', {'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')
+ @sched.scheduled_job('cron', {'day': 'last sun'})
def some_decorated_task():
print "I am printed at 00:00:00 on the last Sunday of every month!"
diff --git a/docs/dateschedule.rst b/docs/dateschedule.rst
index cb01ba8..494b1a6 100644
--- a/docs/dateschedule.rst
+++ b/docs/dateschedule.rst
@@ -22,18 +22,18 @@ This is the in-process equivalent to the UNIX "at" command.
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'])
+ job = sched.add_job(my_job, 'simple', [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'])
+ job = sched.add_job(my_job, 'simple', [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'])
+ job = sched.add_job(my_job, 'simple', ['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'])
+ job = sched.add_job(my_job, 'simple', ['2009-11-06 16:30:05.720400'], ['text'])
diff --git a/docs/index.rst b/docs/index.rst
index 430a40e..e8c09b3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,9 +23,9 @@ provides features not present in Quartz (such as multiple job stores).
Features
--------
-* No (hard) external dependencies
+* No (hard) external dependencies, except for setuptools/distribute
* Thread-safe API
-* Excellent test coverage (tested on CPython 2.5 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 1.9)
+* Excellent test coverage (tested on CPython 2.6 - 2.7, 3.2 - 3.3, Jython 2.5.3, PyPy 1.9)
* Configurable scheduling mechanisms (triggers):
* Cron-like scheduling
@@ -85,7 +85,7 @@ Scheduler instance available from the very beginning::
sched = Scheduler()
- @sched.interval_schedule(hours=3)
+ @sched.scheduled_job('interval', {'hours': 3})
def some_job():
print "Decorated job"
diff --git a/docs/intervalschedule.rst b/docs/intervalschedule.rst
index 328d315..1bf1b46 100644
--- a/docs/intervalschedule.rst
+++ b/docs/intervalschedule.rst
@@ -22,21 +22,15 @@ that.
print "Hello World"
# Schedule job_function to be called every two hours
- sched.add_interval_job(job_function, hours=2)
+ sched.add_job(job_function, 'interval', {'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')
+ sched.add_job(job_function, 'interval', {'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
@@ -46,7 +40,7 @@ decorator can be attached to any function, and has the same syntax as
sched.start()
# Schedule job_function to be called every two hours
- @sched.interval_schedule(hours=2)
+ @sched.scheduled_job('interval', {'hours': 2})
def job_function():
print "Hello World"
diff --git a/docs/migration.rst b/docs/migration.rst
index 11028dd..a338373 100644
--- a/docs/migration.rst
+++ b/docs/migration.rst
@@ -1,3 +1,25 @@
+Migrating from APScheduler v2.x to 3.0
+======================================
+
+The 3.0 series is API incompatible with previous releases due to a design
+overhaul.
+
+API changes
+-----------
+
+* The trigger-specific scheduling methods have been removed entirely from the
+ Scheduler class. Instead, you have to use the
+ :meth:`~apscheduler.scheduler.Scheduler.add_job` method or the
+ :meth:`~apscheduler.scheduler.Scheduler.scheduled_job` decorator, giving the
+ entry point name of the trigger, or an already constructed instance.
+ It should also be noted that the signature of
+ :meth:`~apscheduler.scheduler.Scheduler.add_job` has changed due to this.
+* Adding a job before the scheduler has been started can now fail if no next
+ execution time can be calculated for it. Previously it would only fail when
+ the scheduler was started.
+* The "simple" trigger has been renamed to "date"
+
+
Migrating from APScheduler v1.x to 2.0
======================================
diff --git a/examples/interval.py b/examples/interval.py
index cb7c4f4..5469fef 100644
--- a/examples/interval.py
+++ b/examples/interval.py
@@ -13,7 +13,7 @@ def tick():
if __name__ == '__main__':
scheduler = Scheduler(standalone=True)
- scheduler.add_interval_job(tick, seconds=3)
+ scheduler.add_job(tick, 'interval', {'seconds': 3})
print('Press Ctrl+C to exit')
try:
scheduler.start()
diff --git a/examples/persistent.py b/examples/persistent.py
index 51c2571..b75b778 100644
--- a/examples/persistent.py
+++ b/examples/persistent.py
@@ -18,8 +18,7 @@ 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()])
+ scheduler.add_job(alarm, 'simple', [alarm_time], jobstore='shelve', args=[datetime.now()])
print('To clear the alarms, delete the example.db file.')
print('Press Ctrl+C to exit')
try:
diff --git a/examples/reference.py b/examples/reference.py
index 8be45e9..dc9756c 100644
--- a/examples/reference.py
+++ b/examples/reference.py
@@ -7,7 +7,7 @@ from apscheduler.scheduler import Scheduler
if __name__ == '__main__':
scheduler = Scheduler(standalone=True)
- scheduler.add_interval_job('sys:stdout.write', args=['tick\n'], seconds=3)
+ scheduler.add_job('sys:stdout.write', 'interval', {'seconds': 3}, args=['tick\n'])
print('Press Ctrl+C to exit')
try:
scheduler.start()
diff --git a/examples/threaded.py b/examples/threaded.py
index 2c612cd..7a11937 100644
--- a/examples/threaded.py
+++ b/examples/threaded.py
@@ -14,7 +14,7 @@ def tick():
if __name__ == '__main__':
scheduler = Scheduler()
- scheduler.add_interval_job(tick, seconds=3)
+ scheduler.add_job(tick, 'interval', {'seconds': 3})
print('Press Ctrl+C to exit')
scheduler.start()
diff --git a/setup.py b/setup.py
index 924dd73..8e702c9 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,7 @@
# 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 = {}
+from setuptools import setup, find_packages
import apscheduler
@@ -37,5 +31,15 @@ setup(
],
keywords='scheduling cron',
license='MIT',
- packages=('apscheduler', 'apscheduler.jobstores', 'apscheduler.triggers', 'apscheduler.triggers.cron'),
+ packages=find_packages(),
+ test_suite='nose.collector',
+ tests_require=['nose'],
+ zip_safe=False,
+ entry_points={
+ 'apscheduler.triggers': [
+ 'date = apscheduler.triggers.date:DateTrigger',
+ 'interval = apscheduler.triggers.interval:IntervalTrigger',
+ 'cron = apscheduler.triggers.cron:CronTrigger'
+ ]
+ }
)
diff --git a/tests/testintegration.py b/tests/testintegration.py
index c6f0be8..7dfafa1 100644
--- a/tests/testintegration.py
+++ b/tests/testintegration.py
@@ -47,7 +47,7 @@ class IntegrationTestBase(object):
# running when the next appointed time hits.
vals = [0]
- self.scheduler.add_interval_job(increment, jobstore='persistent', seconds=1, args=[vals, 2])
+ self.scheduler.add_job(increment, 'interval', {'seconds': 1}, args=[vals, 2], jobstore='persistent')
sleep(2.5)
eq_(vals, [1])
@@ -55,8 +55,8 @@ class IntegrationTestBase(object):
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])
+ self.scheduler.add_job(increment, 'interval', {'seconds': 0.3}, max_instances=2, max_runs=4, args=[vals, 1],
+ jobstore='persistent')
sleep(2.4)
eq_(vals, [2])
eq_(len(events), 4)
diff --git a/tests/testjob.py b/tests/testjob.py
index 726ceea..f11d2fb 100644
--- a/tests/testjob.py
+++ b/tests/testjob.py
@@ -1,10 +1,10 @@
from datetime import datetime, timedelta
from threading import Lock
-from nose.tools import eq_, raises, assert_raises # @UnresolvedImport
+from nose.tools import eq_, assert_raises # @UnresolvedImport
from apscheduler.job import Job, MaxInstancesReachedError
-from apscheduler.triggers.simple import SimpleTrigger
+from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
@@ -19,25 +19,23 @@ 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)
+ self.trigger = DateTrigger(self.RUNTIME)
+ self.job = Job(self.trigger, dummyfunc, [], {}, 1, False, None, None, 1)
def test_compute_next_run_time(self):
- self.job.compute_next_run_time(
- self.RUNTIME - timedelta(microseconds=1))
+ 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))
+ 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.trigger = IntervalTrigger(seconds=1, start_date=self.RUNTIME)
self.job.compute_next_run_time(expected_times[0])
eq_(self.job.next_run_time, expected_times[0])
@@ -70,7 +68,7 @@ class TestJob(object):
max_instances=1, runs=0))
def test_setstate(self):
- trigger = SimpleTrigger('2010-12-14 13:05:00')
+ trigger = DateTrigger('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,
@@ -88,7 +86,7 @@ class TestJob(object):
def test_jobs_equal(self):
assert self.job == self.job
- job2 = Job(SimpleTrigger(self.RUNTIME), lambda: None, [], {}, 1, False)
+ job2 = Job(DateTrigger(self.RUNTIME), lambda: None, [], {}, 1, False, None, None, 1)
assert self.job != job2
job2.id = self.job.id = 123
@@ -119,36 +117,6 @@ class TestJob(object):
def test_repr(self):
self.job.compute_next_run_time(self.RUNTIME)
eq_(repr(self.job),
- "<Job (name=dummyfunc, trigger=<SimpleTrigger (run_date=datetime.datetime(2010, 12, 13, 0, 8))>)>")
+ "<Job (name=dummyfunc, trigger=<DateTrigger (run_date=datetime.datetime(2010, 12, 13, 0, 8))>)>")
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_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)
diff --git a/tests/testjobstores.py b/tests/testjobstores.py
index d57ae11..551348c 100644
--- a/tests/testjobstores.py
+++ b/tests/testjobstores.py
@@ -8,7 +8,7 @@ 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.triggers.date import DateTrigger
from apscheduler.job import Job
try:
@@ -48,8 +48,8 @@ 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.trigger = DateTrigger(self.trigger_date)
+ self.job = Job(self.trigger, dummy_job, [], {}, 1, False, None, None, 1)
self.job.next_run_time = self.trigger_date
def test_jobstore_add_update_remove(self):
@@ -76,9 +76,9 @@ class JobStoreTestBase(object):
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)
+ job1 = Job(self.trigger, dummy_job, [], {}, 1, False, None, None, 1)
+ job2 = Job(self.trigger, dummy_job2, [], {}, 1, False, None, None, 1)
+ job3 = Job(self.trigger, dummy_job3, [], {}, 1, False, None, None, 1)
for job in job1, job2, job3:
job.next_run_time = self.trigger_date
self.jobstore.add_job(job)
diff --git a/tests/testscheduler.py b/tests/testscheduler.py
index a23dafb..dd5679a 100644
--- a/tests/testscheduler.py
+++ b/tests/testscheduler.py
@@ -32,12 +32,12 @@ class TestOfflineScheduler(object):
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')
+ job = self.scheduler.add_job(lambda: None, 'date', [datetime(2200, 7, 24)], jobstore='dummy')
assert isinstance(job, Job)
eq_(self.scheduler.get_jobs(), [])
def test_add_job_by_reference(self):
- job = self.scheduler.add_date_job('copy:copy', datetime(2200, 7, 24))
+ job = self.scheduler.add_job('copy:copy', 'date', [datetime(2200, 7, 24)])
eq_(job.func, copy)
eq_(job.func_ref, 'copy:copy')
@@ -82,7 +82,7 @@ class TestOfflineScheduler(object):
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))
+ self.scheduler.add_job(lambda: None, 'date', [datetime(9999, 9, 9)])
eq_(self.scheduler.get_jobs(), [])
self.scheduler.start()
@@ -138,7 +138,7 @@ class TestJobExecution(object):
def my_job():
pass
- job = self.scheduler.add_interval_job(my_job, start_date=datetime(2010, 5, 19))
+ job = self.scheduler.add_job(my_job, 'interval', {'start_date': datetime(2010, 5, 19)})
eq_(repr(job),
'<Job (name=my_job, trigger=<IntervalTrigger (interval=datetime.timedelta(0, 1), '
'start_date=datetime.datetime(2010, 5, 19, 0, 0))>)>')
@@ -153,7 +153,7 @@ class TestJobExecution(object):
self.val += 1
a = A()
- job = self.scheduler.add_interval_job(a, seconds=1)
+ job = self.scheduler.add_job(a, 'interval', {'seconds': 1})
self.scheduler._process_jobs(job.next_run_time)
self.scheduler._process_jobs(job.next_run_time)
eq_(a.val, 2)
@@ -168,7 +168,7 @@ class TestJobExecution(object):
self.val += 1
a = A()
- job = self.scheduler.add_interval_job(a.method, seconds=1)
+ job = self.scheduler.add_job(a.method, 'interval', {'seconds': 1})
self.scheduler._process_jobs(job.next_run_time)
self.scheduler._process_jobs(job.next_run_time)
eq_(a.val, 2)
@@ -178,7 +178,7 @@ class TestJobExecution(object):
vals[0] += 1
vals = [0]
- job = self.scheduler.add_cron_job(increment)
+ job = self.scheduler.add_job(increment, 'cron')
self.scheduler._process_jobs(job.next_run_time)
eq_(vals[0], 1)
self.scheduler.unschedule_job(job)
@@ -193,9 +193,9 @@ class TestJobExecution(object):
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)
+ job1 = self.scheduler.add_job(increment, 'cron')
+ job2 = self.scheduler.add_job(increment2, 'cron')
+ job3 = self.scheduler.add_job(increment, 'cron')
eq_(self.scheduler.get_jobs(), [job1, job2, job3])
self.scheduler.unschedule_func(increment)
@@ -210,7 +210,7 @@ class TestJobExecution(object):
vals[0] += 1
vals = [0]
- job = self.scheduler.add_interval_job(increment, max_runs=1)
+ job = self.scheduler.add_job(increment, 'interval', max_runs=1)
self.scheduler._process_jobs(job.next_run_time)
eq_(vals, [1])
assert job not in self.scheduler.get_jobs()
@@ -219,16 +219,16 @@ class TestJobExecution(object):
def failure():
raise DummyException
- job = self.scheduler.add_date_job(failure, datetime(9999, 9, 9))
+ job = self.scheduler.add_job(failure, 'date', [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)
+ job = self.scheduler.add_job(lambda: None, 'interval', {'seconds': 1})
eq_(job.misfire_grace_time, 3)
- job = self.scheduler.add_interval_job(lambda: None, seconds=1, misfire_grace_time=2)
+ job = self.scheduler.add_job(lambda: None, 'interval', {'seconds': 1}, misfire_grace_time=2)
eq_(job.misfire_grace_time, 2)
def test_coalesce_on(self):
@@ -241,8 +241,8 @@ class TestJobExecution(object):
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)
+ job = self.scheduler.add_job(increment, 'interval', {'seconds': 1, 'start_date': FakeDateTime.now()},
+ coalesce=True, misfire_grace_time=2)
# Turn the clock 14 seconds forward
FakeDateTime._now += timedelta(seconds=2)
@@ -263,8 +263,8 @@ class TestJobExecution(object):
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)
+ job = self.scheduler.add_job(increment, 'interval', {'seconds': 1, 'start_date': FakeDateTime.now()},
+ coalesce=False, misfire_grace_time=2)
# Turn the clock 2 seconds forward
FakeDateTime._now += timedelta(seconds=2)
@@ -283,13 +283,13 @@ class TestJobExecution(object):
vals[1] += 1
vals = [0, 0]
- job = self.scheduler.add_interval_job(increment, seconds=1, args=[2])
+ job = self.scheduler.add_job(increment, 'interval', {'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)
+ @self.scheduler.scheduled_job('interval', {'seconds': 1})
def increment():
vals[0] += 1
@@ -305,7 +305,7 @@ class TestJobExecution(object):
vals[1] += 1
vals = [0, 0]
- job = self.scheduler.add_cron_job(increment, args=[3])
+ job = self.scheduler.add_job(increment, 'cron', args=[3])
start = job.next_run_time
self.scheduler._process_jobs(start)
eq_(vals, [3, 1])
@@ -315,7 +315,7 @@ class TestJobExecution(object):
eq_(vals, [9, 3])
def test_cron_schedule_1(self):
- @self.scheduler.cron_schedule()
+ @self.scheduler.scheduled_job('cron')
def increment():
vals[0] += 1
@@ -326,7 +326,7 @@ class TestJobExecution(object):
eq_(vals[0], 2)
def test_cron_schedule_2(self):
- @self.scheduler.cron_schedule(minute='*')
+ @self.scheduler.scheduled_job('cron', {'minute': '*'})
def increment():
vals[0] += 1
@@ -344,7 +344,7 @@ class TestJobExecution(object):
vals = []
date = datetime.now() + timedelta(seconds=1)
- self.scheduler.add_date_job(append_val, date, kwargs={'value': 'test'})
+ self.scheduler.add_job(append_val, 'date', [date], kwargs={'value': 'test'})
self.scheduler._process_jobs(date)
eq_(vals, ['test'])
@@ -355,7 +355,7 @@ class TestJobExecution(object):
' No scheduled jobs%s' % (os.linesep, os.linesep)
eq_(out.getvalue(), expected)
- self.scheduler.add_date_job(copy, datetime(2200, 5, 19))
+ self.scheduler.add_job(copy, 'date', [datetime(2200, 5, 19)])
out = StringIO()
self.scheduler.print_jobs(out)
expected = 'Jobstore default:%s '\
@@ -365,7 +365,7 @@ class TestJobExecution(object):
def test_jobstore(self):
self.scheduler.add_jobstore(RAMJobStore(), 'dummy')
- job = self.scheduler.add_date_job(lambda: None, datetime(2200, 7, 24), jobstore='dummy')
+ job = self.scheduler.add_job(lambda: None, 'date', [datetime(2200, 7, 24)], jobstore='dummy')
eq_(self.scheduler.get_jobs(), [job])
self.scheduler.remove_jobstore('dummy')
eq_(self.scheduler.get_jobs(), [])
@@ -381,9 +381,8 @@ class TestJobExecution(object):
vars = [0]
scheduler.datetime = FakeDateTime
- job = self.scheduler.add_interval_job(
- increment, seconds=1, misfire_grace_time=3,
- start_date=FakeDateTime.now())
+ job = self.scheduler.add_job(increment, 'interval', {'seconds': 1, 'start_date': FakeDateTime.now()},
+ misfire_grace_time=3)
start = job.next_run_time
self.scheduler._process_jobs(start)
diff --git a/tests/testtriggers.py b/tests/testtriggers.py
index 86cc64f..875d714 100644
--- a/tests/testtriggers.py
+++ b/tests/testtriggers.py
@@ -1,8 +1,10 @@
-from datetime import datetime, timedelta
+from datetime import datetime
from nose.tools import eq_, raises
-from apscheduler.triggers import CronTrigger, SimpleTrigger, IntervalTrigger
+from apscheduler.triggers.cron import CronTrigger
+from apscheduler.triggers.date import DateTrigger
+from apscheduler.triggers.interval import IntervalTrigger
def test_cron_trigger_1():
@@ -140,8 +142,8 @@ def test_cron_bad_kwarg():
def test_date_trigger_earlier():
fire_date = datetime(2009, 7, 6)
- trigger = SimpleTrigger(fire_date)
- eq_(repr(trigger), "<SimpleTrigger (run_date=datetime.datetime(2009, 7, 6, 0, 0))>")
+ trigger = DateTrigger(fire_date)
+ eq_(repr(trigger), "<DateTrigger (run_date=datetime.datetime(2009, 7, 6, 0, 0))>")
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)
@@ -149,20 +151,20 @@ def test_date_trigger_earlier():
def test_date_trigger_exact():
fire_date = datetime(2009, 7, 6)
- trigger = SimpleTrigger(fire_date)
+ trigger = DateTrigger(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)
+ trigger = DateTrigger(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')
+ trigger = DateTrigger('2009-7-6')
start_date = datetime(2009, 7, 6)
eq_(trigger.get_next_fire_time(start_date), datetime(2009, 7, 6))
@@ -174,9 +176,7 @@ def test_interval_invalid_interval():
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)
+ self.trigger = IntervalTrigger(seconds=1, start_date=datetime(2009, 8, 4, second=2))
def test_interval_repr(self):
eq_(repr(self.trigger),