summaryrefslogtreecommitdiff
path: root/apscheduler
diff options
context:
space:
mode:
authorAlex Grönholm <alex.gronholm@nextday.fi>2013-07-27 18:07:45 +0300
committerAlex Grönholm <alex.gronholm@nextday.fi>2013-07-27 18:07:45 +0300
commit307e0f13220a465de8c1764e63cd4092272d95b4 (patch)
tree66d9ac3cc841d7c33dc67618c1b4295b2a9d6fe7 /apscheduler
parenta1629b01f91ce0cbbfc0cbeeb283ca4011756c25 (diff)
downloadapscheduler-307e0f13220a465de8c1764e63cd4092272d95b4.tar.gz
Added timezone awareness
Diffstat (limited to 'apscheduler')
-rw-r--r--apscheduler/job.py4
-rw-r--r--apscheduler/scheduler.py21
-rw-r--r--apscheduler/triggers/cron/__init__.py19
-rw-r--r--apscheduler/triggers/date.py16
-rw-r--r--apscheduler/triggers/interval.py16
-rw-r--r--apscheduler/util.py34
6 files changed, 71 insertions, 39 deletions
diff --git a/apscheduler/job.py b/apscheduler/job.py
index a025983..b5d1130 100644
--- a/apscheduler/job.py
+++ b/apscheduler/job.py
@@ -5,7 +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, datetime_repr
class MaxInstancesReachedError(Exception):
@@ -102,4 +102,4 @@ class Job(object):
return '<Job (name=%s, trigger=%r)>' % (self.name, self.trigger)
def __str__(self):
- return '%s (trigger: %s, next run at: %s)' % (self.name, self.trigger, self.next_run_time)
+ return '%s (trigger: %s, next run at: %s)' % (self.name, self.trigger, datetime_repr(self.next_run_time))
diff --git a/apscheduler/scheduler.py b/apscheduler/scheduler.py
index ffa19ef..a21c7aa 100644
--- a/apscheduler/scheduler.py
+++ b/apscheduler/scheduler.py
@@ -10,6 +10,8 @@ import os
import sys
from pkg_resources import iter_entry_points
+from dateutil.tz import gettz, tzlocal
+from six import string_types, u
from apscheduler.util import *
from apscheduler.jobstores.memory import MemoryJobStore
@@ -61,6 +63,11 @@ class Scheduler(object):
self.coalesce = asbool(config.pop('coalesce', True))
self.daemonic = asbool(config.pop('daemonic', True))
self.standalone = asbool(config.pop('standalone', False))
+ timezone = config.pop('timezone', None)
+ self.timezone = gettz(timezone) if isinstance(timezone, string_types) else timezone or tzlocal()
+
+ # Set trigger defaults
+ self.trigger_defaults = {'timezone': self.timezone}
# Configure the thread pool
if 'threadpool' in config:
@@ -220,7 +227,7 @@ class Scheduler(object):
def _real_add_job(self, job, jobstore, wakeup):
# Recalculate the next run time
- job.compute_next_run_time(datetime.now())
+ job.compute_next_run_time(datetime.now(self.timezone))
# Add the job to the given job store
store = self._jobstores.get(jobstore)
@@ -291,9 +298,9 @@ class Scheduler(object):
raise KeyError('No trigger by the name "%s" was found' % trigger)
if isinstance(trigger_args, Mapping):
- trigger = trigger_cls(**trigger_args)
+ trigger = trigger_cls(self.trigger_defaults, **trigger_args)
elif isinstance(trigger_args, Iterable):
- trigger = trigger_cls(*trigger_args)
+ trigger = trigger_cls(self.trigger_defaults, *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')):
@@ -310,7 +317,7 @@ class Scheduler(object):
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:
+ if job.compute_next_run_time(datetime.now(self.timezone)) 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
@@ -390,7 +397,7 @@ class Scheduler(object):
job_strs = []
with self._jobstores_lock:
for alias, jobstore in iteritems(self._jobstores):
- job_strs.append('Jobstore %s:' % alias)
+ job_strs.append(u('Jobstore %s:') % alias)
if jobstore.jobs:
for job in jobstore.jobs:
job_strs.append(' %s' % job)
@@ -406,7 +413,7 @@ class Scheduler(object):
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
+ difference = datetime.now(self.timezone) - run_time
grace_time = timedelta(seconds=job.misfire_grace_time)
if difference > grace_time:
# Notify listeners about a missed run
@@ -486,7 +493,7 @@ class Scheduler(object):
self._wakeup.clear()
while not self._stopped:
logger.debug('Looking for jobs to run')
- now = datetime.now()
+ now = datetime.now(self.timezone)
next_wakeup_time = self._process_jobs(now)
# Sleep until the next job is scheduled to be run,
diff --git a/apscheduler/triggers/cron/__init__.py b/apscheduler/triggers/cron/__init__.py
index 2c4559e..21db0c2 100644
--- a/apscheduler/triggers/cron/__init__.py
+++ b/apscheduler/triggers/cron/__init__.py
@@ -1,7 +1,7 @@
from datetime import date, datetime
from apscheduler.triggers.cron.fields import *
-from apscheduler.util import datetime_ceil, convert_to_datetime, iteritems
+from apscheduler.util import datetime_ceil, convert_to_datetime, iteritems, datetime_repr
class CronTrigger(object):
@@ -17,11 +17,11 @@ class CronTrigger(object):
'second': BaseField
}
- def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None,
- second=None, start_date=None):
+ def __init__(self, defaults, year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None,
+ second=None, start_date=None, timezone=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
@@ -30,8 +30,11 @@ class CronTrigger(object):
:param hour: hour to run on
:param second: second to run on
:param start_date: earliest possible date/time to trigger on
+ :param timezone: time zone for ``start_date``
+ :type timezone: str or an instance of a :cls:`~datetime.tzinfo` subclass
"""
- self.start_date = convert_to_datetime(start_date) if start_date else None
+ self.timezone = timezone or defaults['timezone']
+ self.start_date = convert_to_datetime(start_date, self.timezone, '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)
@@ -91,7 +94,7 @@ class CronTrigger(object):
values[field.name] = value + 1
i += 1
- return datetime(**values), fieldnum
+ return datetime(tzinfo=self.timezone, **values), fieldnum
def _set_field_value(self, dateval, fieldnum, new_value):
values = {}
@@ -104,7 +107,7 @@ class CronTrigger(object):
else:
values[field.name] = new_value
- return datetime(**values)
+ return datetime(tzinfo=self.timezone, **values)
def get_next_fire_time(self, start_date):
if self.start_date:
@@ -140,5 +143,5 @@ class CronTrigger(object):
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(' '))
+ options.append("start_date='%s'" % datetime_repr(self.start_date))
return '<%s (%s)>' % (self.__class__.__name__, ', '.join(options))
diff --git a/apscheduler/triggers/date.py b/apscheduler/triggers/date.py
index 33a30f9..df5b72b 100644
--- a/apscheduler/triggers/date.py
+++ b/apscheduler/triggers/date.py
@@ -1,22 +1,24 @@
-from apscheduler.util import convert_to_datetime
+from apscheduler.util import convert_to_datetime, datetime_repr
class DateTrigger(object):
- def __init__(self, run_date):
+ def __init__(self, defaults, run_date, timezone=None):
"""
Triggers once on the given datetime.
-
+
:param run_date: the date/time to run the job at
+ :param timezone: time zone for ``run_date``
+ :type timezone: str or an instance of a :cls:`~datetime.tzinfo` subclass
"""
- self.run_date = convert_to_datetime(run_date)
+ timezone = timezone or defaults['timezone']
+ self.run_date = convert_to_datetime(run_date, timezone, '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)
+ return 'date[%s]' % datetime_repr(self.run_date)
def __repr__(self):
- return '<%s (run_date=%s)>' % (
- self.__class__.__name__, repr(self.run_date))
+ return "<%s (run_date='%s')>" % (self.__class__.__name__, datetime_repr(self.run_date))
diff --git a/apscheduler/triggers/interval.py b/apscheduler/triggers/interval.py
index 5c3206a..03a9b8d 100644
--- a/apscheduler/triggers/interval.py
+++ b/apscheduler/triggers/interval.py
@@ -1,11 +1,11 @@
from datetime import datetime, timedelta
from math import ceil
-from apscheduler.util import convert_to_datetime, timedelta_seconds
+from apscheduler.util import convert_to_datetime, timedelta_seconds, datetime_repr
class IntervalTrigger(object):
- def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None):
+ def __init__(self, defaults, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None, timezone=None):
"""
Triggers on specified intervals.
@@ -15,7 +15,10 @@ class IntervalTrigger(object):
: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 timezone: time zone for ``start_date``
+ :type timezone: str or an instance of a :cls:`~datetime.tzinfo` subclass
"""
+ timezone = timezone or defaults['timezone']
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:
@@ -23,9 +26,9 @@ class IntervalTrigger(object):
self.interval_length = 1
if start_date is None:
- self.start_date = datetime.now() + self.interval
+ self.start_date = datetime.now(timezone) + self.interval
else:
- self.start_date = convert_to_datetime(start_date)
+ self.start_date = convert_to_datetime(start_date, timezone, 'start_date')
def get_next_fire_time(self, start_date):
if start_date < self.start_date:
@@ -39,6 +42,5 @@ class IntervalTrigger(object):
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))
+ return "<%s (interval=%r, start_date='%s')>" % (self.__class__.__name__, self.interval,
+ datetime_repr(self.start_date))
diff --git a/apscheduler/util.py b/apscheduler/util.py
index 6e42ab8..1e86ed0 100644
--- a/apscheduler/util.py
+++ b/apscheduler/util.py
@@ -7,6 +7,9 @@ from time import mktime
import re
import sys
+from dateutil.tz import gettz
+from six import string_types
+
__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')
@@ -45,11 +48,12 @@ _DATE_REGEX = re.compile(
r'(?:\.(?P<microsecond>\d{1,6}))?)?')
-def convert_to_datetime(input):
+def convert_to_datetime(input, timezone, arg_name):
"""
- 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.
+ Converts the given object to a timezone aware datetime object.
+ If a timezone aware datetime object is passed, it is returned unmodified.
+ If a native datetime object is passed, it is given the specified timezone.
+ If the input is a string, it is parsed as a datetime with the given timezone.
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
@@ -58,17 +62,27 @@ def convert_to_datetime(input):
:rtype: datetime
"""
if isinstance(input, datetime):
- return input
+ datetime_ = input
elif isinstance(input, date):
- return datetime.fromordinal(input.toordinal())
+ datetime_ = 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))
+ datetime_ = datetime(**values)
+ else:
+ raise TypeError('Unsupported input type: %s' % type(input))
+
+ if datetime_.tzinfo is not None:
+ return datetime_
+ if timezone is None:
+ raise ValueError('The "timezone" argument must be specified if %s has no timezone information' % arg_name)
+ if isinstance(timezone, string_types):
+ timezone = gettz(timezone)
+
+ return datetime_.replace(tzinfo=timezone)
def timedelta_seconds(delta):
@@ -110,6 +124,10 @@ def datetime_ceil(dateval):
return dateval
+def datetime_repr(dateval):
+ return dateval.strftime('%Y-%m-%d %H:%M:%S %Z') if dateval else 'None'
+
+
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,