diff options
author | Alex Grönholm <alex.gronholm@nextday.fi> | 2013-07-27 18:07:45 +0300 |
---|---|---|
committer | Alex Grönholm <alex.gronholm@nextday.fi> | 2013-07-27 18:07:45 +0300 |
commit | 307e0f13220a465de8c1764e63cd4092272d95b4 (patch) | |
tree | 66d9ac3cc841d7c33dc67618c1b4295b2a9d6fe7 /apscheduler | |
parent | a1629b01f91ce0cbbfc0cbeeb283ca4011756c25 (diff) | |
download | apscheduler-307e0f13220a465de8c1764e63cd4092272d95b4.tar.gz |
Added timezone awareness
Diffstat (limited to 'apscheduler')
-rw-r--r-- | apscheduler/job.py | 4 | ||||
-rw-r--r-- | apscheduler/scheduler.py | 21 | ||||
-rw-r--r-- | apscheduler/triggers/cron/__init__.py | 19 | ||||
-rw-r--r-- | apscheduler/triggers/date.py | 16 | ||||
-rw-r--r-- | apscheduler/triggers/interval.py | 16 | ||||
-rw-r--r-- | apscheduler/util.py | 34 |
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, |