From cc519b72476f0bd03fd5087dc8be1b26c2cc3ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Tue, 16 Jan 2018 00:33:36 +0200 Subject: Added support for UTC offsets in datetime parsing Fixes #271. --- apscheduler/util.py | 24 ++++++++++++++++++------ docs/versionhistory.rst | 3 +++ tests/test_util.py | 8 +++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apscheduler/util.py b/apscheduler/util.py index d9beb18..88254ff 100644 --- a/apscheduler/util.py +++ b/apscheduler/util.py @@ -7,7 +7,7 @@ from calendar import timegm from functools import partial import re -from pytz import timezone, utc +from pytz import timezone, utc, FixedOffset import six try: @@ -94,8 +94,9 @@ def astimezone(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}))?)?') + r'(?:[ T](?P\d{1,2}):(?P\d{1,2}):(?P\d{1,2})' + r'(?:\.(?P\d{1,6}))?' + r'(?PZ|[+-]\d\d:\d\d)?)?$') def convert_to_datetime(input, tz, arg_name): @@ -107,7 +108,9 @@ def convert_to_datetime(input, tz, arg_name): 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 (Y-m-d H:M:S.micro). + (Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). Additionally you can + override the time zone by giving a specific offset in the format specified by ISO 8601: + Z (UTC), +HH:MM or -HH:MM. :param str|datetime input: the datetime or string to convert to a timezone aware datetime :param datetime.tzinfo tz: timezone to interpret ``input`` in @@ -125,8 +128,17 @@ def convert_to_datetime(input, tz, arg_name): 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) + + values = m.groupdict() + tzname = values.pop('timezone') + if tzname == 'Z': + tz = utc + elif tzname: + hours, minutes = (int(x) for x in tzname[1:].split(':')) + sign = 1 if tzname[0] == '+' else -1 + tz = FixedOffset(sign * (hours * 60 + minutes)) + + values = {k: int(v or 0) for k, v in values.items()} datetime_ = datetime(**values) else: raise TypeError('Unsupported type for %s: %s' % (arg_name, input.__class__.__name__)) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index d5a22d0..8911030 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -12,6 +12,9 @@ APScheduler, see the :doc:`migration section `. * Fixed ``CronTrigger`` sometimes producing fire times beyond ``end_date`` when jitter is enabled (thanks to gilbsgilbs for the tests) +* Fixed ISO 8601 UTC offset information being silently discarded from string formatted datetimes by + adding support for parsing them + 3.5.0 ----- diff --git a/tests/test_util.py b/tests/test_util.py index 55bf197..973b81a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -107,9 +107,15 @@ class TestConvertToDatetime(object): (datetime(2009, 8, 1, 5, 6, 12), datetime(2009, 8, 1, 5, 6, 12)), ('2009-8-1', datetime(2009, 8, 1)), ('2009-8-1 5:16:12', datetime(2009, 8, 1, 5, 16, 12)), + ('2009-8-1T5:16:12Z', datetime(2009, 8, 1, 5, 16, 12, tzinfo=pytz.utc)), + ('2009-8-1T5:16:12+02:30', + pytz.FixedOffset(150).localize(datetime(2009, 8, 1, 5, 16, 12))), + ('2009-8-1T5:16:12-05:30', + pytz.FixedOffset(-330).localize(datetime(2009, 8, 1, 5, 16, 12))), (pytz.FixedOffset(-60).localize(datetime(2009, 8, 1)), pytz.FixedOffset(-60).localize(datetime(2009, 8, 1))) - ], ids=['None', 'date', 'datetime', 'date as text', 'datetime as text', 'existing tzinfo']) + ], ids=['None', 'date', 'datetime', 'date as text', 'datetime as text', 'utc', 'tzoffset', + 'negtzoffset', 'existing tzinfo']) def test_date(self, timezone, input, expected): returned = convert_to_datetime(input, timezone, None) if expected is not None: -- cgit v1.2.1