Hello community,
here is the log from the commit of package python-croniter for openSUSE:Factory checked in at 2018-10-18 15:39:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-croniter (Old)
and /work/SRC/openSUSE:Factory/.python-croniter.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-croniter"
Thu Oct 18 15:39:05 2018 rev:7 rq:642790 version:0.3.20
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-croniter/python-croniter.changes 2017-08-28 15:17:19.223417956 +0200
+++ /work/SRC/openSUSE:Factory/.python-croniter.new/python-croniter.changes 2018-10-18 15:39:17.726095245 +0200
@@ -1,0 +2,30 @@
+Wed Oct 17 18:29:01 UTC 2018 - Jan Engelhardt
+
+- Avoid name repetition in summary.
+
+-------------------------------------------------------------------
+Wed Oct 17 13:29:54 UTC 2018 - sjamgade@suse.com
+
+- update to 0.3.20
+ - (tag: 0.3.20) Preparing release 0.3.20
+ - pep8
+ - Fix sao paulo timezone test.
+ - remove outdated comment
+ - correctly handle DST changes
+ - Merge pull request #89 from kiorky/master
+ - Back to development: 0.3.20
+ - (tag: 0.3.19) Preparing release 0.3.19
+ - fix #87: backward dst changes
+ - Merge pull request #88 from kiorky/master
+ - Back to development: 0.3.19
+ - (tag: 0.3.18) Preparing release 0.3.18
+ - Merge pull request #18 from taichino/master
+ - Merge pull request #86 from otherpirate/master
+ - Adding is_valid class method to readme
+ - Adding class method is_valid to validate cron syntax
+ - Creating base croniter error
+ - Merge pull request #85 from kiorky/master
+ - Back to development: 0.3.18
+
+
+-------------------------------------------------------------------
Old:
----
croniter-0.3.17.tar.gz
New:
----
croniter-0.3.20.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-croniter.spec ++++++
--- /var/tmp/diff_new_pack.XVCMZ9/_old 2018-10-18 15:39:18.698094140 +0200
+++ /var/tmp/diff_new_pack.XVCMZ9/_new 2018-10-18 15:39:18.702094135 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-croniter
#
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -12,26 +12,25 @@
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-croniter
-Version: 0.3.17
+Version: 0.3.20
Release: 0
-Summary: Croniter provides iteration for datetime object with cron like format
+Summary: Python iterators for datetime objects with cron-like format
License: MIT
Group: Development/Languages/Python
Url: http://github.com/kiorky/croniter
Source: https://files.pythonhosted.org/packages/source/c/croniter/croniter-%{version}.tar.gz
-BuildRequires: %{python_module devel}
BuildRequires: %{python_module setuptools}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
BuildRequires: unzip
# Test requirements:
-BuildRequires: %{python_module nose}
+BuildRequires: %{python_module pytest}
BuildRequires: %{python_module python-dateutil}
BuildRequires: %{python_module pytz}
Requires: python-python-dateutil
@@ -40,7 +39,7 @@
%python_subpackages
%description
-croniter provides iteration for datetime object with cron like format.
+croniter provides iterators for datetime object with cron-like format.
%prep
%setup -q -n croniter-%{version}
@@ -53,7 +52,7 @@
%python_expand %fdupes %{buildroot}%{$python_sitelib}
%check
-%python_exec %{_bindir}/nosetests
+%python_exec %{_bindir}/py.test src
%files %{python_files}
%defattr(-,root,root,-)
++++++ croniter-0.3.17.tar.gz -> croniter-0.3.20.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/PKG-INFO new/croniter-0.3.20/PKG-INFO
--- old/croniter-0.3.17/PKG-INFO 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/PKG-INFO 2017-11-06 22:22:31.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: croniter
-Version: 0.3.17
+Version: 0.3.20
Summary: croniter provides iteration for datetime object with cron like format
Home-page: http://github.com/kiorky/croniter
Author: Matsumoto Taichi, kiorky
@@ -90,6 +90,11 @@
>>> print itr.get_prev(datetime) # 2010-07-01 00:00:00
>>> print itr.get_prev(datetime) # 2010-06-01 00:00:00
+ You can validate your crons using ``is_valid`` class method. (>= 0.3.18)::
+
+ >>> croniter.is_valid('0 0 1 * *') # True
+ >>> croniter.is_valid('0 wrong_value 1 * *') # False
+
About DST
=========
Be sure to init your croniter instance with a TZ aware datetime for this to work !::
@@ -144,6 +149,27 @@
Changelog
==============
+ 0.3.20 (2017-11-06)
+ -------------------
+
+ - More DST fixes
+ [Kevin Rose ]
+
+
+ 0.3.19 (2017-08-31)
+ -------------------
+
+ - fix #87: backward dst changes
+ [kiorky]
+
+
+ 0.3.18 (2017-08-31)
+ -------------------
+
+ - Add is valid method, refactor errors
+ [otherpirate, Mauro Murari ]
+
+
0.3.17 (2017-05-22)
-------------------
- DOW occurence sharp style support.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/README.rst new/croniter-0.3.20/README.rst
--- old/croniter-0.3.17/README.rst 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/README.rst 2017-11-06 22:22:31.000000000 +0100
@@ -82,6 +82,11 @@
>>> print itr.get_prev(datetime) # 2010-07-01 00:00:00
>>> print itr.get_prev(datetime) # 2010-06-01 00:00:00
+You can validate your crons using ``is_valid`` class method. (>= 0.3.18)::
+
+ >>> croniter.is_valid('0 0 1 * *') # True
+ >>> croniter.is_valid('0 wrong_value 1 * *') # False
+
About DST
=========
Be sure to init your croniter instance with a TZ aware datetime for this to work !::
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/docs/CHANGES.rst new/croniter-0.3.20/docs/CHANGES.rst
--- old/croniter-0.3.17/docs/CHANGES.rst 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/docs/CHANGES.rst 2017-11-06 22:22:31.000000000 +0100
@@ -1,6 +1,27 @@
Changelog
==============
+0.3.20 (2017-11-06)
+-------------------
+
+- More DST fixes
+ [Kevin Rose ]
+
+
+0.3.19 (2017-08-31)
+-------------------
+
+- fix #87: backward dst changes
+ [kiorky]
+
+
+0.3.18 (2017-08-31)
+-------------------
+
+- Add is valid method, refactor errors
+ [otherpirate, Mauro Murari ]
+
+
0.3.17 (2017-05-22)
-------------------
- DOW occurence sharp style support.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/setup.py new/croniter-0.3.20/setup.py
--- old/croniter-0.3.17/setup.py 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/setup.py 2017-11-06 22:22:31.000000000 +0100
@@ -23,7 +23,7 @@
setup(
name='croniter',
- version='0.3.17',
+ version='0.3.20',
py_modules=['croniter', ],
description=(
'croniter provides iteration for datetime '
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/src/croniter/croniter.py new/croniter-0.3.20/src/croniter/croniter.py
--- old/croniter-0.3.17/src/croniter/croniter.py 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/src/croniter/croniter.py 2017-11-06 22:22:31.000000000 +0100
@@ -14,18 +14,23 @@
only_int_re = re.compile(r'^\d+$')
any_int_re = re.compile(r'^\d+')
star_or_int_re = re.compile(r'^(\d+|\*)$')
+VALID_LEN_EXPRESSION = [5, 6]
-class CroniterBadCronError(ValueError):
- '''.'''
+class CroniterError(ValueError):
+ pass
-class CroniterBadDateError(ValueError):
- '''.'''
+class CroniterBadCronError(CroniterError):
+ pass
-class CroniterNotAlphaError(ValueError):
- '''.'''
+class CroniterBadDateError(CroniterError):
+ pass
+
+
+class CroniterNotAlphaError(CroniterError):
+ pass
class croniter(object):
@@ -78,107 +83,18 @@
start_time = self._datetime_to_timestamp(start_time)
self.start_time = start_time
+ self.dst_start_time = start_time
self.cur = start_time
- self.exprs = expr_format.split()
-
- if len(self.exprs) != 5 and len(self.exprs) != 6:
- raise CroniterBadCronError(self.bad_length)
-
- expanded = []
- nth_weekday_of_month = {}
-
- for i, expr in enumerate(self.exprs):
- e_list = expr.split(',')
- res = []
-
- while len(e_list) > 0:
- e = e_list.pop()
-
- if i == 4:
- e, sep, nth = str(e).partition('#')
- if nth and not re.match(r'[1-5]', nth):
- raise CroniterBadDateError(
- "[{0}] is not acceptable".format(expr_format))
-
- t = re.sub(r'^\*(\/.+)$', r'%d-%d\1' % (
- self.RANGES[i][0],
- self.RANGES[i][1]),
- str(e))
- m = search_re.search(t)
-
- if not m:
- t = re.sub(r'^(.+)\/(.+)$', r'\1-%d/\2' % (
- self.RANGES[i][1]),
- str(e))
- m = step_search_re.search(t)
-
- if m:
- (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
-
- if not any_int_re.search(low):
- low = "{0}".format(self._alphaconv(i, low))
-
- if not any_int_re.search(high):
- high = "{0}".format(self._alphaconv(i, high))
-
- if (
- not low or not high or int(low) > int(high)
- or not only_int_re.search(str(step))
- ):
- raise CroniterBadDateError(
- "[{0}] is not acceptable".format(expr_format))
-
- low, high, step = map(int, [low, high, step])
- rng = range(low, high + 1, step)
- e_list += (["{0}#{1}".format(item, nth) for item in rng]
- if i == 4 and nth else rng)
- else:
- if t.startswith('-'):
- raise CroniterBadCronError(
- "[{0}] is not acceptable,\
- negative numbers not allowed".format(
- expr_format))
- if not star_or_int_re.search(t):
- t = self._alphaconv(i, t)
-
- try:
- t = int(t)
- except:
- pass
-
- if t in self.LOWMAP[i]:
- t = self.LOWMAP[i][t]
- if (
- t not in ["*", "l"]
- and (int(t) < self.RANGES[i][0] or
- int(t) > self.RANGES[i][1])
- ):
- raise CroniterBadCronError(
- "[{0}] is not acceptable, out of range".format(
- expr_format))
+ self.expanded, self.nth_weekday_of_month = self.expand(expr_format)
- res.append(t)
-
- if i == 4 and nth:
- if t not in nth_weekday_of_month:
- nth_weekday_of_month[t] = set()
- nth_weekday_of_month[t].add(int(nth))
-
- res.sort()
- expanded.append(['*'] if (len(res) == 1
- and res[0] == '*')
- else res)
-
- self.expanded = expanded
- self.nth_weekday_of_month = nth_weekday_of_month
-
- def _alphaconv(self, index, key):
+ @classmethod
+ def _alphaconv(cls, index, key, expressions):
try:
- return self.ALPHACONV[index][key.lower()]
+ return cls.ALPHACONV[index][key.lower()]
except KeyError:
raise CroniterNotAlphaError(
- "[{0}] is not acceptable".format(" ".join(self.exprs)))
+ "[{0}] is not acceptable".format(" ".join(expressions)))
def get_next(self, ret_type=None):
return self._get_next(ret_type or self._ret_type, is_prev=False)
@@ -192,14 +108,15 @@
return self._timestamp_to_datetime(self.cur)
return self.cur
- def _datetime_to_timestamp(self, d):
+ @classmethod
+ def _datetime_to_timestamp(cls, d):
"""
Converts a `datetime` object `d` into a UNIX timestamp.
"""
if d.tzinfo is not None:
d = d.replace(tzinfo=None) - d.utcoffset()
- return self._timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
+ return cls._timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
def _timestamp_to_datetime(self, timestamp):
"""
@@ -269,29 +186,34 @@
else:
result = t1 if t1 > t2 else t2
else:
- result = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
+ result = self._calc(self.cur, expanded,
+ nth_weekday_of_month, is_prev)
# DST Handling for cron job spanning accross days
- dtstarttime = self._timestamp_to_datetime(self.start_time)
- dtresult = self._timestamp_to_datetime(result)
- dtresult_utcoffset = dtresult.utcoffset() or datetime.timedelta(0)
+ dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
dtstarttime_utcoffset = (
dtstarttime.utcoffset() or datetime.timedelta(0))
- hours_before_midnight = 24 - dtstarttime.hour
- lag_hours = (
- self._timedelta_to_seconds(dtresult - dtstarttime) / (60*60)
- )
- if (
- lag_hours >= hours_before_midnight and
- (dtresult_utcoffset or dtstarttime_utcoffset) and
- (dtresult_utcoffset != dtstarttime_utcoffset)
- ):
+ dtresult = self._timestamp_to_datetime(result)
+ lag = lag_hours = 0
+ # do we trigger DST on next crontab (handle backward changes)
+ dtresult_utcoffset = dtstarttime_utcoffset
+ if dtresult and self.tzinfo:
+ dtresult_utcoffset = dtresult.utcoffset()
+ lag_hours = (
+ self._timedelta_to_seconds(dtresult - dtstarttime) / (60*60)
+ )
lag = self._timedelta_to_seconds(
dtresult_utcoffset - dtstarttime_utcoffset
)
- dtresult = dtresult - datetime.timedelta(seconds=lag)
- result = self._datetime_to_timestamp(dtresult)
-
+ hours_before_midnight = 24 - dtstarttime.hour
+ if dtresult_utcoffset != dtstarttime_utcoffset:
+ if ((lag > 0 and lag_hours >= hours_before_midnight)
+ or (lag < 0 and
+ ((3600*lag_hours+abs(lag)) >= hours_before_midnight*3600))
+ ):
+ dtresult = dtresult - datetime.timedelta(seconds=lag)
+ result = self._datetime_to_timestamp(dtresult)
+ self.dst_start_time = result
self.cur = result
if issubclass(ret_type, datetime.datetime):
result = dtresult
@@ -532,3 +454,107 @@
return True
else:
return False
+
+ @classmethod
+ def expand(cls, expr_format):
+ expressions = expr_format.split()
+
+ if len(expressions) not in VALID_LEN_EXPRESSION:
+ raise CroniterBadCronError(cls.bad_length)
+
+ expanded = []
+ nth_weekday_of_month = {}
+
+ for i, expr in enumerate(expressions):
+ e_list = expr.split(',')
+ res = []
+
+ while len(e_list) > 0:
+ e = e_list.pop()
+
+ if i == 4:
+ e, sep, nth = str(e).partition('#')
+ if nth and not re.match(r'[1-5]', nth):
+ raise CroniterBadDateError(
+ "[{0}] is not acceptable".format(expr_format))
+
+ t = re.sub(r'^\*(\/.+)$', r'%d-%d\1' % (
+ cls.RANGES[i][0],
+ cls.RANGES[i][1]),
+ str(e))
+ m = search_re.search(t)
+
+ if not m:
+ t = re.sub(r'^(.+)\/(.+)$', r'\1-%d/\2' % (
+ cls.RANGES[i][1]),
+ str(e))
+ m = step_search_re.search(t)
+
+ if m:
+ (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
+
+ if not any_int_re.search(low):
+ low = "{0}".format(cls._alphaconv(i, low, expressions))
+
+ if not any_int_re.search(high):
+ high = "{0}".format(cls._alphaconv(i, high, expressions))
+
+ if (
+ not low or not high or int(low) > int(high)
+ or not only_int_re.search(str(step))
+ ):
+ raise CroniterBadDateError(
+ "[{0}] is not acceptable".format(expr_format))
+
+ low, high, step = map(int, [low, high, step])
+ rng = range(low, high + 1, step)
+ e_list += (["{0}#{1}".format(item, nth) for item in rng]
+ if i == 4 and nth else rng)
+ else:
+ if t.startswith('-'):
+ raise CroniterBadCronError(
+ "[{0}] is not acceptable,\
+ negative numbers not allowed".format(
+ expr_format))
+ if not star_or_int_re.search(t):
+ t = cls._alphaconv(i, t, expressions)
+
+ try:
+ t = int(t)
+ except:
+ pass
+
+ if t in cls.LOWMAP[i]:
+ t = cls.LOWMAP[i][t]
+
+ if (
+ t not in ["*", "l"]
+ and (int(t) < cls.RANGES[i][0] or
+ int(t) > cls.RANGES[i][1])
+ ):
+ raise CroniterBadCronError(
+ "[{0}] is not acceptable, out of range".format(
+ expr_format))
+
+ res.append(t)
+
+ if i == 4 and nth:
+ if t not in nth_weekday_of_month:
+ nth_weekday_of_month[t] = set()
+ nth_weekday_of_month[t].add(int(nth))
+
+ res.sort()
+ expanded.append(['*'] if (len(res) == 1
+ and res[0] == '*')
+ else res)
+
+ return expanded, nth_weekday_of_month
+
+ @classmethod
+ def is_valid(cls, expression):
+ try:
+ cls.expand(expression)
+ except CroniterError:
+ return False
+ else:
+ return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/src/croniter/tests/test_croniter.py new/croniter-0.3.20/src/croniter/tests/test_croniter.py
--- old/croniter-0.3.17/src/croniter/tests/test_croniter.py 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/src/croniter/tests/test_croniter.py 2017-11-06 22:22:31.000000000 +0100
@@ -2,10 +2,10 @@
# -*- coding: utf-8 -*-
import unittest
-from datetime import datetime, timedelta
+from datetime import datetime
from time import sleep
import pytz
-from croniter import croniter, CroniterBadDateError
+from croniter import croniter, CroniterBadDateError, CroniterBadCronError, CroniterNotAlphaError
from croniter.tests import base
@@ -733,5 +733,62 @@
val = croniter('0 * * * *', local_date).get_next(datetime)
self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 6)))
+ def test_std_dst2(self):
+ """
+ DST tests
+
+ This fixes https://github.com/taichino/croniter/issues/87
+
+ São Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00
+
+ """
+ tz = pytz.timezone("America/Sao_Paulo")
+ local_dates = [
+ # 17-22: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 21, 0, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 17-23: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 22, 0, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 17-23: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 23, 0, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 18-00: 00 -> 19-00:00
+ (tz.localize(datetime(2018, 2, 18, 0, 0, 0)),
+ '2018-02-19 00:00:00-03:00'),
+ # 17-22: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 21, 5, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 17-23: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 22, 5, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 17-23: 00 -> 18-00:00
+ (tz.localize(datetime(2018, 2, 17, 23, 5, 0)),
+ '2018-02-18 00:00:00-03:00'),
+ # 18-00: 00 -> 19-00:00
+ (tz.localize(datetime(2018, 2, 18, 0, 5, 0)),
+ '2018-02-19 00:00:00-03:00'),
+ ]
+ ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime)
+ for d in local_dates]
+ sret1 = ['{0}'.format(d) for d in ret1]
+ lret1 = ['{0}'.format(d[1]) for d in local_dates]
+ self.assertEqual(sret1, lret1)
+
+ def test_error_alpha_cron(self):
+ self.assertRaises(CroniterNotAlphaError, croniter.expand,
+ '* * * janu-jun *')
+
+ def test_error_bad_cron(self):
+ self.assertRaises(CroniterBadCronError, croniter.expand,
+ '* * * *')
+ self.assertRaises(CroniterBadCronError, croniter.expand,
+ '* * * * * * *')
+
+ def test_is_valid(self):
+ self.assertTrue(croniter.is_valid('0 * * * *'))
+ self.assertFalse(croniter.is_valid('0 * *'))
+ self.assertFalse(croniter.is_valid('* * * janu-jun *'))
+
if __name__ == '__main__':
unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/croniter-0.3.17/src/croniter.egg-info/PKG-INFO new/croniter-0.3.20/src/croniter.egg-info/PKG-INFO
--- old/croniter-0.3.17/src/croniter.egg-info/PKG-INFO 2017-05-22 12:11:46.000000000 +0200
+++ new/croniter-0.3.20/src/croniter.egg-info/PKG-INFO 2017-11-06 22:22:31.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: croniter
-Version: 0.3.17
+Version: 0.3.20
Summary: croniter provides iteration for datetime object with cron like format
Home-page: http://github.com/kiorky/croniter
Author: Matsumoto Taichi, kiorky
@@ -90,6 +90,11 @@
>>> print itr.get_prev(datetime) # 2010-07-01 00:00:00
>>> print itr.get_prev(datetime) # 2010-06-01 00:00:00
+ You can validate your crons using ``is_valid`` class method. (>= 0.3.18)::
+
+ >>> croniter.is_valid('0 0 1 * *') # True
+ >>> croniter.is_valid('0 wrong_value 1 * *') # False
+
About DST
=========
Be sure to init your croniter instance with a TZ aware datetime for this to work !::
@@ -144,6 +149,27 @@
Changelog
==============
+ 0.3.20 (2017-11-06)
+ -------------------
+
+ - More DST fixes
+ [Kevin Rose ]
+
+
+ 0.3.19 (2017-08-31)
+ -------------------
+
+ - fix #87: backward dst changes
+ [kiorky]
+
+
+ 0.3.18 (2017-08-31)
+ -------------------
+
+ - Add is valid method, refactor errors
+ [otherpirate, Mauro Murari ]
+
+
0.3.17 (2017-05-22)
-------------------
- DOW occurence sharp style support.