bug 1247212: improve release automation publishing of releases - allow pushes to be scheduled instead of done directly. r=rail
authorBen Hearsum <bhearsum@mozilla.com>
Mon, 03 Apr 2017 10:22:11 -0400
changeset 7415 68fd4127c104
parent 7414 0167a5d06a92
child 7416 db2276c714b0
push id5520
push userbhearsum@mozilla.com
push dateMon, 03 Apr 2017 14:22:20 +0000
reviewersrail
bugs1247212
bug 1247212: improve release automation publishing of releases - allow pushes to be scheduled instead of done directly. r=rail
lib/python/balrog/submitter/api.py
lib/python/balrog/submitter/cli.py
lib/python/vendor/balrogclient-0.0.1/balrogclient/__init__.py
lib/python/vendor/balrogclient-0.0.1/balrogclient/api.py
lib/python/vendor/python-dateutil-2.6.0/.gitattributes
lib/python/vendor/python-dateutil-2.6.0/.gitignore
lib/python/vendor/python-dateutil-2.6.0/.travis.yml
lib/python/vendor/python-dateutil-2.6.0/LICENSE
lib/python/vendor/python-dateutil-2.6.0/MANIFEST.in
lib/python/vendor/python-dateutil-2.6.0/NEWS
lib/python/vendor/python-dateutil-2.6.0/PKG-INFO
lib/python/vendor/python-dateutil-2.6.0/README.rst
lib/python/vendor/python-dateutil-2.6.0/RELEASING
lib/python/vendor/python-dateutil-2.6.0/appveyor.yml
lib/python/vendor/python-dateutil-2.6.0/ci_tools/pypy_upgrade.sh
lib/python/vendor/python-dateutil-2.6.0/ci_tools/retry.bat
lib/python/vendor/python-dateutil-2.6.0/ci_tools/retry.sh
lib/python/vendor/python-dateutil-2.6.0/codecov.yml
lib/python/vendor/python-dateutil-2.6.0/dateutil/__init__.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/_common.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/easter.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/parser.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/relativedelta.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/rrule.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/__init__.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/_common.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_easter.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_imports.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_parser.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_relativedelta.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_rrule.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_tz.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/tz/__init__.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/tz/_common.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/tz/tz.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/tz/win.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/tzwin.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/zoneinfo/__init__.py
lib/python/vendor/python-dateutil-2.6.0/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
lib/python/vendor/python-dateutil-2.6.0/dateutil/zoneinfo/rebuild.py
lib/python/vendor/python-dateutil-2.6.0/docs/Makefile
lib/python/vendor/python-dateutil-2.6.0/docs/conf.py
lib/python/vendor/python-dateutil-2.6.0/docs/easter.rst
lib/python/vendor/python-dateutil-2.6.0/docs/examples.rst
lib/python/vendor/python-dateutil-2.6.0/docs/index.rst
lib/python/vendor/python-dateutil-2.6.0/docs/make.bat
lib/python/vendor/python-dateutil-2.6.0/docs/parser.rst
lib/python/vendor/python-dateutil-2.6.0/docs/relativedelta.rst
lib/python/vendor/python-dateutil-2.6.0/docs/rrule.rst
lib/python/vendor/python-dateutil-2.6.0/docs/samples/EST5EDT.ics
lib/python/vendor/python-dateutil-2.6.0/docs/tz.rst
lib/python/vendor/python-dateutil-2.6.0/docs/zoneinfo.rst
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/PKG-INFO
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/SOURCES.txt
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/dependency_links.txt
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/requires.txt
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/top_level.txt
lib/python/vendor/python-dateutil-2.6.0/python_dateutil.egg-info/zip-safe
lib/python/vendor/python-dateutil-2.6.0/setup.cfg
lib/python/vendor/python-dateutil-2.6.0/setup.py
lib/python/vendor/python-dateutil-2.6.0/tox.ini
lib/python/vendor/python-dateutil-2.6.0/updatezinfo.py
lib/python/vendor/python-dateutil-2.6.0/zonefile_metadata.json
lib/python/vendor/six-1.10.0/CHANGES
lib/python/vendor/six-1.10.0/LICENSE
lib/python/vendor/six-1.10.0/MANIFEST.in
lib/python/vendor/six-1.10.0/PKG-INFO
lib/python/vendor/six-1.10.0/README
lib/python/vendor/six-1.10.0/documentation/Makefile
lib/python/vendor/six-1.10.0/documentation/conf.py
lib/python/vendor/six-1.10.0/documentation/index.rst
lib/python/vendor/six-1.10.0/setup.cfg
lib/python/vendor/six-1.10.0/setup.py
lib/python/vendor/six-1.10.0/six.egg-info/PKG-INFO
lib/python/vendor/six-1.10.0/six.egg-info/SOURCES.txt
lib/python/vendor/six-1.10.0/six.egg-info/dependency_links.txt
lib/python/vendor/six-1.10.0/six.egg-info/top_level.txt
lib/python/vendor/six-1.10.0/six.py
lib/python/vendor/six-1.10.0/test_six.py
lib/python/vendorlibs.pth
scripts/build-promotion/balrog-release-shipper.py
--- a/lib/python/balrog/submitter/api.py
+++ b/lib/python/balrog/submitter/api.py
@@ -1,8 +1,8 @@
 import os
 import site
 
 site.addsitedir(os.path.join(os.path.dirname(__file__), "../.."))
 
-from balrogclient import is_csrf_token_expired, SingleLocale, Release, Rule
+from balrogclient import is_csrf_token_expired, SingleLocale, Release, Rule, ScheduledRuleChange
 
-__all__ = [ 'is_csrf_token_expired', 'SingleLocale', 'Release', 'Rule' ]
+__all__ = [ 'is_csrf_token_expired', 'SingleLocale', 'Release', 'Rule', 'ScheduledRuleChange' ]
--- a/lib/python/balrog/submitter/cli.py
+++ b/lib/python/balrog/submitter/cli.py
@@ -1,19 +1,20 @@
+import arrow
 try:
     import simplejson as json
 except ImportError:
     import json
 
 from release.info import getProductDetails
 from release.paths import makeCandidatesDir
 from release.platforms import buildbot2updatePlatforms, buildbot2bouncer, \
   buildbot2ftp
 from release.versions import getPrettyVersion
-from balrog.submitter.api import Release, SingleLocale, Rule
+from balrog.submitter.api import Release, SingleLocale, Rule, ScheduledRuleChange
 from balrog.submitter.updates import merge_partial_updates
 from util.algorithms import recursive_update
 from util.retry import retry
 import logging
 from requests.exceptions import HTTPError
 
 log = logging.getLogger(__name__)
 
@@ -469,22 +470,50 @@ class ReleaseSubmitterV4(ReleaseSubmitte
 
 
 class ReleasePusher(object):
     def __init__(self, api_root, auth, dummy=False):
         self.api_root = api_root
         self.auth = auth
         self.dummy = dummy
 
-    def run(self, productName, version, build_number, rule_ids):
+    def run(self, productName, version, build_number, rule_ids, backgroundRate=None):
         name = get_release_blob_name(productName, version, build_number,
                                      self.dummy)
         for rule_id in rule_ids:
+            data = {"mapping": name}
+            if backgroundRate:
+                data["backgroundRate"] = backgroundRate
             Rule(api_root=self.api_root, auth=self.auth, rule_id=rule_id
-                 ).update_rule(mapping=name)
+                 ).update_rule(**data)
+
+
+class ReleaseScheduler(object):
+    def __init__(self, api_root, auth, dummy=False):
+        self.api_root = api_root
+        self.auth = auth
+        self.dummy = dummy
+
+    def run(self, productName, version, build_number, rule_ids, when, backgroundRate=None):
+        name = get_release_blob_name(productName, version, build_number,
+                                     self.dummy)
+        for rule_id in rule_ids:
+            data, data_version = Rule(api_root=self.api_root, auth=self.auth, rule_id=rule_id).get_data()
+            data["fallbackMapping"] = data["mapping"]
+            data["mapping"] = name
+            data["data_verison"] = data_version
+            data["rule_id"] = rule_id
+            data["change_type"] = "update"
+            # We receive an iso8601 datetime, but what Balrog needs is a to-the-millisecond epoch timestamp
+            data["when"] = arrow.get(when).timestamp * 1000
+            if backgroundRate:
+                data["backgroundRate"] = backgroundRate
+
+            ScheduledRuleChange(api_root=self.api_root, auth=self.auth, rule_id=rule_id
+                               ).add_scheduled_rule_change(**data)
 
 
 class BlobTweaker(object):
     def __init__(self, api_root, auth):
         self.api_root = api_root
         self.auth = auth
 
     def run(self, name, data):
--- a/lib/python/vendor/balrogclient-0.0.1/balrogclient/__init__.py
+++ b/lib/python/vendor/balrogclient-0.0.1/balrogclient/__init__.py
@@ -1,3 +1,3 @@
-from balrogclient.api import is_csrf_token_expired, SingleLocale, Release, Rule
+from balrogclient.api import is_csrf_token_expired, SingleLocale, Release, Rule, ScheduledRuleChange
 
-__all__ = [ 'is_csrf_token_expired', 'SingleLocale', 'Release', 'Rule' ]
\ No newline at end of file
+__all__ = [ 'is_csrf_token_expired', 'SingleLocale', 'Release', 'Rule', 'ScheduledRuleChange' ]
--- a/lib/python/vendor/balrogclient-0.0.1/balrogclient/api.py
+++ b/lib/python/vendor/balrogclient-0.0.1/balrogclient/api.py
@@ -231,8 +231,23 @@ class Rule(API):
     def __init__(self, rule_id, **kwargs):
         super(Rule, self).__init__(**kwargs)
         self.rule_id = rule_id
         self.url_template_vars = dict(rule_id=rule_id)
 
     def update_rule(self, **rule_data):
         """wrapper for self.request"""
         return self.request(method='POST', data=rule_data)
+
+
+class ScheduledRuleChange(API):
+    """Update Balrog rules"""
+    url_template = '/scheduled_changes/rules'
+    prerequest_url_template = '/rules/%(rule_id)s'
+
+    def __init__(self, rule_id, **kwargs):
+        super(ScheduledRuleChange, self).__init__(**kwargs)
+        self.rule_id = rule_id
+        self.url_template_vars = dict(rule_id=rule_id)
+
+    def add_scheduled_rule_change(self, **rule_data):
+        """wrapper for self.request"""
+        return self.request(method='POST', data=rule_data)
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/.gitattributes
@@ -0,0 +1,18 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Specify what's text and should be normalized
+*.py text
+*.in text
+*.rst text
+*.cfg text
+*.ini text
+*.yml text
+*.json text
+*.bat text
+*.sh text
+RELEASING text
+
+# NEWS and Windows batch files should be crlf
+NEWS eol=crlf
+*.bat eol=crlf
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/.gitignore
@@ -0,0 +1,14 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+build/
+dist/
+*.egg-info/
+.tox/
+
+# Sphinx documentation
+docs/_build/
+
+# Timezone information
+tzdata*.tar.gz
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/.travis.yml
@@ -0,0 +1,50 @@
+language: python
+python:
+  - "2.6"
+  - "2.7"
+  - "3.2"
+  - "3.3"
+  - "3.4"
+  - "3.5"
+  - "3.6-dev"
+  - "nightly"
+  - "pypy"
+  - "pypy3"
+
+matrix:
+  # pypy3 latest version is not playing nice.
+  allow_failures:
+    - python: "pypy3"
+    - python: "3.6-dev"
+    - python: "nightly"
+
+before_install:
+  # Travis version of Pypy is old and is causing some jobs to fail, so
+  # we should build this ourselves
+  - "export PYENV_ROOT=$HOME/.pyenv"
+  - |
+    if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
+      export PYPY_VERSION="5.4.1"
+      source ./ci_tools/pypy_upgrade.sh
+    fi
+  # Install codecov
+  - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install coverage==3.7.1; fi
+  - pip install codecov
+
+install:
+  - pip install six
+  - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi
+  - ./ci_tools/retry.sh python updatezinfo.py
+
+cache:
+  directories:
+    - $HOME/.pyenv
+    - $HOME/.cache/pip
+
+script:
+  - coverage run --omit=setup.py,dateutil/test/* setup.py test
+
+after_success:
+  - codecov
+
+sudo: false
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/LICENSE
@@ -0,0 +1,31 @@
+dateutil - Extensions to the standard Python datetime module.
+
+Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
+Copyright (c) 2012-2014 - Tomi Pieviläinen <tomi.pievilainen@iki.fi>
+Copyright (c) 2014      - Yaron de Leeuw <me@jarondl.net>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the copyright holder nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE NEWS zonefile_metadata.json updatezinfo.py
+recursive-include dateutil/test *
+global-exclude __pycache__
+global-exclude *.py[co]
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/NEWS
@@ -0,0 +1,427 @@
+Version 2.6.0
+-------------
+- Added PEP-495-compatible methods to address ambiguous and imaginary dates in
+  time zones in a backwards-compatible way. Ambiguous dates and times can now
+  be safely represented by all dateutil time zones. Many thanks to Alexander
+  Belopolski (@abalkin) and Tim Peters @tim-one for their inputs on how to
+  address this. Original issues reported by Yupeng and @zed (lP: 1390262,
+  gh issues #57, #112, #249, #284, #286, prs #127, #225, #248, #264, #302).
+- Added new methods for working with ambiguous and imaginary dates to the tz
+  module. datetime_ambiguous() determines if a datetime is ambiguous for a given
+  zone and datetime_exists() determines if a datetime exists in a given zone.
+  This works for all fold-aware datetimes, not just those provided by dateutil.
+  (gh issue #253, gh pr #302)
+- Fixed an issue where dst() in Portugal in 1996 was returning the wrong value
+  in tz.tzfile objects. Reported by @abalkin (gh issue #128, pr #225)
+- Fixed an issue where zoneinfo.ZoneInfoFile errors were not being properly
+  deep-copied. (gh issue #226, pr #225)
+- Refactored tzwin and tzrange as a subclass of a common class, tzrangebase, as
+  there was substantial overlapping functionality. As part of this change,
+  tzrange and tzstr now expose a transitions() function, which returns the
+  DST on and off transitions for a given year. (gh issue #260, pr #302)
+- Deprecated zoneinfo.gettz() due to confusion with tz.gettz(), in favor of
+  get() method of zoneinfo.ZoneInfoFile objects. (gh issue #11, pr #310)
+- For non-character, non-stream arguments, parser.parse now raises TypeError
+  instead of AttributeError. (gh issues #171, #269, pr #247)
+- Fixed an issue where tzfile objects were not properly handling dst() and
+  tzname() when attached to datetime.time objects. Reported by @ovacephaloid.
+  (gh issue #292, pr #309)
+- /usr/share/lib/zoneinfo was added to TZPATHS for compatibility with Solaris
+  systems. Reported by @dhduvall (gh issue #276, pr #307)
+- tzoffset and tzrange objects now accept either a number of seconds or a
+  datetime.timedelta() object wherever previously only a number of seconds was
+  allowed. (gh pr #264, #277)
+- datetime.timedelta objects can now be added to relativedelta objects. Reported
+  and added by Alec Nikolas Reiter (@justanr) (gh issue #282, pr #283
+- Refactored relativedelta.weekday and rrule.weekday into a common base class
+  to reduce code duplication. (gh issue #140, pr #311)
+- An issue where the WKST parameter was improperly rendering in str(rrule) was
+  reported and fixed by Daniel LePage (@dplepage). (gh issue #262, pr #263)
+- A replace() method has been added to rrule objects by @jendas1, which creates
+  new rrule with modified attributes, analogous to datetime.replace (gh pr #167)
+- Made some significant performance improvements to rrule objects in Python 2.x
+  (gh pr #245)
+- All classes defining equality functions now return NotImplemented when
+  compared to unsupported classes, rather than raising TypeError, to allow other
+  classes to provide fallback support. (gh pr #236)
+- Several classes have been marked as explicitly unhashable to maintain
+  identical behavior between Python 2 and 3. Submitted by Roy Williams 
+  (@rowillia) (gh pr #296)
+- Trailing whitespace in easter.py has been removed. Submitted by @OmgImAlexis
+  (gh pr #299)
+- Windows-only batch files in build scripts had line endings switched to CRLF.
+  (gh pr #237)
+- @adamchainz updated the documentation links to reflect that the canonical
+  location for readthedocs links is now at .io, not .org. (gh pr #272)
+- Made some changes to the CI and codecov to test against newer versions of
+  Python and pypy, and to adjust the code coverage requirements. For the moment,
+  full pypy3 compatibility is not supported until a new release is available,
+  due to upstream bugs in the old version affecting PEP-495 support.
+  (gh prs #265, #266, #304, #308)
+- The full PGP signing key fingerprint was added to the README.md in favor of
+  the previously used long-id. Reported by @valholl (gh issue #287, pr #304)
+- Updated zoneinfo to 2016i. (gh issue #298, gh pr #306)
+
+
+Version 2.5.3
+-------------
+- Updated zoneinfo to 2016d
+- Fixed parser bug where unambiguous datetimes fail to parse when dayfirst is
+  set to true. (gh issue #233, pr #234)
+- Bug in zoneinfo file on platforms such as Google App Engine which do not
+  do not allow importing of subprocess.check_call was reported and fixed by
+  @savraj (gh issue #239, gh pr #240)
+- Fixed incorrect version in documentation (gh issue #235, pr #243)
+
+Version 2.5.2
+-------------
+- Updated zoneinfo to 2016c
+- Fixed parser bug where yearfirst and dayfirst parameters were not being
+  respected when no separator was present. (gh issue #81 and #217, pr #229)
+
+Version 2.5.1
+-------------
+- Updated zoneinfo to 2016b
+- Changed MANIFEST.in to explicitly include test suite in source distributions,
+  with help from @koobs (gh issue #193, pr #194, #201, #221)
+- Explicitly set all line-endings to LF, except for the NEWS file, on a
+  per-repository basis (gh pr #218)
+- Fixed an issue with improper caching behavior in rruleset objects (gh issue
+  #104, pr #207)
+- Changed to an explicit error when rrulestr strings contain a missing BYDAY
+  (gh issue #162, pr #211)
+- tzfile now correctly handles files containing leapcnt (although the leapcnt
+  information is not actually used). Contributed by @hjoukl (gh issue #146, pr
+  #147)
+- Fixed recursive import issue with tz module (gh pr #204)
+- Added compatibility between tzwin objects and datetime.time objects (gh issue
+  #216, gh pr #219)
+- Refactored monolithic test suite by module (gh issue #61, pr #200 and #206)
+- Improved test coverage in the relativedelta module (gh pr #215)
+- Adjusted documentation to reflect possibly counter-intuitive properties of
+  RFC-5545-compliant rrules, and other documentation improvements in the rrule
+  module (gh issue #105, gh issue #149 - pointer to the solution by @phep,
+  pr #213).
+
+
+Version 2.5.0
+-------------
+- Updated zoneinfo to 2016a
+- zoneinfo_metadata file version increased to 2.0 - the updated updatezinfo.py
+  script will work with older zoneinfo_metadata.json files, but new metadata
+  files will not work with older updatezinfo.py versions. Additionally, we have
+  started hosting our own mirror of the Olson databases on a github pages
+  site (https://dateutil.github.io/tzdata/) (gh pr #183)
+- dateutil zoneinfo tarballs now contain the full zoneinfo_metadata file used
+  to generate them. (gh issue #27, gh pr #85)
+- relativedelta can now be safely subclassed without derived objects reverting
+  to base relativedelta objects as a result of arithmetic operations.
+  (lp:1010199, gh issue #44, pr #49)
+- relativedelta 'weeks' parameter can now be set and retrieved as a property of
+  relativedelta instances. (lp: 727525, gh issue #45, pr #49)
+- relativedelta now explicitly supports fractional relative weeks, days, hours,
+  minutes and seconds. Fractional values in absolute parameters (year, day, etc)
+  are now deprecated. (gh issue #40, pr #190)
+- relativedelta objects previously did not use microseconds to determine of two
+  relativedelta objects were equal. This oversight has been corrected.
+  Contributed by @elprans (gh pr #113)
+- rrule now has an xafter() method for retrieving multiple recurrences after a
+  specified date. (gh pr #38)
+- str(rrule) now returns an RFC2445-compliant rrule string, contributed by
+  @schinckel and @armicron (lp:1406305, gh issue #47, prs #50, #62 and #160)
+- rrule performance under certain conditions has been significantly improved
+  thanks to a patch contributed by @dekoza, based on an article by Brian Beck
+  (@exogen) (gh pr #136)
+- The use of both the 'until' and 'count' parameters is now deprecated as
+  inconsistent with RFC2445 (gh pr #62, #185)
+- Parsing an empty string will now raise a ValueError, rather than returning the
+  datetime passed to the 'default' parameter. (gh issue #78, pr #187)
+- tzwinlocal objects now have a meaningful repr() and str() implementation
+  (gh issue #148, prs #184 and #186)
+- Added equality logic for tzwin and tzwinlocal objects. (gh issue #151,
+  pr #180, #184)
+- Added some flexibility in subclassing timelex, and switched the default
+  behavior over to using string methods rather than comparing against a fixed
+  list. (gh pr #122, #139)
+- An issue causing tzstr() to crash on Python 2.x was fixed. (lp: 1331576,
+  gh issue #51, pr #55)
+- An issue with string encoding causing exceptions under certain circumstances
+  when tzname() is called was fixed. (gh issue #60, #74, pr #75)
+- Parser issue where calling parse() on dates with no day specified when the
+  day of the month in the default datetime (which is "today" if unspecified) is
+  greater than the number of days in the parsed month was fixed (this issue
+  tended to crop up between the 29th and 31st of the month, for obvious reasons)
+  (canonical gh issue #25, pr #30, #191)
+- Fixed parser issue causing fuzzy_with_tokens to raise an unexpected exception
+  in certain circumstances. Contributed by @MichaelAquilina (gh pr #91)
+- Fixed parser issue where years > 100 AD were incorrectly parsed. Contributed
+  by @Bachmann1234 (gh pr #130)
+- Fixed parser issue where commas were not a valid separator between seconds
+  and microseconds, preventing parsing of ISO 8601 dates. Contributed by
+  @ryanss (gh issue #28, pr #106)
+- Fixed issue with tzwin encoding in locales with non-Latin alphabets
+  (gh issue #92, pr #98)
+- Fixed an issue where tzwin was not being properly imported on Windows.
+  Contributed by @labrys. (gh pr #134)
+- Fixed a problem causing issues importing zoneinfo in certain circumstances.
+  Issue and solution contributed by @alexxv (gh issue #97, pr #99)
+- Fixed an issue where dateutil timezones were not compatible with basic time
+  objects. One of many, many timezone related issues contributed and tested by
+  @labrys. (gh issue #132, pr #181)
+- Fixed issue where tzwinlocal had an invalid utcoffset. (gh issue #135,
+  pr #141, #142)
+- Fixed issue with tzwin and tzwinlocal where DST transitions were incorrectly
+  parsed from the registry. (gh issue #143, pr #178)
+- updatezinfo.py no longer suppresses certain OSErrors. Contributed by @bjamesv
+  (gh pr #164)
+- An issue that arose when timezone locale changes during runtime has been
+  fixed by @carlosxl and @mjschultz (gh issue #100, prs #107, #109)
+- Python 3.5 was added to the supported platforms in the metadata (@tacaswell
+  gh pr #159) and the test suites (@moreati gh pr #117).
+- An issue with tox failing without unittest2 installed in Python 2.6 was fixed
+  by @moreati (gh pr #115)
+- Several deprecated functions were replaced in the tests by @moreati
+  (gh pr #116)
+- Improved the logic in Travis and Appveyor to alleviate issues where builds
+  were failing due to connection issues when downloading the IANA timezone
+  files. In addition to adding our own mirror for the files (gh pr #183), the
+  download is now retried a number of times (with a delay) (gh pr #177)
+- Many failing doctests were fixed by @moreati. (gh pr #120)
+- Many fixes to the documentation (gh pr #103, gh pr #87 from @radarhere,
+  gh pr #154 from @gpoesia, gh pr #156 from @awsum, gh pr #168 from @ja8zyjits)
+- Added a code coverage tool to the CI to help improve the library. (gh pr #182)
+- We now have a mailing list - dateutil@python.org, graciously hosted by
+  Python.org.
+
+
+Version 2.4.2
+-------------
+- Updated zoneinfo to 2015b.
+- Fixed issue with parsing of tzstr on Python 2.7.x; tzstr will now be decoded
+  if not a unicode type. gh #51 (lp:1331576), gh pr #55.
+- Fix a parser issue where AM and PM tokens were showing up in fuzzy date
+  stamps, triggering inappropriate errors. gh #56 (lp: 1428895), gh pr #63.
+- Missing function "setcachesize" removed from zoneinfo __all__ list by @ryanss,
+  fixing an issue with wildcard imports of dateutil.zoneinfo. (gh pr #66).
+- (PyPi only) Fix an issue with source distributions not including the test
+  suite.
+
+
+Version 2.4.1
+-------------
+
+- Added explicit check for valid hours if AM/PM is specified in parser.
+  (gh pr #22, issue #21)
+- Fix bug in rrule introduced in 2.4.0 where byweekday parameter was not
+  handled properly. (gh pr #35, issue #34)
+- Fix error where parser allowed some invalid dates, overwriting existing hours
+  with the last 2-digit number in the string. (gh pr #32, issue #31)
+- Fix and add test for Python 2.x compatibility with boolean checking of
+  relativedelta objects. Implemented by @nimasmi (gh pr #43) and Cédric Krier
+  (lp: 1035038)
+- Replaced parse() calls with explicit datetime objects in unit tests unrelated
+  to parser. (gh pr #36)
+- Changed private _byxxx from sets to sorted tuples and fixed one currently
+  unreachable bug in _construct_byset. (gh pr #54)
+- Additional documentation for parser (gh pr #29, #33, #41) and rrule.
+- Formatting fixes to documentation of rrule and README.rst.
+- Updated zoneinfo to 2015a.
+
+Version 2.4.0
+-------------
+
+- Fix an issue with relativedelta and freezegun (lp:1374022)
+- Fix tzinfo in windows for timezones without dst (lp:1010050, gh #2)
+- Ignore missing timezones in windows like in POSIX
+- Fix minimal version requirement for six (gh #6)
+- Many rrule changes and fixes by @pganssle (gh pull requests #13 #14 #17),
+    including defusing some infinite loops (gh #4)
+
+Version 2.3
+-----------
+
+- Cleanup directory structure, moved test.py to dateutil/tests/test.py
+
+- Changed many aspects of dealing with the zone info file. Instead of a cache,
+  all the zones are loaded to memory, but symbolic links are loaded only once,
+  so not much memory is used.
+
+- The package is now zip-safe, and universal-wheelable, thanks to changes in
+  the handling of the zoneinfo file.
+
+- Fixed tzwin silently not imported on windows python2
+
+- New maintainer, together with new hosting: GitHub, Travis, Read-The-Docs
+
+Version 2.2
+-----------
+
+- Updated zoneinfo to 2013h
+
+- fuzzy_with_tokens parse addon from Christopher Corley
+
+- Bug with LANG=C fixed by Mike Gilbert
+
+Version 2.1
+-----------
+
+- New maintainer
+
+- Dateutil now works on Python 2.6, 2.7 and 3.2 from same codebase (with six)
+
+- #704047: Ismael Carnales' patch for a new time format
+
+- Small bug fixes, thanks for reporters!
+
+
+Version 2.0
+-----------
+
+- Ported to Python 3, by Brian Jones.  If you need dateutil for Python 2.X,
+  please continue using the 1.X series.
+
+- There's no such thing as a "PSF License".  This source code is now
+  made available under the Simplified BSD license.  See LICENSE for
+  details.
+
+Version 1.5
+-----------
+
+- As reported by Mathieu Bridon, rrules were matching the bysecond rules
+  incorrectly against byminute in some circumstances when the SECONDLY
+  frequency was in use, due to a copy & paste bug.  The problem has been
+  unittested and corrected.
+
+- Adam Ryan reported a problem in the relativedelta implementation which
+  affected the yearday parameter in the month of January specifically.
+  This has been unittested and fixed.
+
+- Updated timezone information.
+
+
+Version 1.4.1
+-------------
+
+- Updated timezone information.
+
+
+Version 1.4
+-----------
+
+- Fixed another parser precision problem on conversion of decimal seconds
+  to microseconds, as reported by Erik Brown.  Now these issues are gone
+  for real since it's not using floating point arithmetic anymore.
+
+- Fixed case where tzrange.utcoffset and tzrange.dst() might fail due
+  to a date being used where a datetime was expected (reported and fixed
+  by Lennart Regebro).
+
+- Prevent tzstr from introducing daylight timings in strings that didn't
+  specify them (reported by Lennart Regebro).
+
+- Calls like gettz("GMT+3") and gettz("UTC-2") will now return the
+  expected values, instead of the TZ variable behavior.
+
+- Fixed DST signal handling in zoneinfo files.  Reported by
+  Nicholas F. Fabry and John-Mark Gurney.
+
+
+Version 1.3
+-----------
+
+- Fixed precision problem on conversion of decimal seconds to
+  microseconds, as reported by Skip Montanaro.
+
+- Fixed bug in constructor of parser, and converted parser classes to
+  new-style classes.  Original report and patch by Michael Elsdörfer.
+
+- Initialize tzid and comps in tz.py, to prevent the code from ever
+  raising a NameError (even with broken files).  Johan Dahlin suggested
+  the fix after a pyflakes run.
+
+- Version is now published in dateutil.__version__, as requested
+  by Darren Dale.
+
+- All code is compatible with new-style division.
+
+
+Version 1.2
+-----------
+
+- Now tzfile will round timezones to full-minutes if necessary,
+  since Python's datetime doesn't support sub-minute offsets.
+  Thanks to Ilpo Nyyssönen for reporting the issue.
+
+- Removed bare string exceptions, as reported and fixed by
+  Wilfredo Sánchez Vega.
+
+- Fix bug in leap count parsing (reported and fixed by Eugene Oden).
+
+
+Version 1.1
+-----------
+
+- Fixed rrule byyearday handling. Abramo Bagnara pointed out that
+  RFC2445 allows negative numbers.
+
+- Fixed --prefix handling in setup.py (by Sidnei da Silva).
+
+- Now tz.gettz() returns a tzlocal instance when not given any
+  arguments and no other timezone information is found.
+
+- Updating timezone information to version 2005q.
+
+
+Version 1.0
+-----------
+
+- Fixed parsing of XXhXXm formatted time after day/month/year
+  has been parsed.
+
+- Added patch by Jeffrey Harris optimizing rrule.__contains__.
+
+
+Version 0.9
+-----------
+
+- Fixed pickling of timezone types, as reported by
+  Andreas Köhler.
+
+- Implemented internal timezone information with binary
+  timezone files [1]. datautil.tz.gettz() function will now
+  try to use the system timezone files, and fallback to
+  the internal versions. It's also possible to ask for
+  the internal versions directly by using
+  dateutil.zoneinfo.gettz().
+
+- New tzwin timezone type, allowing access to Windows
+  internal timezones (contributed by Jeffrey Harris).
+
+- Fixed parsing of unicode date strings.
+
+- Accept parserinfo instances as the parser constructor
+  parameter, besides parserinfo (sub)classes.
+
+- Changed weekday to spell the not-set n value as None
+  instead of 0.
+
+- Fixed other reported bugs.
+
+[1] http://www.twinsun.com/tz/tz-link.htm
+
+
+Version 0.5
+-----------
+
+- Removed FREQ_ prefix from rrule frequency constants
+  WARNING: this breaks compatibility with previous versions.
+
+- Fixed rrule.between() for cases where "after" is achieved
+  before even starting, as reported by Andreas Köhler.
+
+- Fixed two digit zero-year parsing (such as 31-Dec-00), as
+  reported by Jim Abramson, and included test case for this.
+
+- Sort exdate and rdate before iterating over them, so that
+  it's not necessary to sort them before adding to the rruleset,
+  as reported by Nicholas Piper.
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/PKG-INFO
@@ -0,0 +1,28 @@
+Metadata-Version: 1.1
+Name: python-dateutil
+Version: 2.6.0
+Summary: Extensions to the standard Python datetime module
+Home-page: https://dateutil.readthedocs.io
+Author: Paul Ganssle, Yaron de Leeuw
+Author-email: dateutil@python.org
+License: Simplified BSD
+Description: 
+        The dateutil module provides powerful extensions to the
+        datetime module available in the Python standard library.
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Software Development :: Libraries
+Requires: six
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/README.rst
@@ -0,0 +1,143 @@
+dateutil - powerful extensions to datetime
+==========================================
+
+.. image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square
+    :target: https://travis-ci.org/dateutil/dateutil
+    :alt: travis build status
+
+.. image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square
+    :target: https://ci.appveyor.com/project/dateutil/dateutil
+    :alt: appveyor build status
+
+.. image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master
+    :target: https://codecov.io/github/dateutil/dateutil?branch=master
+    :alt: Code coverage
+
+.. image:: https://img.shields.io/pypi/dd/python-dateutil.svg?style=flat-square
+    :target: https://pypi.python.org/pypi/python-dateutil/
+    :alt: pypi downloads per day
+
+.. image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square
+    :target: https://pypi.python.org/pypi/python-dateutil/
+    :alt: pypi version
+
+
+The `dateutil` module provides powerful extensions to
+the standard `datetime` module, available in Python.
+
+
+Download
+========
+dateutil is available on PyPI
+https://pypi.python.org/pypi/python-dateutil/
+
+The documentation is hosted at:
+https://dateutil.readthedocs.io/
+
+Code
+====
+https://github.com/dateutil/dateutil/
+
+Features
+========
+
+* Computing of relative deltas (next month, next year,
+  next monday, last week of month, etc);
+* Computing of relative deltas between two given
+  date and/or datetime objects;
+* Computing of dates based on very flexible recurrence rules,
+  using a superset of the `iCalendar <https://www.ietf.org/rfc/rfc2445.txt>`_
+  specification. Parsing of RFC strings is supported as well.
+* Generic parsing of dates in almost any string format;
+* Timezone (tzinfo) implementations for tzfile(5) format
+  files (/etc/localtime, /usr/share/zoneinfo, etc), TZ
+  environment string (in all known formats), iCalendar
+  format files, given ranges (with help from relative deltas),
+  local machine timezone, fixed offset timezone, UTC timezone,
+  and Windows registry-based time zones.
+* Internal up-to-date world timezone information based on
+  Olson's database.
+* Computing of Easter Sunday dates for any given year,
+  using Western, Orthodox or Julian algorithms;
+* A comprehensive test suite.
+
+Quick example
+=============
+Here's a snapshot, just to give an idea about the power of the
+package. For more examples, look at the documentation.
+
+Suppose you want to know how much time is left, in
+years/months/days/etc, before the next easter happening on a
+year with a Friday 13th in August, and you want to get today's
+date out of the "date" unix system command. Here is the code:
+
+.. doctest:: readmeexample
+
+    >>> from dateutil.relativedelta import *
+    >>> from dateutil.easter import *
+    >>> from dateutil.rrule import *
+    >>> from dateutil.parser import *
+    >>> from datetime import *
+    >>> now = parse("Sat Oct 11 17:13:46 UTC 2003")
+    >>> today = now.date()
+    >>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year
+    >>> rdelta = relativedelta(easter(year), today)
+    >>> print("Today is: %s" % today)
+    Today is: 2003-10-11
+    >>> print("Year with next Aug 13th on a Friday is: %s" % year)
+    Year with next Aug 13th on a Friday is: 2004
+    >>> print("How far is the Easter of that year: %s" % rdelta)
+    How far is the Easter of that year: relativedelta(months=+6)
+    >>> print("And the Easter of that year is: %s" % (today+rdelta))
+    And the Easter of that year is: 2004-04-11
+
+Being exactly 6 months ahead was **really** a coincidence :)
+
+
+Author
+======
+The dateutil module was written by Gustavo Niemeyer <gustavo@niemeyer.net>
+in 2003.
+
+It is maintained by:
+
+* Gustavo Niemeyer <gustavo@niemeyer.net> 2003-2011
+* Tomi Pieviläinen <tomi.pievilainen@iki.fi> 2012-2014
+* Yaron de Leeuw <me@jarondl.net> 2014-
+* Paul Ganssle <paul@ganssle.io> 2015-
+
+Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of 
+conduct <https://www.python.org/psf/codeofconduct/>`_.
+
+Building and releasing
+======================
+When you get the source, it does not contain the internal zoneinfo
+database. To get (and update) the database, run the updatezinfo.py script. Make sure
+that the zic command is in your path, and that you have network connectivity
+to get the latest timezone information from IANA, or from `our mirror of the
+IANA database <https://dateutil.github.io/tzdata/>`_.
+
+Starting with version 2.4.1, all source and binary distributions will be signed
+by a PGP key that has, at the very least, been signed by the key which made the
+previous release. A table of release signing keys can be found below:
+
+===========  ============================
+Releases     Signing key fingerprint
+===========  ============================
+2.4.1-       `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_
+===========  ============================
+
+Testing
+=======
+dateutil has a comprehensive test suite, which can be run simply by running
+`python setup.py test [-q]` in the project root. Note that if you don't have the internal
+zoneinfo database, some tests will fail. Apart from that, all tests should pass.
+
+To easily test dateutil against all supported Python versions, you can use
+`tox <https://tox.readthedocs.io/en/latest/>`_.
+
+All github pull requests are automatically tested using travis and appveyor.
+
+
+.. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB:
+   https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/RELEASING
@@ -0,0 +1,95 @@
+Release Checklist
+-----------------------------------------
+[ ] Update __version__ string
+[ ] Update classifiers in setup.py to include the latest supported Python
+    versions.
+[ ] Update the metadata in zonefile_metadata.json to include the latest tzdata
+    release from https://www.iana.org/time-zones.
+[ ] If necessary, update the tzdata mirror at https://github.com/dateutil/tzdata
+[ ] Update NEWS with list of changes, giving credit to contributors.
+[ ] Build the source distribution as, at a minimum, .tar.gz and .zip
+[ ] Verify that the source distribution contains all necessary components.
+[ ] Build the binary distribution as a wheel
+[ ] Verify that the binary distribution can be installed and works.
+[ ] Generate MD5 hashes for the source and binary distributions
+[ ] Sign the source and binary distributions with a GPG key (if not the one
+    that made the release, then one signed by the one that made the last
+    release)
+[ ] Commit the changes in git and make a pull request.
+[ ] Accept the pull request and tag the repository with the release number.
+[ ] Add the contents of the NEWS file to the github release notes for the
+    release.
+[ ] Upload the source and binary distributions along with hashes and signatures
+    to pypi.
+
+Optional:
+----------
+[ ] Check that README.rst is up-to-date.
+[ ] Check that the documentation builds correctly (cd docs, make html)
+
+
+Instructions
+-----------------------------------------
+See the instructions at https://packaging.python.org/en/latest/distributing/
+for more details.
+
+
+Versioning
+----------
+Try and keep to a semantic versioning scheme (http://semver.org/).
+
+
+Source releases
+----------
+Release the sources with, at a minimum, .tar.gz and .zip. Make sure you have a
+relatively recent version of setuptools when making the source distribution, as
+earlier version will not include the tests. Other formats are
+optional. They can be generated using:
+
+    python setup.py sdist --formats=zip,gztar
+
+To verify that a source release is correct, inspect it using whatever archive
+utility you have and make sure it contains all the modules and the tests. Also
+make sure that the zoneinfo file is included in the You
+may also want to generate a new clean virtualenv and run the tests from the
+source distribution (python setup.py test).
+
+
+Binary releases
+----------
+It should always be possible to generate a universal wheel binary distribution
+for each release. Generally we do not generate .egg files. In order to generate
+a wheel, you need the wheel package (https://wheel.readthedocs.io/en/latest/)
+installed, which can be installed with:
+
+    pip install wheel
+
+Once this is done, generate the wheel with:
+
+    python setup.py bdist_wheel
+
+
+Signing and generate checksums
+----------
+Since all the outputs are generated in the dist/ directory, can generate all the
+md5 checksums at once from the base directory by executing:
+
+    md5sum dist/*
+
+Save these for when uploading the files. Following this, go into the dist
+directory and sign each of the results with the relevant key:
+
+    gpg --armor --output <fname>.asc --detach-sig <fname>
+
+To automate this for all files, you can use the following command:
+
+    for f  in dist/*; do gpg --armor --output $f.asc --detach-sig $f; done
+
+Save these .asc files for when uploading to pypi. Before uploading them, verify
+that they are valid signatures:
+
+    gpg --verify <fname>.asc <fname>
+
+To do this in bulk, you can use the command:
+
+    for f in $(find ./dist -type f | grep -v '.asc$'); do gpg --verify $f.asc $f; done
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/appveyor.yml
@@ -0,0 +1,35 @@
+build: false
+environment:
+  matrix:
+    - PYTHON: "C:/Python27"
+    - PYTHON: "C:/Python27-x64"
+    - PYTHON: "C:/Python33"
+    - PYTHON: "C:/Python33-x64"
+    - PYTHON: "C:/Python34"
+    - PYTHON: "C:/Python34-x64"
+    - PYTHON: "C:/Python35"
+    - PYTHON: "C:/Python35-x64"
+install:
+  # Add PostgreSQL (zic), Python and scripts directory to current path
+  - set path=c:\Program Files\PostgreSQL\9.3\bin\;%PATH%
+  - set path=%PATH%;%PYTHON%;%PYTHON%/Scripts
+
+  # If this isn't done, I guess Appveyor will install to the Python2.7 version
+  - set pip_cmd=%PYTHON%/python.exe -m pip
+
+  # Download scripts and dependencies
+  - ps: Start-FileDownload 'https://bootstrap.pypa.io/get-pip.py'
+  - "%PYTHON%/python.exe get-pip.py"
+  - "%pip_cmd% install six"
+  - "%pip_cmd% install coverage"
+  - "%pip_cmd% install codecov"
+
+  # This frequently fails with network errors, so we'll retry it up to 5 times
+  # with a 1 minute rate limit.
+  - "ci_tools/retry.bat %PYTHON%/python.exe updatezinfo.py"
+  # This environment variable tells the test suite it's OK to mess with the time zone.
+  - set DATEUTIL_MAY_CHANGE_TZ=1
+test_script:
+  - "coverage run --omit=setup.py,dateutil/test/* setup.py test"
+after_test:
+  - "codecov"
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/ci_tools/pypy_upgrade.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Need to install an upgraded version of pypy.
+if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
+  pushd "$PYENV_ROOT" && git pull && popd
+else
+  rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
+fi
+
+"$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION"
+virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
+source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/ci_tools/retry.bat
@@ -0,0 +1,18 @@
+@echo off
+REM This script takes a command and retries it a few times if it fails, with a
+REM timeout between each retry.
+
+setlocal EnableDelayedExpansion
+
+REM Loop at most n_retries times, waiting sleep_time times between
+set sleep_time=60
+set n_retries=5
+
+for /l %%x in (1, 1, %n_retries%) do (
+  call %*
+  if not ERRORLEVEL 1 EXIT /B 0
+  timeout /t %sleep_time% /nobreak > nul
+)
+
+REM If it failed all n_retries times, we can give up at last.
+EXIT /B 1
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/ci_tools/retry.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+sleep_time=60
+n_retries=5
+
+for i in `seq 1 $n_retries`; do
+    "$@" && exit 0
+    sleep $sleep_time
+done
+
+exit 1
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/codecov.yml
@@ -0,0 +1,9 @@
+coverage:
+  status:
+    patch: false
+    changes: false
+    project:
+      default:
+        target: '80'
+
+comment: false
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+__version__ = "2.6.0"
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/_common.py
@@ -0,0 +1,33 @@
+"""
+Common code used in multiple modules.
+"""
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    __hash__ = None
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/easter.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a generic easter computing method for any given year, using
+Western, Orthodox or Julian algorithms.
+"""
+
+import datetime
+
+__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
+
+EASTER_JULIAN = 1
+EASTER_ORTHODOX = 2
+EASTER_WESTERN = 3
+
+
+def easter(year, method=EASTER_WESTERN):
+    """
+    This method was ported from the work done by GM Arts,
+    on top of the algorithm by Claus Tondering, which was
+    based in part on the algorithm of Ouding (1940), as
+    quoted in "Explanatory Supplement to the Astronomical
+    Almanac", P.  Kenneth Seidelmann, editor.
+
+    This algorithm implements three different easter
+    calculation methods:
+
+    1 - Original calculation in Julian calendar, valid in
+        dates after 326 AD
+    2 - Original method, with date converted to Gregorian
+        calendar, valid in years 1583 to 4099
+    3 - Revised method, in Gregorian calendar, valid in
+        years 1583 to 4099 as well
+
+    These methods are represented by the constants:
+
+    * ``EASTER_JULIAN   = 1``
+    * ``EASTER_ORTHODOX = 2``
+    * ``EASTER_WESTERN  = 3``
+
+    The default method is method 3.
+
+    More about the algorithm may be found at:
+
+    http://users.chariot.net.au/~gmarts/eastalg.htm
+
+    and
+
+    http://www.tondering.dk/claus/calendar.html
+
+    """
+
+    if not (1 <= method <= 3):
+        raise ValueError("invalid method")
+
+    # g - Golden year - 1
+    # c - Century
+    # h - (23 - Epact) mod 30
+    # i - Number of days from March 21 to Paschal Full Moon
+    # j - Weekday for PFM (0=Sunday, etc)
+    # p - Number of days from March 21 to Sunday on or before PFM
+    #     (-6 to 28 methods 1 & 3, to 56 for method 2)
+    # e - Extra days to add for method 2 (converting Julian
+    #     date to Gregorian date)
+
+    y = year
+    g = y % 19
+    e = 0
+    if method < 3:
+        # Old method
+        i = (19*g + 15) % 30
+        j = (y + y//4 + i) % 7
+        if method == 2:
+            # Extra dates to convert Julian to Gregorian date
+            e = 10
+            if y > 1600:
+                e = e + y//100 - 16 - (y//100 - 16)//4
+    else:
+        # New method
+        c = y//100
+        h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
+        i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
+        j = (y + y//4 + i + 2 - c + c//4) % 7
+
+    # p can be from -6 to 56 corresponding to dates 22 March to 23 May
+    # (later dates apply to method 2, although 23 May never actually occurs)
+    p = i - j + e
+    d = 1 + (p + 27 + (p + 6)//40) % 31
+    m = 3 + (p + 26)//30
+    return datetime.date(int(y), int(m), int(d))
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/parser.py
@@ -0,0 +1,1360 @@
+# -*- coding:iso-8859-1 -*-
+"""
+This module offers a generic date/time string parser which is able to parse
+most known formats to represent a date and/or time.
+
+This module attempts to be forgiving with regards to unlikely input formats,
+returning a datetime object even for dates which are ambiguous. If an element
+of a date/time stamp is omitted, the following rules are applied:
+- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour
+  on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
+  specified.
+- If a time zone is omitted, a timezone-naive datetime is returned.
+
+If any other elements are missing, they are taken from the
+:class:`datetime.datetime` object passed to the parameter ``default``. If this
+results in a day number exceeding the valid number of days per month, the
+value falls back to the end of the month.
+
+Additional resources about date/time string formats can be found below:
+
+- `A summary of the international standard date and time notation
+  <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
+- `W3C Date and Time Formats <http://www.w3.org/TR/NOTE-datetime>`_
+- `Time Formats (Planetary Rings Node) <http://pds-rings.seti.org/tools/time_formats.html>`_
+- `CPAN ParseDate module
+  <http://search.cpan.org/~muir/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_
+- `Java SimpleDateFormat Class
+  <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_
+"""
+from __future__ import unicode_literals
+
+import datetime
+import string
+import time
+import collections
+import re
+from io import StringIO
+from calendar import monthrange, isleap
+
+from six import text_type, binary_type, integer_types
+
+from . import relativedelta
+from . import tz
+
+__all__ = ["parse", "parserinfo"]
+
+
+class _timelex(object):
+    # Fractional seconds are sometimes split by a comma
+    _split_decimal = re.compile("([\.,])")
+
+    def __init__(self, instream):
+        if isinstance(instream, binary_type):
+            instream = instream.decode()
+
+        if isinstance(instream, text_type):
+            instream = StringIO(instream)
+
+        if getattr(instream, 'read', None) is None:
+            raise TypeError('Parser must be a string or character stream, not '
+                            '{itype}'.format(itype=instream.__class__.__name__))
+
+        self.instream = instream
+        self.charstack = []
+        self.tokenstack = []
+        self.eof = False
+
+    def get_token(self):
+        """
+        This function breaks the time string into lexical units (tokens), which
+        can be parsed by the parser. Lexical units are demarcated by changes in
+        the character set, so any continuous string of letters is considered
+        one unit, any continuous string of numbers is considered one unit.
+
+        The main complication arises from the fact that dots ('.') can be used
+        both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
+        "4:30:21.447"). As such, it is necessary to read the full context of
+        any dot-separated strings before breaking it into tokens; as such, this
+        function maintains a "token stack", for when the ambiguous context
+        demands that multiple tokens be parsed at once.
+        """
+        if self.tokenstack:
+            return self.tokenstack.pop(0)
+
+        seenletters = False
+        token = None
+        state = None
+
+        while not self.eof:
+            # We only realize that we've reached the end of a token when we
+            # find a character that's not part of the current token - since
+            # that character may be part of the next token, it's stored in the
+            # charstack.
+            if self.charstack:
+                nextchar = self.charstack.pop(0)
+            else:
+                nextchar = self.instream.read(1)
+                while nextchar == '\x00':
+                    nextchar = self.instream.read(1)
+
+            if not nextchar:
+                self.eof = True
+                break
+            elif not state:
+                # First character of the token - determines if we're starting
+                # to parse a word, a number or something else.
+                token = nextchar
+                if self.isword(nextchar):
+                    state = 'a'
+                elif self.isnum(nextchar):
+                    state = '0'
+                elif self.isspace(nextchar):
+                    token = ' '
+                    break  # emit token
+                else:
+                    break  # emit token
+            elif state == 'a':
+                # If we've already started reading a word, we keep reading
+                # letters until we find something that's not part of a word.
+                seenletters = True
+                if self.isword(nextchar):
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == '0':
+                # If we've already started reading a number, we keep reading
+                # numbers until we find something that doesn't fit.
+                if self.isnum(nextchar):
+                    token += nextchar
+                elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == 'a.':
+                # If we've seen some letters and a dot separator, continue
+                # parsing, and the tokens will be broken up later.
+                seenletters = True
+                if nextchar == '.' or self.isword(nextchar):
+                    token += nextchar
+                elif self.isnum(nextchar) and token[-1] == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+            elif state == '0.':
+                # If we've seen at least one dot separator, keep going, we'll
+                # break up the tokens later.
+                if nextchar == '.' or self.isnum(nextchar):
+                    token += nextchar
+                elif self.isword(nextchar) and token[-1] == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break  # emit token
+
+        if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
+                                       token[-1] in '.,')):
+            l = self._split_decimal.split(token)
+            token = l[0]
+            for tok in l[1:]:
+                if tok:
+                    self.tokenstack.append(tok)
+
+        if state == '0.' and token.count('.') == 0:
+            token = token.replace(',', '.')
+
+        return token
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        token = self.get_token()
+        if token is None:
+            raise StopIteration
+
+        return token
+
+    def next(self):
+        return self.__next__()  # Python 2.x support
+
+    @classmethod
+    def split(cls, s):
+        return list(cls(s))
+
+    @classmethod
+    def isword(cls, nextchar):
+        """ Whether or not the next character is part of a word """
+        return nextchar.isalpha()
+
+    @classmethod
+    def isnum(cls, nextchar):
+        """ Whether the next character is part of a number """
+        return nextchar.isdigit()
+
+    @classmethod
+    def isspace(cls, nextchar):
+        """ Whether the next character is whitespace """
+        return nextchar.isspace()
+
+
+class _resultbase(object):
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def _repr(self, classname):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, repr(value)))
+        return "%s(%s)" % (classname, ", ".join(l))
+
+    def __len__(self):
+        return (sum(getattr(self, attr) is not None
+                    for attr in self.__slots__))
+
+    def __repr__(self):
+        return self._repr(self.__class__.__name__)
+
+
+class parserinfo(object):
+    """
+    Class which handles what inputs are accepted. Subclass this to customize
+    the language and acceptable values for each parameter.
+
+    :param dayfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+            ``yearfirst`` is set to ``True``, this distinguishes between YDM
+            and YMD. Default is ``False``.
+
+    :param yearfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+            to be the year, otherwise the last number is taken to be the year.
+            Default is ``False``.
+    """
+
+    # m from a.m/p.m, t from ISO T separator
+    JUMP = [" ", ".", ",", ";", "-", "/", "'",
+            "at", "on", "and", "ad", "m", "t", "of",
+            "st", "nd", "rd", "th"]
+
+    WEEKDAYS = [("Mon", "Monday"),
+                ("Tue", "Tuesday"),
+                ("Wed", "Wednesday"),
+                ("Thu", "Thursday"),
+                ("Fri", "Friday"),
+                ("Sat", "Saturday"),
+                ("Sun", "Sunday")]
+    MONTHS = [("Jan", "January"),
+              ("Feb", "February"),
+              ("Mar", "March"),
+              ("Apr", "April"),
+              ("May", "May"),
+              ("Jun", "June"),
+              ("Jul", "July"),
+              ("Aug", "August"),
+              ("Sep", "Sept", "September"),
+              ("Oct", "October"),
+              ("Nov", "November"),
+              ("Dec", "December")]
+    HMS = [("h", "hour", "hours"),
+           ("m", "minute", "minutes"),
+           ("s", "second", "seconds")]
+    AMPM = [("am", "a"),
+            ("pm", "p")]
+    UTCZONE = ["UTC", "GMT", "Z"]
+    PERTAIN = ["of"]
+    TZOFFSET = {}
+
+    def __init__(self, dayfirst=False, yearfirst=False):
+        self._jump = self._convert(self.JUMP)
+        self._weekdays = self._convert(self.WEEKDAYS)
+        self._months = self._convert(self.MONTHS)
+        self._hms = self._convert(self.HMS)
+        self._ampm = self._convert(self.AMPM)
+        self._utczone = self._convert(self.UTCZONE)
+        self._pertain = self._convert(self.PERTAIN)
+
+        self.dayfirst = dayfirst
+        self.yearfirst = yearfirst
+
+        self._year = time.localtime().tm_year
+        self._century = self._year // 100 * 100
+
+    def _convert(self, lst):
+        dct = {}
+        for i, v in enumerate(lst):
+            if isinstance(v, tuple):
+                for v in v:
+                    dct[v.lower()] = i
+            else:
+                dct[v.lower()] = i
+        return dct
+
+    def jump(self, name):
+        return name.lower() in self._jump
+
+    def weekday(self, name):
+        if len(name) >= 3:
+            try:
+                return self._weekdays[name.lower()]
+            except KeyError:
+                pass
+        return None
+
+    def month(self, name):
+        if len(name) >= 3:
+            try:
+                return self._months[name.lower()] + 1
+            except KeyError:
+                pass
+        return None
+
+    def hms(self, name):
+        try:
+            return self._hms[name.lower()]
+        except KeyError:
+            return None
+
+    def ampm(self, name):
+        try:
+            return self._ampm[name.lower()]
+        except KeyError:
+            return None
+
+    def pertain(self, name):
+        return name.lower() in self._pertain
+
+    def utczone(self, name):
+        return name.lower() in self._utczone
+
+    def tzoffset(self, name):
+        if name in self._utczone:
+            return 0
+
+        return self.TZOFFSET.get(name)
+
+    def convertyear(self, year, century_specified=False):
+        if year < 100 and not century_specified:
+            year += self._century
+            if abs(year - self._year) >= 50:
+                if year < self._year:
+                    year += 100
+                else:
+                    year -= 100
+        return year
+
+    def validate(self, res):
+        # move to info
+        if res.year is not None:
+            res.year = self.convertyear(res.year, res.century_specified)
+
+        if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
+            res.tzname = "UTC"
+            res.tzoffset = 0
+        elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
+            res.tzoffset = 0
+        return True
+
+
+class _ymd(list):
+    def __init__(self, tzstr, *args, **kwargs):
+        super(self.__class__, self).__init__(*args, **kwargs)
+        self.century_specified = False
+        self.tzstr = tzstr
+
+    @staticmethod
+    def token_could_be_year(token, year):
+        try:
+            return int(token) == year
+        except ValueError:
+            return False
+
+    @staticmethod
+    def find_potential_year_tokens(year, tokens):
+        return [token for token in tokens if _ymd.token_could_be_year(token, year)]
+
+    def find_probable_year_index(self, tokens):
+        """
+        attempt to deduce if a pre 100 year was lost
+         due to padded zeros being taken off
+        """
+        for index, token in enumerate(self):
+            potential_year_tokens = _ymd.find_potential_year_tokens(token, tokens)
+            if len(potential_year_tokens) == 1 and len(potential_year_tokens[0]) > 2:
+                return index
+
+    def append(self, val):
+        if hasattr(val, '__len__'):
+            if val.isdigit() and len(val) > 2:
+                self.century_specified = True
+        elif val > 100:
+            self.century_specified = True
+
+        super(self.__class__, self).append(int(val))
+
+    def resolve_ymd(self, mstridx, yearfirst, dayfirst):
+        len_ymd = len(self)
+        year, month, day = (None, None, None)
+
+        if len_ymd > 3:
+            raise ValueError("More than three YMD values")
+        elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
+            # One member, or two members with a month string
+            if mstridx != -1:
+                month = self[mstridx]
+                del self[mstridx]
+
+            if len_ymd > 1 or mstridx == -1:
+                if self[0] > 31:
+                    year = self[0]
+                else:
+                    day = self[0]
+
+        elif len_ymd == 2:
+            # Two members with numbers
+            if self[0] > 31:
+                # 99-01
+                year, month = self
+            elif self[1] > 31:
+                # 01-99
+                month, year = self
+            elif dayfirst and self[1] <= 12:
+                # 13-01
+                day, month = self
+            else:
+                # 01-13
+                month, day = self
+
+        elif len_ymd == 3:
+            # Three members
+            if mstridx == 0:
+                month, day, year = self
+            elif mstridx == 1:
+                if self[0] > 31 or (yearfirst and self[2] <= 31):
+                    # 99-Jan-01
+                    year, month, day = self
+                else:
+                    # 01-Jan-01
+                    # Give precendence to day-first, since
+                    # two-digit years is usually hand-written.
+                    day, month, year = self
+
+            elif mstridx == 2:
+                # WTF!?
+                if self[1] > 31:
+                    # 01-99-Jan
+                    day, year, month = self
+                else:
+                    # 99-01-Jan
+                    year, day, month = self
+
+            else:
+                if self[0] > 31 or \
+                    self.find_probable_year_index(_timelex.split(self.tzstr)) == 0 or \
+                   (yearfirst and self[1] <= 12 and self[2] <= 31):
+                    # 99-01-01
+                    if dayfirst and self[2] <= 12:
+                        year, day, month = self
+                    else:
+                        year, month, day = self
+                elif self[0] > 12 or (dayfirst and self[1] <= 12):
+                    # 13-01-01
+                    day, month, year = self
+                else:
+                    # 01-13-01
+                    month, day, year = self
+
+        return year, month, day
+
+
+class parser(object):
+    def __init__(self, info=None):
+        self.info = info or parserinfo()
+
+    def parse(self, timestr, default=None, ignoretz=False, tzinfos=None, **kwargs):
+        """
+        Parse the date/time string into a :class:`datetime.datetime` object.
+
+        :param timestr:
+            Any date/time string using the supported formats.
+
+        :param default:
+            The default datetime object, if this is a datetime object and not
+            ``None``, elements specified in ``timestr`` replace elements in the
+            default object.
+
+        :param ignoretz:
+            If set ``True``, time zones in parsed strings are ignored and a
+            naive :class:`datetime.datetime` object is returned.
+
+        :param tzinfos:
+            Additional time zone names / aliases which may be present in the
+            string. This argument maps time zone names (and optionally offsets
+            from those time zones) to time zones. This parameter can be a
+            dictionary with timezone aliases mapping time zone names to time
+            zones or a function taking two parameters (``tzname`` and
+            ``tzoffset``) and returning a time zone.
+
+            The timezones to which the names are mapped can be an integer
+            offset from UTC in minutes or a :class:`tzinfo` object.
+
+            .. doctest::
+               :options: +NORMALIZE_WHITESPACE
+
+                >>> from dateutil.parser import parse
+                >>> from dateutil.tz import gettz
+                >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")}
+                >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800))
+                >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21,
+                                  tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+            This parameter is ignored if ``ignoretz`` is set.
+
+        :param **kwargs:
+            Keyword arguments as passed to ``_parse()``.
+
+        :return:
+            Returns a :class:`datetime.datetime` object or, if the
+            ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+            first element being a :class:`datetime.datetime` object, the second
+            a tuple containing the fuzzy tokens.
+
+        :raises ValueError:
+            Raised for invalid or unknown string format, if the provided
+            :class:`tzinfo` is not in a valid format, or if an invalid date
+            would be created.
+
+        :raises OverflowError:
+            Raised if the parsed date exceeds the largest valid C integer on
+            your system.
+        """
+
+        if default is None:
+            effective_dt = datetime.datetime.now()
+            default = datetime.datetime.now().replace(hour=0, minute=0,
+                                                      second=0, microsecond=0)
+        else:
+            effective_dt = default
+
+        res, skipped_tokens = self._parse(timestr, **kwargs)
+
+        if res is None:
+            raise ValueError("Unknown string format")
+
+        if len(res) == 0:
+            raise ValueError("String does not contain a date.")
+
+        repl = {}
+        for attr in ("year", "month", "day", "hour",
+                     "minute", "second", "microsecond"):
+            value = getattr(res, attr)
+            if value is not None:
+                repl[attr] = value
+
+        if 'day' not in repl:
+            # If the default day exceeds the last day of the month, fall back to
+            # the end of the month.
+            cyear = default.year if res.year is None else res.year
+            cmonth = default.month if res.month is None else res.month
+            cday = default.day if res.day is None else res.day
+
+            if cday > monthrange(cyear, cmonth)[1]:
+                repl['day'] = monthrange(cyear, cmonth)[1]
+
+        ret = default.replace(**repl)
+
+        if res.weekday is not None and not res.day:
+            ret = ret+relativedelta.relativedelta(weekday=res.weekday)
+
+        if not ignoretz:
+            if (isinstance(tzinfos, collections.Callable) or
+                    tzinfos and res.tzname in tzinfos):
+
+                if isinstance(tzinfos, collections.Callable):
+                    tzdata = tzinfos(res.tzname, res.tzoffset)
+                else:
+                    tzdata = tzinfos.get(res.tzname)
+
+                if isinstance(tzdata, datetime.tzinfo):
+                    tzinfo = tzdata
+                elif isinstance(tzdata, text_type):
+                    tzinfo = tz.tzstr(tzdata)
+                elif isinstance(tzdata, integer_types):
+                    tzinfo = tz.tzoffset(res.tzname, tzdata)
+                else:
+                    raise ValueError("Offset must be tzinfo subclass, "
+                                     "tz string, or int offset.")
+                ret = ret.replace(tzinfo=tzinfo)
+            elif res.tzname and res.tzname in time.tzname:
+                ret = ret.replace(tzinfo=tz.tzlocal())
+            elif res.tzoffset == 0:
+                ret = ret.replace(tzinfo=tz.tzutc())
+            elif res.tzoffset:
+                ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+
+        if kwargs.get('fuzzy_with_tokens', False):
+            return ret, skipped_tokens
+        else:
+            return ret
+
+    class _result(_resultbase):
+        __slots__ = ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond",
+                     "tzname", "tzoffset", "ampm"]
+
+    def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
+               fuzzy_with_tokens=False):
+        """
+        Private method which performs the heavy lifting of parsing, called from
+        ``parse()``, which passes on its ``kwargs`` to this function.
+
+        :param timestr:
+            The string to parse.
+
+        :param dayfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+            ``yearfirst`` is set to ``True``, this distinguishes between YDM
+            and YMD. If set to ``None``, this value is retrieved from the
+            current :class:`parserinfo` object (which itself defaults to
+            ``False``).
+
+        :param yearfirst:
+            Whether to interpret the first value in an ambiguous 3-integer date
+            (e.g. 01/05/09) as the year. If ``True``, the first number is taken
+            to be the year, otherwise the last number is taken to be the year.
+            If this is set to ``None``, the value is retrieved from the current
+            :class:`parserinfo` object (which itself defaults to ``False``).
+
+        :param fuzzy:
+            Whether to allow fuzzy parsing, allowing for string like "Today is
+            January 1, 2047 at 8:21:00AM".
+
+        :param fuzzy_with_tokens:
+            If ``True``, ``fuzzy`` is automatically set to True, and the parser
+            will return a tuple where the first element is the parsed
+            :class:`datetime.datetime` datetimestamp and the second element is
+            a tuple containing the portions of the string which were ignored:
+
+            .. doctest::
+
+                >>> from dateutil.parser import parse
+                >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+                (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+        """
+        if fuzzy_with_tokens:
+            fuzzy = True
+
+        info = self.info
+
+        if dayfirst is None:
+            dayfirst = info.dayfirst
+
+        if yearfirst is None:
+            yearfirst = info.yearfirst
+
+        res = self._result()
+        l = _timelex.split(timestr)         # Splits the timestr into tokens
+
+        # keep up with the last token skipped so we can recombine
+        # consecutively skipped tokens (-2 for when i begins at 0).
+        last_skipped_token_i = -2
+        skipped_tokens = list()
+
+        try:
+            # year/month/day list
+            ymd = _ymd(timestr)
+
+            # Index of the month string in ymd
+            mstridx = -1
+
+            len_l = len(l)
+            i = 0
+            while i < len_l:
+
+                # Check if it's a number
+                try:
+                    value_repr = l[i]
+                    value = float(value_repr)
+                except ValueError:
+                    value = None
+
+                if value is not None:
+                    # Token is a number
+                    len_li = len(l[i])
+                    i += 1
+
+                    if (len(ymd) == 3 and len_li in (2, 4)
+                        and res.hour is None and (i >= len_l or (l[i] != ':' and
+                                                  info.hms(l[i]) is None))):
+                        # 19990101T23[59]
+                        s = l[i-1]
+                        res.hour = int(s[:2])
+
+                        if len_li == 4:
+                            res.minute = int(s[2:])
+
+                    elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
+                        # YYMMDD or HHMMSS[.ss]
+                        s = l[i-1]
+
+                        if not ymd and l[i-1].find('.') == -1:
+                            #ymd.append(info.convertyear(int(s[:2])))
+
+                            ymd.append(s[:2])
+                            ymd.append(s[2:4])
+                            ymd.append(s[4:])
+                        else:
+                            # 19990101T235959[.59]
+                            res.hour = int(s[:2])
+                            res.minute = int(s[2:4])
+                            res.second, res.microsecond = _parsems(s[4:])
+
+                    elif len_li in (8, 12, 14):
+                        # YYYYMMDD
+                        s = l[i-1]
+                        ymd.append(s[:4])
+                        ymd.append(s[4:6])
+                        ymd.append(s[6:8])
+
+                        if len_li > 8:
+                            res.hour = int(s[8:10])
+                            res.minute = int(s[10:12])
+
+                            if len_li > 12:
+                                res.second = int(s[12:])
+
+                    elif ((i < len_l and info.hms(l[i]) is not None) or
+                          (i+1 < len_l and l[i] == ' ' and
+                           info.hms(l[i+1]) is not None)):
+
+                        # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+                        if l[i] == ' ':
+                            i += 1
+
+                        idx = info.hms(l[i])
+
+                        while True:
+                            if idx == 0:
+                                res.hour = int(value)
+
+                                if value % 1:
+                                    res.minute = int(60*(value % 1))
+
+                            elif idx == 1:
+                                res.minute = int(value)
+
+                                if value % 1:
+                                    res.second = int(60*(value % 1))
+
+                            elif idx == 2:
+                                res.second, res.microsecond = \
+                                    _parsems(value_repr)
+
+                            i += 1
+
+                            if i >= len_l or idx == 2:
+                                break
+
+                            # 12h00
+                            try:
+                                value_repr = l[i]
+                                value = float(value_repr)
+                            except ValueError:
+                                break
+                            else:
+                                i += 1
+                                idx += 1
+
+                                if i < len_l:
+                                    newidx = info.hms(l[i])
+
+                                    if newidx is not None:
+                                        idx = newidx
+
+                    elif (i == len_l and l[i-2] == ' ' and
+                          info.hms(l[i-3]) is not None):
+                        # X h MM or X m SS
+                        idx = info.hms(l[i-3]) + 1
+
+                        if idx == 1:
+                            res.minute = int(value)
+
+                            if value % 1:
+                                res.second = int(60*(value % 1))
+                            elif idx == 2:
+                                res.second, res.microsecond = \
+                                    _parsems(value_repr)
+                                i += 1
+
+                    elif i+1 < len_l and l[i] == ':':
+                        # HH:MM[:SS[.ss]]
+                        res.hour = int(value)
+                        i += 1
+                        value = float(l[i])
+                        res.minute = int(value)
+
+                        if value % 1:
+                            res.second = int(60*(value % 1))
+
+                        i += 1
+
+                        if i < len_l and l[i] == ':':
+                            res.second, res.microsecond = _parsems(l[i+1])
+                            i += 2
+
+                    elif i < len_l and l[i] in ('-', '/', '.'):
+                        sep = l[i]
+                        ymd.append(value_repr)
+                        i += 1
+
+                        if i < len_l and not info.jump(l[i]):
+                            try:
+                                # 01-01[-01]
+                                ymd.append(l[i])
+                            except ValueError:
+                                # 01-Jan[-01]
+                                value = info.month(l[i])
+
+                                if value is not None:
+                                    ymd.append(value)
+                                    assert mstridx == -1
+                                    mstridx = len(ymd)-1
+                                else:
+                                    return None, None
+
+                            i += 1
+
+                            if i < len_l and l[i] == sep:
+                                # We have three members
+                                i += 1
+                                value = info.month(l[i])
+
+                                if value is not None:
+                                    ymd.append(value)
+                                    mstridx = len(ymd)-1
+                                    assert mstridx == -1
+                                else:
+                                    ymd.append(l[i])
+
+                                i += 1
+                    elif i >= len_l or info.jump(l[i]):
+                        if i+1 < len_l and info.ampm(l[i+1]) is not None:
+                            # 12 am
+                            res.hour = int(value)
+
+                            if res.hour < 12 and info.ampm(l[i+1]) == 1:
+                                res.hour += 12
+                            elif res.hour == 12 and info.ampm(l[i+1]) == 0:
+                                res.hour = 0
+
+                            i += 1
+                        else:
+                            # Year, month or day
+                            ymd.append(value)
+                        i += 1
+                    elif info.ampm(l[i]) is not None:
+
+                        # 12am
+                        res.hour = int(value)
+
+                        if res.hour < 12 and info.ampm(l[i]) == 1:
+                            res.hour += 12
+                        elif res.hour == 12 and info.ampm(l[i]) == 0:
+                            res.hour = 0
+                        i += 1
+
+                    elif not fuzzy:
+                        return None, None
+                    else:
+                        i += 1
+                    continue
+
+                # Check weekday
+                value = info.weekday(l[i])
+                if value is not None:
+                    res.weekday = value
+                    i += 1
+                    continue
+
+                # Check month name
+                value = info.month(l[i])
+                if value is not None:
+                    ymd.append(value)
+                    assert mstridx == -1
+                    mstridx = len(ymd)-1
+
+                    i += 1
+                    if i < len_l:
+                        if l[i] in ('-', '/'):
+                            # Jan-01[-99]
+                            sep = l[i]
+                            i += 1
+                            ymd.append(l[i])
+                            i += 1
+
+                            if i < len_l and l[i] == sep:
+                                # Jan-01-99
+                                i += 1
+                                ymd.append(l[i])
+                                i += 1
+
+                        elif (i+3 < len_l and l[i] == l[i+2] == ' '
+                              and info.pertain(l[i+1])):
+                            # Jan of 01
+                            # In this case, 01 is clearly year
+                            try:
+                                value = int(l[i+3])
+                            except ValueError:
+                                # Wrong guess
+                                pass
+                            else:
+                                # Convert it here to become unambiguous
+                                ymd.append(str(info.convertyear(value)))
+                            i += 4
+                    continue
+
+                # Check am/pm
+                value = info.ampm(l[i])
+                if value is not None:
+                    # For fuzzy parsing, 'a' or 'am' (both valid English words)
+                    # may erroneously trigger the AM/PM flag. Deal with that
+                    # here.
+                    val_is_ampm = True
+
+                    # If there's already an AM/PM flag, this one isn't one.
+                    if fuzzy and res.ampm is not None:
+                        val_is_ampm = False
+
+                    # If AM/PM is found and hour is not, raise a ValueError
+                    if res.hour is None:
+                        if fuzzy:
+                            val_is_ampm = False
+                        else:
+                            raise ValueError('No hour specified with ' +
+                                             'AM or PM flag.')
+                    elif not 0 <= res.hour <= 12:
+                        # If AM/PM is found, it's a 12 hour clock, so raise 
+                        # an error for invalid range
+                        if fuzzy:
+                            val_is_ampm = False
+                        else:
+                            raise ValueError('Invalid hour specified for ' +
+                                             '12-hour clock.')
+
+                    if val_is_ampm:
+                        if value == 1 and res.hour < 12:
+                            res.hour += 12
+                        elif value == 0 and res.hour == 12:
+                            res.hour = 0
+
+                        res.ampm = value
+
+                    i += 1
+                    continue
+
+                # Check for a timezone name
+                if (res.hour is not None and len(l[i]) <= 5 and
+                        res.tzname is None and res.tzoffset is None and
+                        not [x for x in l[i] if x not in
+                             string.ascii_uppercase]):
+                    res.tzname = l[i]
+                    res.tzoffset = info.tzoffset(res.tzname)
+                    i += 1
+
+                    # Check for something like GMT+3, or BRST+3. Notice
+                    # that it doesn't mean "I am 3 hours after GMT", but
+                    # "my time +3 is GMT". If found, we reverse the
+                    # logic so that timezone parsing code will get it
+                    # right.
+                    if i < len_l and l[i] in ('+', '-'):
+                        l[i] = ('+', '-')[l[i] == '+']
+                        res.tzoffset = None
+                        if info.utczone(res.tzname):
+                            # With something like GMT+3, the timezone
+                            # is *not* GMT.
+                            res.tzname = None
+
+                    continue
+
+                # Check for a numbered timezone
+                if res.hour is not None and l[i] in ('+', '-'):
+                    signal = (-1, 1)[l[i] == '+']
+                    i += 1
+                    len_li = len(l[i])
+
+                    if len_li == 4:
+                        # -0300
+                        res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
+                    elif i+1 < len_l and l[i+1] == ':':
+                        # -03:00
+                        res.tzoffset = int(l[i])*3600+int(l[i+2])*60
+                        i += 2
+                    elif len_li <= 2:
+                        # -[0]3
+                        res.tzoffset = int(l[i][:2])*3600
+                    else:
+                        return None, None
+                    i += 1
+
+                    res.tzoffset *= signal
+
+                    # Look for a timezone name between parenthesis
+                    if (i+3 < len_l and
+                        info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
+                        3 <= len(l[i+2]) <= 5 and
+                        not [x for x in l[i+2]
+                             if x not in string.ascii_uppercase]):
+                        # -0300 (BRST)
+                        res.tzname = l[i+2]
+                        i += 4
+                    continue
+
+                # Check jumps
+                if not (info.jump(l[i]) or fuzzy):
+                    return None, None
+
+                if last_skipped_token_i == i - 1:
+                    # recombine the tokens
+                    skipped_tokens[-1] += l[i]
+                else:
+                    # just append
+                    skipped_tokens.append(l[i])
+                last_skipped_token_i = i
+                i += 1
+
+            # Process year/month/day
+            year, month, day = ymd.resolve_ymd(mstridx, yearfirst, dayfirst)
+            if year is not None:
+                res.year = year
+                res.century_specified = ymd.century_specified
+
+            if month is not None:
+                res.month = month
+
+            if day is not None:
+                res.day = day
+
+        except (IndexError, ValueError, AssertionError):
+            return None, None
+
+        if not info.validate(res):
+            return None, None
+
+        if fuzzy_with_tokens:
+            return res, tuple(skipped_tokens)
+        else:
+            return res, None
+
+DEFAULTPARSER = parser()
+
+
+def parse(timestr, parserinfo=None, **kwargs):
+    """
+
+    Parse a string in one of the supported formats, using the
+    ``parserinfo`` parameters.
+
+    :param timestr:
+        A string containing a date/time stamp.
+
+    :param parserinfo:
+        A :class:`parserinfo` object containing parameters for the parser.
+        If ``None``, the default arguments to the :class:`parserinfo`
+        constructor are used.
+
+    The ``**kwargs`` parameter takes the following keyword arguments:
+
+    :param default:
+        The default datetime object, if this is a datetime object and not
+        ``None``, elements specified in ``timestr`` replace elements in the
+        default object.
+
+    :param ignoretz:
+        If set ``True``, time zones in parsed strings are ignored and a naive
+        :class:`datetime` object is returned.
+
+    :param tzinfos:
+            Additional time zone names / aliases which may be present in the
+            string. This argument maps time zone names (and optionally offsets
+            from those time zones) to time zones. This parameter can be a
+            dictionary with timezone aliases mapping time zone names to time
+            zones or a function taking two parameters (``tzname`` and
+            ``tzoffset``) and returning a time zone.
+
+            The timezones to which the names are mapped can be an integer
+            offset from UTC in minutes or a :class:`tzinfo` object.
+
+            .. doctest::
+               :options: +NORMALIZE_WHITESPACE
+
+                >>> from dateutil.parser import parse
+                >>> from dateutil.tz import gettz
+                >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")}
+                >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800))
+                >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
+                datetime.datetime(2012, 1, 19, 17, 21,
+                                  tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
+
+            This parameter is ignored if ``ignoretz`` is set.
+
+    :param dayfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the day (``True``) or month (``False``). If
+        ``yearfirst`` is set to ``True``, this distinguishes between YDM and
+        YMD. If set to ``None``, this value is retrieved from the current
+        :class:`parserinfo` object (which itself defaults to ``False``).
+
+    :param yearfirst:
+        Whether to interpret the first value in an ambiguous 3-integer date
+        (e.g. 01/05/09) as the year. If ``True``, the first number is taken to
+        be the year, otherwise the last number is taken to be the year. If
+        this is set to ``None``, the value is retrieved from the current
+        :class:`parserinfo` object (which itself defaults to ``False``).
+
+    :param fuzzy:
+        Whether to allow fuzzy parsing, allowing for string like "Today is
+        January 1, 2047 at 8:21:00AM".
+
+    :param fuzzy_with_tokens:
+        If ``True``, ``fuzzy`` is automatically set to True, and the parser
+        will return a tuple where the first element is the parsed
+        :class:`datetime.datetime` datetimestamp and the second element is
+        a tuple containing the portions of the string which were ignored:
+
+        .. doctest::
+
+            >>> from dateutil.parser import parse
+            >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
+            (datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
+
+    :return:
+        Returns a :class:`datetime.datetime` object or, if the
+        ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
+        first element being a :class:`datetime.datetime` object, the second
+        a tuple containing the fuzzy tokens.
+
+    :raises ValueError:
+        Raised for invalid or unknown string format, if the provided
+        :class:`tzinfo` is not in a valid format, or if an invalid date
+        would be created.
+
+    :raises OverflowError:
+        Raised if the parsed date exceeds the largest valid C integer on
+        your system.
+    """
+    if parserinfo:
+        return parser(parserinfo).parse(timestr, **kwargs)
+    else:
+        return DEFAULTPARSER.parse(timestr, **kwargs)
+
+
+class _tzparser(object):
+
+    class _result(_resultbase):
+
+        __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
+                     "start", "end"]
+
+        class _attr(_resultbase):
+            __slots__ = ["month", "week", "weekday",
+                         "yday", "jyday", "day", "time"]
+
+        def __repr__(self):
+            return self._repr("")
+
+        def __init__(self):
+            _resultbase.__init__(self)
+            self.start = self._attr()
+            self.end = self._attr()
+
+    def parse(self, tzstr):
+        res = self._result()
+        l = _timelex.split(tzstr)
+        try:
+
+            len_l = len(l)
+
+            i = 0
+            while i < len_l:
+                # BRST+3[BRDT[+2]]
+                j = i
+                while j < len_l and not [x for x in l[j]
+                                         if x in "0123456789:,-+"]:
+                    j += 1
+                if j != i:
+                    if not res.stdabbr:
+                        offattr = "stdoffset"
+                        res.stdabbr = "".join(l[i:j])
+                    else:
+                        offattr = "dstoffset"
+                        res.dstabbr = "".join(l[i:j])
+                    i = j
+                    if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
+                                       "0123456789")):
+                        if l[i] in ('+', '-'):
+                            # Yes, that's right.  See the TZ variable
+                            # documentation.
+                            signal = (1, -1)[l[i] == '+']
+                            i += 1
+                        else:
+                            signal = -1
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            setattr(res, offattr, (int(l[i][:2])*3600 +
+                                                   int(l[i][2:])*60)*signal)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            setattr(res, offattr,
+                                    (int(l[i])*3600+int(l[i+2])*60)*signal)
+                            i += 2
+                        elif len_li <= 2:
+                            # -[0]3
+                            setattr(res, offattr,
+                                    int(l[i][:2])*3600*signal)
+                        else:
+                            return None
+                        i += 1
+                    if res.dstabbr:
+                        break
+                else:
+                    break
+
+            if i < len_l:
+                for j in range(i, len_l):
+                    if l[j] == ';':
+                        l[j] = ','
+
+                assert l[i] == ','
+
+                i += 1
+
+            if i >= len_l:
+                pass
+            elif (8 <= l.count(',') <= 9 and
+                  not [y for x in l[i:] if x != ','
+                       for y in x if y not in "0123456789"]):
+                # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
+                for x in (res.start, res.end):
+                    x.month = int(l[i])
+                    i += 2
+                    if l[i] == '-':
+                        value = int(l[i+1])*-1
+                        i += 1
+                    else:
+                        value = int(l[i])
+                    i += 2
+                    if value:
+                        x.week = value
+                        x.weekday = (int(l[i])-1) % 7
+                    else:
+                        x.day = int(l[i])
+                    i += 2
+                    x.time = int(l[i])
+                    i += 2
+                if i < len_l:
+                    if l[i] in ('-', '+'):
+                        signal = (-1, 1)[l[i] == "+"]
+                        i += 1
+                    else:
+                        signal = 1
+                    res.dstoffset = (res.stdoffset+int(l[i]))*signal
+            elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
+                  not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
+                                                     '.', '-', ':')
+                       for y in x if y not in "0123456789"]):
+                for x in (res.start, res.end):
+                    if l[i] == 'J':
+                        # non-leap year day (1 based)
+                        i += 1
+                        x.jyday = int(l[i])
+                    elif l[i] == 'M':
+                        # month[-.]week[-.]weekday
+                        i += 1
+                        x.month = int(l[i])
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.week = int(l[i])
+                        if x.week == 5:
+                            x.week = -1
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.weekday = (int(l[i])-1) % 7
+                    else:
+                        # year day (zero based)
+                        x.yday = int(l[i])+1
+
+                    i += 1
+
+                    if i < len_l and l[i] == '/':
+                        i += 1
+                        # start time
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            x.time = int(l[i])*3600+int(l[i+2])*60
+                            i += 2
+                            if i+1 < len_l and l[i+1] == ':':
+                                i += 2
+                                x.time += int(l[i])
+                        elif len_li <= 2:
+                            # -[0]3
+                            x.time = (int(l[i][:2])*3600)
+                        else:
+                            return None
+                        i += 1
+
+                    assert i == len_l or l[i] == ','
+
+                    i += 1
+
+                assert i >= len_l
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+
+        return res
+
+
+DEFAULTTZPARSER = _tzparser()
+
+
+def _parsetz(tzstr):
+    return DEFAULTTZPARSER.parse(tzstr)
+
+
+def _parsems(value):
+    """Parse a I[.F] seconds value into (seconds, microseconds)."""
+    if "." not in value:
+        return int(value), 0
+    else:
+        i, f = value.split(".")
+        return int(i), int(f.ljust(6, "0")[:6])
+
+
+# vim:ts=4:sw=4:et
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/relativedelta.py
@@ -0,0 +1,531 @@
+# -*- coding: utf-8 -*-
+import datetime
+import calendar
+
+import operator
+from math import copysign
+
+from six import integer_types
+from warnings import warn
+
+from ._common import weekday
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+
+class relativedelta(object):
+    """
+    The relativedelta type is based on the specification of the excellent
+    work done by M.-A. Lemburg in his
+    `mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ extension.
+    However, notice that this type does *NOT* implement the same algorithm as
+    his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
+
+    There are two different ways to build a relativedelta instance. The
+    first one is passing it two date/datetime classes::
+
+        relativedelta(datetime1, datetime2)
+
+    The second one is passing it any number of the following keyword arguments::
+
+        relativedelta(arg1=x,arg2=y,arg3=z...)
+
+        year, month, day, hour, minute, second, microsecond:
+            Absolute information (argument is singular); adding or subtracting a
+            relativedelta with absolute information does not perform an aritmetic
+            operation, but rather REPLACES the corresponding value in the
+            original datetime with the value(s) in relativedelta.
+
+        years, months, weeks, days, hours, minutes, seconds, microseconds:
+            Relative information, may be negative (argument is plural); adding
+            or subtracting a relativedelta with relative information performs
+            the corresponding aritmetic operation on the original datetime value
+            with the information in the relativedelta.
+
+        weekday:
+            One of the weekday instances (MO, TU, etc). These instances may
+            receive a parameter N, specifying the Nth weekday, which could
+            be positive or negative (like MO(+1) or MO(-2). Not specifying
+            it is the same as specifying +1. You can also use an integer,
+            where 0=MO.
+
+        leapdays:
+            Will add given days to the date found, if year is a leap
+            year, and the date found is post 28 of february.
+
+        yearday, nlyearday:
+            Set the yearday or the non-leap year day (jump leap days).
+            These are converted to day/month/leapdays information.
+
+    Here is the behavior of operations with relativedelta:
+
+    1. Calculate the absolute year, using the 'year' argument, or the
+       original datetime year, if the argument is not present.
+
+    2. Add the relative 'years' argument to the absolute year.
+
+    3. Do steps 1 and 2 for month/months.
+
+    4. Calculate the absolute day, using the 'day' argument, or the
+       original datetime day, if the argument is not present. Then,
+       subtract from the day until it fits in the year and month
+       found after their operations.
+
+    5. Add the relative 'days' argument to the absolute day. Notice
+       that the 'weeks' argument is multiplied by 7 and added to
+       'days'.
+
+    6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
+       microsecond/microseconds.
+
+    7. If the 'weekday' argument is present, calculate the weekday,
+       with the given (wday, nth) tuple. wday is the index of the
+       weekday (0-6, 0=Mon), and nth is the number of weeks to add
+       forward or backward, depending on its signal. Notice that if
+       the calculated date is already Monday, for example, using
+       (0, 1) or (0, -1) won't change the day.
+    """
+
+    def __init__(self, dt1=None, dt2=None,
+                 years=0, months=0, days=0, leapdays=0, weeks=0,
+                 hours=0, minutes=0, seconds=0, microseconds=0,
+                 year=None, month=None, day=None, weekday=None,
+                 yearday=None, nlyearday=None,
+                 hour=None, minute=None, second=None, microsecond=None):
+
+        # Check for non-integer values in integer-only quantities
+        if any(x is not None and x != int(x) for x in (years, months)):
+            raise ValueError("Non-integer years and months are "
+                             "ambiguous and not currently supported.")
+
+        if dt1 and dt2:
+            # datetime is a subclass of date. So both must be date
+            if not (isinstance(dt1, datetime.date) and
+                    isinstance(dt2, datetime.date)):
+                raise TypeError("relativedelta only diffs datetime/date")
+
+            # We allow two dates, or two datetimes, so we coerce them to be
+            # of the same type
+            if (isinstance(dt1, datetime.datetime) !=
+                    isinstance(dt2, datetime.datetime)):
+                if not isinstance(dt1, datetime.datetime):
+                    dt1 = datetime.datetime.fromordinal(dt1.toordinal())
+                elif not isinstance(dt2, datetime.datetime):
+                    dt2 = datetime.datetime.fromordinal(dt2.toordinal())
+
+            self.years = 0
+            self.months = 0
+            self.days = 0
+            self.leapdays = 0
+            self.hours = 0
+            self.minutes = 0
+            self.seconds = 0
+            self.microseconds = 0
+            self.year = None
+            self.month = None
+            self.day = None
+            self.weekday = None
+            self.hour = None
+            self.minute = None
+            self.second = None
+            self.microsecond = None
+            self._has_time = 0
+
+            # Get year / month delta between the two
+            months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
+            self._set_months(months)
+
+            # Remove the year/month delta so the timedelta is just well-defined
+            # time units (seconds, days and microseconds)
+            dtm = self.__radd__(dt2)
+
+            # If we've overshot our target, make an adjustment
+            if dt1 < dt2:
+                compare = operator.gt
+                increment = 1
+            else:
+                compare = operator.lt
+                increment = -1
+
+            while compare(dt1, dtm):
+                months += increment
+                self._set_months(months)
+                dtm = self.__radd__(dt2)
+
+            # Get the timedelta between the "months-adjusted" date and dt1
+            delta = dt1 - dtm
+            self.seconds = delta.seconds + delta.days * 86400
+            self.microseconds = delta.microseconds
+        else:
+            # Relative information
+            self.years = years
+            self.months = months
+            self.days = days + weeks * 7
+            self.leapdays = leapdays
+            self.hours = hours
+            self.minutes = minutes
+            self.seconds = seconds
+            self.microseconds = microseconds
+
+            # Absolute information
+            self.year = year
+            self.month = month
+            self.day = day
+            self.hour = hour
+            self.minute = minute
+            self.second = second
+            self.microsecond = microsecond
+
+            if any(x is not None and int(x) != x
+                   for x in (year, month, day, hour,
+                             minute, second, microsecond)):
+                # For now we'll deprecate floats - later it'll be an error.
+                warn("Non-integer value passed as absolute information. " +
+                     "This is not a well-defined condition and will raise " +
+                     "errors in future versions.", DeprecationWarning)
+
+
+            if isinstance(weekday, integer_types):
+                self.weekday = weekdays[weekday]
+            else:
+                self.weekday = weekday
+
+            yday = 0
+            if nlyearday:
+                yday = nlyearday
+            elif yearday:
+                yday = yearday
+                if yearday > 59:
+                    self.leapdays = -1
+            if yday:
+                ydayidx = [31, 59, 90, 120, 151, 181, 212,
+                           243, 273, 304, 334, 366]
+                for idx, ydays in enumerate(ydayidx):
+                    if yday <= ydays:
+                        self.month = idx+1
+                        if idx == 0:
+                            self.day = yday
+                        else:
+                            self.day = yday-ydayidx[idx-1]
+                        break
+                else:
+                    raise ValueError("invalid year day (%d)" % yday)
+
+        self._fix()
+
+    def _fix(self):
+        if abs(self.microseconds) > 999999:
+            s = _sign(self.microseconds)
+            div, mod = divmod(self.microseconds * s, 1000000)
+            self.microseconds = mod * s
+            self.seconds += div * s
+        if abs(self.seconds) > 59:
+            s = _sign(self.seconds)
+            div, mod = divmod(self.seconds * s, 60)
+            self.seconds = mod * s
+            self.minutes += div * s
+        if abs(self.minutes) > 59:
+            s = _sign(self.minutes)
+            div, mod = divmod(self.minutes * s, 60)
+            self.minutes = mod * s
+            self.hours += div * s
+        if abs(self.hours) > 23:
+            s = _sign(self.hours)
+            div, mod = divmod(self.hours * s, 24)
+            self.hours = mod * s
+            self.days += div * s
+        if abs(self.months) > 11:
+            s = _sign(self.months)
+            div, mod = divmod(self.months * s, 12)
+            self.months = mod * s
+            self.years += div * s
+        if (self.hours or self.minutes or self.seconds or self.microseconds
+                or self.hour is not None or self.minute is not None or
+                self.second is not None or self.microsecond is not None):
+            self._has_time = 1
+        else:
+            self._has_time = 0
+
+    @property
+    def weeks(self):
+        return self.days // 7
+    @weeks.setter
+    def weeks(self, value):
+        self.days = self.days - (self.weeks * 7) + value * 7
+
+    def _set_months(self, months):
+        self.months = months
+        if abs(self.months) > 11:
+            s = _sign(self.months)
+            div, mod = divmod(self.months * s, 12)
+            self.months = mod * s
+            self.years = div * s
+        else:
+            self.years = 0
+
+    def normalized(self):
+        """
+        Return a version of this object represented entirely using integer
+        values for the relative attributes.
+
+        >>> relativedelta(days=1.5, hours=2).normalized()
+        relativedelta(days=1, hours=14)
+
+        :return:
+            Returns a :class:`dateutil.relativedelta.relativedelta` object.
+        """
+        # Cascade remainders down (rounding each to roughly nearest microsecond)
+        days = int(self.days)
+
+        hours_f = round(self.hours + 24 * (self.days - days), 11)
+        hours = int(hours_f)
+
+        minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
+        minutes = int(minutes_f)
+
+        seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
+        seconds = int(seconds_f)
+
+        microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
+
+        # Constructor carries overflow back up with call to _fix()
+        return self.__class__(years=self.years, months=self.months,
+                              days=days, hours=hours, minutes=minutes,
+                              seconds=seconds, microseconds=microseconds,
+                              leapdays=self.leapdays, year=self.year,
+                              month=self.month, day=self.day,
+                              weekday=self.weekday, hour=self.hour,
+                              minute=self.minute, second=self.second,
+                              microsecond=self.microsecond)
+
+    def __add__(self, other):
+        if isinstance(other, relativedelta):
+            return self.__class__(years=other.years + self.years,
+                                 months=other.months + self.months,
+                                 days=other.days + self.days,
+                                 hours=other.hours + self.hours,
+                                 minutes=other.minutes + self.minutes,
+                                 seconds=other.seconds + self.seconds,
+                                 microseconds=(other.microseconds +
+                                               self.microseconds),
+                                 leapdays=other.leapdays or self.leapdays,
+                                 year=other.year or self.year,
+                                 month=other.month or self.month,
+                                 day=other.day or self.day,
+                                 weekday=other.weekday or self.weekday,
+                                 hour=other.hour or self.hour,
+                                 minute=other.minute or self.minute,
+                                 second=other.second or self.second,
+                                 microsecond=(other.microsecond or
+                                              self.microsecond))
+        if isinstance(other, datetime.timedelta):
+            return self.__class__(years=self.years,
+                                  months=self.months,
+                                  days=self.days + other.days,
+                                  hours=self.hours,
+                                  minutes=self.minutes,
+                                  seconds=self.seconds + other.seconds,
+                                  microseconds=self.microseconds + other.microseconds,
+                                  leapdays=self.leapdays,
+                                  year=self.year,
+                                  month=self.month,
+                                  day=self.day,
+                                  weekday=self.weekday,
+                                  hour=self.hour,
+                                  minute=self.minute,
+                                  second=self.second,
+                                  microsecond=self.microsecond)
+        if not isinstance(other, datetime.date):
+            return NotImplemented
+        elif self._has_time and not isinstance(other, datetime.datetime):
+            other = datetime.datetime.fromordinal(other.toordinal())
+        year = (self.year or other.year)+self.years
+        month = self.month or other.month
+        if self.months:
+            assert 1 <= abs(self.months) <= 12
+            month += self.months
+            if month > 12:
+                year += 1
+                month -= 12
+            elif month < 1:
+                year -= 1
+                month += 12
+        day = min(calendar.monthrange(year, month)[1],
+                  self.day or other.day)
+        repl = {"year": year, "month": month, "day": day}
+        for attr in ["hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                repl[attr] = value
+        days = self.days
+        if self.leapdays and month > 2 and calendar.isleap(year):
+            days += self.leapdays
+        ret = (other.replace(**repl)
+               + datetime.timedelta(days=days,
+                                    hours=self.hours,
+                                    minutes=self.minutes,
+                                    seconds=self.seconds,
+                                    microseconds=self.microseconds))
+        if self.weekday:
+            weekday, nth = self.weekday.weekday, self.weekday.n or 1
+            jumpdays = (abs(nth) - 1) * 7
+            if nth > 0:
+                jumpdays += (7 - ret.weekday() + weekday) % 7
+            else:
+                jumpdays += (ret.weekday() - weekday) % 7
+                jumpdays *= -1
+            ret += datetime.timedelta(days=jumpdays)
+        return ret
+
+    def __radd__(self, other):
+        return self.__add__(other)
+
+    def __rsub__(self, other):
+        return self.__neg__().__radd__(other)
+
+    def __sub__(self, other):
+        if not isinstance(other, relativedelta):
+            return NotImplemented   # In case the other object defines __rsub__
+        return self.__class__(years=self.years - other.years,
+                             months=self.months - other.months,
+                             days=self.days - other.days,
+                             hours=self.hours - other.hours,
+                             minutes=self.minutes - other.minutes,
+                             seconds=self.seconds - other.seconds,
+                             microseconds=self.microseconds - other.microseconds,
+                             leapdays=self.leapdays or other.leapdays,
+                             year=self.year or other.year,
+                             month=self.month or other.month,
+                             day=self.day or other.day,
+                             weekday=self.weekday or other.weekday,
+                             hour=self.hour or other.hour,
+                             minute=self.minute or other.minute,
+                             second=self.second or other.second,
+                             microsecond=self.microsecond or other.microsecond)
+
+    def __neg__(self):
+        return self.__class__(years=-self.years,
+                             months=-self.months,
+                             days=-self.days,
+                             hours=-self.hours,
+                             minutes=-self.minutes,
+                             seconds=-self.seconds,
+                             microseconds=-self.microseconds,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __bool__(self):
+        return not (not self.years and
+                    not self.months and
+                    not self.days and
+                    not self.hours and
+                    not self.minutes and
+                    not self.seconds and
+                    not self.microseconds and
+                    not self.leapdays and
+                    self.year is None and
+                    self.month is None and
+                    self.day is None and
+                    self.weekday is None and
+                    self.hour is None and
+                    self.minute is None and
+                    self.second is None and
+                    self.microsecond is None)
+    # Compatibility with Python 2.x
+    __nonzero__ = __bool__
+
+    def __mul__(self, other):
+        try:
+            f = float(other)
+        except TypeError:
+            return NotImplemented
+
+        return self.__class__(years=int(self.years * f),
+                             months=int(self.months * f),
+                             days=int(self.days * f),
+                             hours=int(self.hours * f),
+                             minutes=int(self.minutes * f),
+                             seconds=int(self.seconds * f),
+                             microseconds=int(self.microseconds * f),
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    __rmul__ = __mul__
+
+    def __eq__(self, other):
+        if not isinstance(other, relativedelta):
+            return NotImplemented
+        if self.weekday or other.weekday:
+            if not self.weekday or not other.weekday:
+                return False
+            if self.weekday.weekday != other.weekday.weekday:
+                return False
+            n1, n2 = self.weekday.n, other.weekday.n
+            if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
+                return False
+        return (self.years == other.years and
+                self.months == other.months and
+                self.days == other.days and
+                self.hours == other.hours and
+                self.minutes == other.minutes and
+                self.seconds == other.seconds and
+                self.microseconds == other.microseconds and
+                self.leapdays == other.leapdays and
+                self.year == other.year and
+                self.month == other.month and
+                self.day == other.day and
+                self.hour == other.hour and
+                self.minute == other.minute and
+                self.second == other.second and
+                self.microsecond == other.microsecond)
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __div__(self, other):
+        try:
+            reciprocal = 1 / float(other)
+        except TypeError:
+            return NotImplemented
+
+        return self.__mul__(reciprocal)
+
+    __truediv__ = __div__
+
+    def __repr__(self):
+        l = []
+        for attr in ["years", "months", "days", "leapdays",
+                     "hours", "minutes", "seconds", "microseconds"]:
+            value = getattr(self, attr)
+            if value:
+                l.append("{attr}={value:+g}".format(attr=attr, value=value))
+        for attr in ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("{attr}={value}".format(attr=attr, value=repr(value)))
+        return "{classname}({attrs})".format(classname=self.__class__.__name__,
+                                             attrs=", ".join(l))
+
+def _sign(x):
+    return int(copysign(1, x))
+
+# vim:ts=4:sw=4:et
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/rrule.py
@@ -0,0 +1,1607 @@
+# -*- coding: utf-8 -*-
+"""
+The rrule module offers a small, complete, and very fast, implementation of
+the recurrence rules documented in the
+`iCalendar RFC <http://www.ietf.org/rfc/rfc2445.txt>`_,
+including support for caching of results.
+"""
+import itertools
+import datetime
+import calendar
+import sys
+
+try:
+    from math import gcd
+except ImportError:
+    from fractions import gcd
+
+from six import advance_iterator, integer_types
+from six.moves import _thread, range
+import heapq
+
+from ._common import weekday as weekdaybase
+
+# For warning about deprecation of until and count
+from warnings import warn
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+           "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+           "HOURLY", "MINUTELY", "SECONDLY",
+           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
+                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
+M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
+WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+FREQNAMES = ['YEARLY','MONTHLY','WEEKLY','DAILY','HOURLY','MINUTELY','SECONDLY']
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = list(range(7))
+
+# Imported on demand.
+easter = None
+parser = None
+
+class weekday(weekdaybase):
+    """
+    This version of weekday does not allow n = 0.
+    """
+    def __init__(self, wkday, n=None):
+        if n == 0:
+            raise ValueError("Can't create weekday with n==0")
+
+        super(weekday, self).__init__(wkday, n)
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+
+def _invalidates_cache(f):
+    """
+    Decorator for rruleset methods which may invalidate the
+    cached length.
+    """
+    def inner_func(self, *args, **kwargs):
+        rv = f(self, *args, **kwargs)
+        self._invalidate_cache()
+        return rv
+
+    return inner_func
+
+
+class rrulebase(object):
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = _thread.allocate_lock()
+            self._invalidate_cache()
+        else:
+            self._cache = None
+            self._cache_complete = False
+            self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _invalidate_cache(self):
+        if self._cache is not None:
+            self._cache = []
+            self._cache_complete = False
+            self._cache_gen = self._iter()
+
+            if self._cache_lock.locked():
+                self._cache_lock.release()
+
+        self._len = None
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(advance_iterator(gen))
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxsize,
+                                             item.step or 1))
+        elif item >= 0:
+            gen = iter(self)
+            try:
+                for i in range(item+1):
+                    res = advance_iterator(gen)
+            except StopIteration:
+                raise IndexError
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+                elif i > item:
+                    return False
+        return False
+
+    # __len__() introduces a large performance penality.
+    def count(self):
+        """ Returns the number of recurrences in this set. It will have go
+            trough the whole recurrence, if this hasn't been done before. """
+        if self._len is None:
+            for x in self:
+                pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        """ Returns the last recurrence before the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        """ Returns the first recurrence after the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned.  """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def xafter(self, dt, count=None, inc=False):
+        """
+        Generator which yields up to `count` recurrences after the given
+        datetime instance, equivalent to `after`.
+
+        :param dt:
+            The datetime at which to start generating recurrences.
+
+        :param count:
+            The maximum number of recurrences to generate. If `None` (default),
+            dates are generated until the recurrence rule is exhausted.
+
+        :param inc:
+            If `dt` is an instance of the rule and `inc` is `True`, it is
+            included in the output.
+
+        :yields: Yields a sequence of `datetime` objects.
+        """
+
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+
+        # Select the comparison function
+        if inc:
+            comp = lambda dc, dtc: dc >= dtc
+        else:
+            comp = lambda dc, dtc: dc > dtc
+
+        # Generate dates
+        n = 0
+        for d in gen:
+            if comp(d, dt):
+                yield d
+
+                if count is not None:
+                    n += 1
+                    if n >= count:
+                        break
+
+    def between(self, after, before, inc=False, count=1):
+        """ Returns all the occurrences of the rrule between after and before.
+        The inc keyword defines what happens if after and/or before are
+        themselves occurrences. With inc=True, they will be included in the
+        list, if they are found in the recurrence set. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if i > before:
+                    break
+                elif not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        else:
+            for i in gen:
+                if i >= before:
+                    break
+                elif not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        return l
+
+
+class rrule(rrulebase):
+    """
+    That's the base of the rrule operation. It accepts all the keywords
+    defined in the RFC as its constructor parameters (except byday,
+    which was renamed to byweekday) and more. The constructor prototype is::
+
+            rrule(freq)
+
+    Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
+    or SECONDLY.
+
+    .. note::
+        Per RFC section 3.3.10, recurrence instances falling on invalid dates
+        and times are ignored rather than coerced:
+
+            Recurrence rules may generate recurrence instances with an invalid
+            date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
+            on a day where the local time is moved forward by an hour at 1:00
+            AM).  Such recurrence instances MUST be ignored and MUST NOT be
+            counted as part of the recurrence set.
+
+        This can lead to possibly surprising behavior when, for example, the
+        start date occurs at the end of the month:
+
+        >>> from dateutil.rrule import rrule, MONTHLY
+        >>> from datetime import datetime
+        >>> start_date = datetime(2014, 12, 31)
+        >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
+        ... # doctest: +NORMALIZE_WHITESPACE
+        [datetime.datetime(2014, 12, 31, 0, 0),
+         datetime.datetime(2015, 1, 31, 0, 0),
+         datetime.datetime(2015, 3, 31, 0, 0),
+         datetime.datetime(2015, 5, 31, 0, 0)]
+
+    Additionally, it supports the following keyword arguments:
+
+    :param cache:
+        If given, it must be a boolean value specifying to enable or disable
+        caching of results. If you will use the same rrule instance multiple
+        times, enabling caching will improve the performance considerably.
+    :param dtstart:
+        The recurrence start. Besides being the base for the recurrence,
+        missing parameters in the final recurrence instances will also be
+        extracted from this date. If not given, datetime.now() will be used
+        instead.
+    :param interval:
+        The interval between each freq iteration. For example, when using
+        YEARLY, an interval of 2 means once every two years, but with HOURLY,
+        it means once every two hours. The default interval is 1.
+    :param wkst:
+        The week start day. Must be one of the MO, TU, WE constants, or an
+        integer, specifying the first day of the week. This will affect
+        recurrences based on weekly periods. The default week start is got
+        from calendar.firstweekday(), and may be modified by
+        calendar.setfirstweekday().
+    :param count:
+        How many occurrences will be generated.
+
+        .. note::
+            As of version 2.5.0, the use of the ``until`` keyword together
+            with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10.
+    :param until:
+        If given, this must be a datetime instance, that will specify the
+        limit of the recurrence. The last recurrence in the rule is the greatest
+        datetime that is less than or equal to the value specified in the
+        ``until`` parameter.
+        
+        .. note::
+            As of version 2.5.0, the use of the ``until`` keyword together
+            with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10.
+    :param bysetpos:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each given integer will specify an occurrence
+        number, corresponding to the nth occurrence of the rule inside the
+        frequency period. For example, a bysetpos of -1 if combined with a
+        MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
+        result in the last work day of every month.
+    :param bymonth:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the months to apply the recurrence to.
+    :param bymonthday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the month days to apply the recurrence to.
+    :param byyearday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the year days to apply the recurrence to.
+    :param byweekno:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the week numbers to apply the recurrence to. Week numbers
+        have the meaning described in ISO8601, that is, the first week of
+        the year is that containing at least four days of the new year.
+    :param byweekday:
+        If given, it must be either an integer (0 == MO), a sequence of
+        integers, one of the weekday constants (MO, TU, etc), or a sequence
+        of these constants. When given, these variables will define the
+        weekdays where the recurrence will be applied. It's also possible to
+        use an argument n for the weekday instances, which will mean the nth
+        occurrence of this weekday in the period. For example, with MONTHLY,
+        or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
+        first friday of the month where the recurrence happens. Notice that in
+        the RFC documentation, this is specified as BYDAY, but was renamed to
+        avoid the ambiguity of that keyword.
+    :param byhour:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the hours to apply the recurrence to.
+    :param byminute:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the minutes to apply the recurrence to.
+    :param bysecond:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the seconds to apply the recurrence to.
+    :param byeaster:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each integer will define an offset from the
+        Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+        Sunday itself. This is an extension to the RFC specification.
+     """
+    def __init__(self, freq, dtstart=None,
+                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
+                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+                 byweekno=None, byweekday=None,
+                 byhour=None, byminute=None, bysecond=None,
+                 cache=False):
+        super(rrule, self).__init__(cache)
+        global easter
+        if not dtstart:
+            dtstart = datetime.datetime.now().replace(microsecond=0)
+        elif not isinstance(dtstart, datetime.datetime):
+            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+        else:
+            dtstart = dtstart.replace(microsecond=0)
+        self._dtstart = dtstart
+        self._tzinfo = dtstart.tzinfo
+        self._freq = freq
+        self._interval = interval
+        self._count = count
+
+        # Cache the original byxxx rules, if they are provided, as the _byxxx
+        # attributes do not necessarily map to the inputs, and this can be
+        # a problem in generating the strings. Only store things if they've
+        # been supplied (the string retrieval will just use .get())
+        self._original_rule = {}
+
+        if until and not isinstance(until, datetime.datetime):
+            until = datetime.datetime.fromordinal(until.toordinal())
+        self._until = until
+
+        if count and until:
+            warn("Using both 'count' and 'until' is inconsistent with RFC 2445"
+                 " and has been deprecated in dateutil. Future versions will "
+                 "raise an error.", DeprecationWarning)
+
+        if wkst is None:
+            self._wkst = calendar.firstweekday()
+        elif isinstance(wkst, integer_types):
+            self._wkst = wkst
+        else:
+            self._wkst = wkst.weekday
+
+        if bysetpos is None:
+            self._bysetpos = None
+        elif isinstance(bysetpos, integer_types):
+            if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+                raise ValueError("bysetpos must be between 1 and 366, "
+                                 "or between -366 and -1")
+            self._bysetpos = (bysetpos,)
+        else:
+            self._bysetpos = tuple(bysetpos)
+            for pos in self._bysetpos:
+                if pos == 0 or not (-366 <= pos <= 366):
+                    raise ValueError("bysetpos must be between 1 and 366, "
+                                     "or between -366 and -1")
+
+        if self._bysetpos:
+            self._original_rule['bysetpos'] = self._bysetpos
+
+        if (byweekno is None and byyearday is None and bymonthday is None and
+                byweekday is None and byeaster is None):
+            if freq == YEARLY:
+                if bymonth is None:
+                    bymonth = dtstart.month
+                    self._original_rule['bymonth'] = None
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == MONTHLY:
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == WEEKLY:
+                byweekday = dtstart.weekday()
+                self._original_rule['byweekday'] = None
+
+        # bymonth
+        if bymonth is None:
+            self._bymonth = None
+        else:
+            if isinstance(bymonth, integer_types):
+                bymonth = (bymonth,)
+
+            self._bymonth = tuple(sorted(set(bymonth)))
+
+            if 'bymonth' not in self._original_rule:
+                self._original_rule['bymonth'] = self._bymonth
+
+        # byyearday
+        if byyearday is None:
+            self._byyearday = None
+        else:
+            if isinstance(byyearday, integer_types):
+                byyearday = (byyearday,)
+
+            self._byyearday = tuple(sorted(set(byyearday)))
+            self._original_rule['byyearday'] = self._byyearday
+
+        # byeaster
+        if byeaster is not None:
+            if not easter:
+                from dateutil import easter
+            if isinstance(byeaster, integer_types):
+                self._byeaster = (byeaster,)
+            else:
+                self._byeaster = tuple(sorted(byeaster))
+
+            self._original_rule['byeaster'] = self._byeaster
+        else:
+            self._byeaster = None
+
+        # bymonthday
+        if bymonthday is None:
+            self._bymonthday = ()
+            self._bynmonthday = ()
+        else:
+            if isinstance(bymonthday, integer_types):
+                bymonthday = (bymonthday,)
+
+            bymonthday = set(bymonthday)            # Ensure it's unique
+
+            self._bymonthday = tuple(sorted([x for x in bymonthday if x > 0]))
+            self._bynmonthday = tuple(sorted([x for x in bymonthday if x < 0]))
+
+            # Storing positive numbers first, then negative numbers
+            if 'bymonthday' not in self._original_rule:
+                self._original_rule['bymonthday'] = tuple(
+                    itertools.chain(self._bymonthday, self._bynmonthday))
+
+        # byweekno
+        if byweekno is None:
+            self._byweekno = None
+        else:
+            if isinstance(byweekno, integer_types):
+                byweekno = (byweekno,)
+
+            self._byweekno = tuple(sorted(set(byweekno)))
+
+            self._original_rule['byweekno'] = self._byweekno
+
+        # byweekday / bynweekday
+        if byweekday is None:
+            self._byweekday = None
+            self._bynweekday = None
+        else:
+            # If it's one of the valid non-sequence types, convert to a
+            # single-element sequence before the iterator that builds the
+            # byweekday set.
+            if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
+                byweekday = (byweekday,)
+
+            self._byweekday = set()
+            self._bynweekday = set()
+            for wday in byweekday:
+                if isinstance(wday, integer_types):
+                    self._byweekday.add(wday)
+                elif not wday.n or freq > MONTHLY:
+                    self._byweekday.add(wday.weekday)
+                else:
+                    self._bynweekday.add((wday.weekday, wday.n))
+
+            if not self._byweekday:
+                self._byweekday = None
+            elif not self._bynweekday:
+                self._bynweekday = None
+
+            if self._byweekday is not None:
+                self._byweekday = tuple(sorted(self._byweekday))
+                orig_byweekday = [weekday(x) for x in self._byweekday]
+            else:
+                orig_byweekday = tuple()
+
+            if self._bynweekday is not None:
+                self._bynweekday = tuple(sorted(self._bynweekday))
+                orig_bynweekday = [weekday(*x) for x in self._bynweekday]
+            else:
+                orig_bynweekday = tuple()
+
+            if 'byweekday' not in self._original_rule:
+                self._original_rule['byweekday'] = tuple(itertools.chain(
+                    orig_byweekday, orig_bynweekday))
+
+        # byhour
+        if byhour is None:
+            if freq < HOURLY:
+                self._byhour = set((dtstart.hour,))
+            else:
+                self._byhour = None
+        else:
+            if isinstance(byhour, integer_types):
+                byhour = (byhour,)
+
+            if freq == HOURLY:
+                self._byhour = self.__construct_byset(start=dtstart.hour,
+                                                      byxxx=byhour,
+                                                      base=24)
+            else:
+                self._byhour = set(byhour)
+
+            self._byhour = tuple(sorted(self._byhour))
+            self._original_rule['byhour'] = self._byhour
+
+        # byminute
+        if byminute is None:
+            if freq < MINUTELY:
+                self._byminute = set((dtstart.minute,))
+            else:
+                self._byminute = None
+        else:
+            if isinstance(byminute, integer_types):
+                byminute = (byminute,)
+
+            if freq == MINUTELY:
+                self._byminute = self.__construct_byset(start=dtstart.minute,
+                                                        byxxx=byminute,
+                                                        base=60)
+            else:
+                self._byminute = set(byminute)
+
+            self._byminute = tuple(sorted(self._byminute))
+            self._original_rule['byminute'] = self._byminute
+
+        # bysecond
+        if bysecond is None:
+            if freq < SECONDLY:
+                self._bysecond = ((dtstart.second,))
+            else:
+                self._bysecond = None
+        else:
+            if isinstance(bysecond, integer_types):
+                bysecond = (bysecond,)
+
+            self._bysecond = set(bysecond)
+
+            if freq == SECONDLY:
+                self._bysecond = self.__construct_byset(start=dtstart.second,
+                                                        byxxx=bysecond,
+                                                        base=60)
+            else:
+                self._bysecond = set(bysecond)
+
+            self._bysecond = tuple(sorted(self._bysecond))
+            self._original_rule['bysecond'] = self._bysecond
+
+        if self._freq >= HOURLY:
+            self._timeset = None
+        else:
+            self._timeset = []
+            for hour in self._byhour:
+                for minute in self._byminute:
+                    for second in self._bysecond:
+                        self._timeset.append(
+                            datetime.time(hour, minute, second,
+                                          tzinfo=self._tzinfo))
+            self._timeset.sort()
+            self._timeset = tuple(self._timeset)
+
+    def __str__(self):
+        """
+        Output a string that would generate this RRULE if passed to rrulestr.
+        This is mostly compatible with RFC2445, except for the
+        dateutil-specific extension BYEASTER.
+        """
+
+        output = []
+        h, m, s = [None] * 3
+        if self._dtstart:
+            output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
+            h, m, s = self._dtstart.timetuple()[3:6]
+
+        parts = ['FREQ=' + FREQNAMES[self._freq]]
+        if self._interval != 1:
+            parts.append('INTERVAL=' + str(self._interval))
+
+        if self._wkst:
+            parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
+
+        if self._count:
+            parts.append('COUNT=' + str(self._count))
+
+        if self._until:
+            parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
+
+        if self._original_rule.get('byweekday') is not None:
+            # The str() method on weekday objects doesn't generate
+            # RFC2445-compliant strings, so we should modify that.
+            original_rule = dict(self._original_rule)
+            wday_strings = []
+            for wday in original_rule['byweekday']:
+                if wday.n:
+                    wday_strings.append('{n:+d}{wday}'.format(
+                        n=wday.n,
+                        wday=repr(wday)[0:2]))
+                else:
+                    wday_strings.append(repr(wday))
+
+            original_rule['byweekday'] = wday_strings
+        else:
+            original_rule = self._original_rule
+
+        partfmt = '{name}={vals}'
+        for name, key in [('BYSETPOS', 'bysetpos'),
+                          ('BYMONTH', 'bymonth'),
+                          ('BYMONTHDAY', 'bymonthday'),
+                          ('BYYEARDAY', 'byyearday'),
+                          ('BYWEEKNO', 'byweekno'),
+                          ('BYDAY', 'byweekday'),
+                          ('BYHOUR', 'byhour'),
+                          ('BYMINUTE', 'byminute'),
+                          ('BYSECOND', 'bysecond'),
+                          ('BYEASTER', 'byeaster')]:
+            value = original_rule.get(key)
+            if value:
+                parts.append(partfmt.format(name=name, vals=(','.join(str(v)
+                                                             for v in value))))
+
+        output.append(';'.join(parts))
+        return '\n'.join(output)
+
+    def replace(self, **kwargs):
+        """Return new rrule with same attributes except for those attributes given new
+           values by whichever keyword arguments are specified."""
+        new_kwargs = {"interval": self._interval,
+                      "count": self._count,
+                      "dtstart": self._dtstart,
+                      "freq": self._freq,
+                      "until": self._until,
+                      "wkst": self._wkst,
+                      "cache": False if self._cache is None else True }
+        new_kwargs.update(self._original_rule)
+        new_kwargs.update(kwargs)
+        return rrule(**new_kwargs)
+
+
+    def _iter(self):
+        year, month, day, hour, minute, second, weekday, yearday, _ = \
+            self._dtstart.timetuple()
+
+        # Some local variables to speed things up a bit
+        freq = self._freq
+        interval = self._interval
+        wkst = self._wkst
+        until = self._until
+        bymonth = self._bymonth
+        byweekno = self._byweekno
+        byyearday = self._byyearday
+        byweekday = self._byweekday
+        byeaster = self._byeaster
+        bymonthday = self._bymonthday
+        bynmonthday = self._bynmonthday
+        bysetpos = self._bysetpos
+        byhour = self._byhour
+        byminute = self._byminute
+        bysecond = self._bysecond
+
+        ii = _iterinfo(self)
+        ii.rebuild(year, month)
+
+        getdayset = {YEARLY: ii.ydayset,
+                     MONTHLY: ii.mdayset,
+                     WEEKLY: ii.wdayset,
+                     DAILY: ii.ddayset,
+                     HOURLY: ii.ddayset,
+                     MINUTELY: ii.ddayset,
+                     SECONDLY: ii.ddayset}[freq]
+
+        if freq < HOURLY:
+            timeset = self._timeset
+        else:
+            gettimeset = {HOURLY: ii.htimeset,
+                          MINUTELY: ii.mtimeset,
+                          SECONDLY: ii.stimeset}[freq]
+            if ((freq >= HOURLY and
+                 self._byhour and hour not in self._byhour) or
+                (freq >= MINUTELY and
+                 self._byminute and minute not in self._byminute) or
+                (freq >= SECONDLY and
+                 self._bysecond and second not in self._bysecond)):
+                timeset = ()
+            else:
+                timeset = gettimeset(hour, minute, second)
+
+        total = 0
+        count = self._count
+        while True:
+            # Get dayset with the right frequency
+            dayset, start, end = getdayset(year, month, day)
+
+            # Do the "hard" work ;-)
+            filtered = False
+            for i in dayset[start:end]:
+                if ((bymonth and ii.mmask[i] not in bymonth) or
+                    (byweekno and not ii.wnomask[i]) or
+                    (byweekday and ii.wdaymask[i] not in byweekday) or
+                    (ii.nwdaymask and not ii.nwdaymask[i]) or
+                    (byeaster and not ii.eastermask[i]) or
+                    ((bymonthday or bynmonthday) and
+                     ii.mdaymask[i] not in bymonthday and
+                     ii.nmdaymask[i] not in bynmonthday) or
+                    (byyearday and
+                     ((i < ii.yearlen and i+1 not in byyearday and
+                       -ii.yearlen+i not in byyearday) or
+                      (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
+                       -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
+                    dayset[i] = None
+                    filtered = True
+
+            # Output results
+            if bysetpos and timeset:
+                poslist = []
+                for pos in bysetpos:
+                    if pos < 0:
+                        daypos, timepos = divmod(pos, len(timeset))
+                    else:
+                        daypos, timepos = divmod(pos-1, len(timeset))
+                    try:
+                        i = [x for x in dayset[start:end]
+                             if x is not None][daypos]
+                        time = timeset[timepos]
+                    except IndexError:
+                        pass
+                    else:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        res = datetime.datetime.combine(date, time)
+                        if res not in poslist:
+                            poslist.append(res)
+                poslist.sort()
+                for res in poslist:
+                    if until and res > until:
+                        self._len = total
+                        return
+                    elif res >= self._dtstart:
+                        total += 1
+                        yield res
+                        if count:
+                            count -= 1
+                            if not count:
+                                self._len = total
+                                return
+            else:
+                for i in dayset[start:end]:
+                    if i is not None:
+                        date = datetime.date.fromordinal(ii.yearordinal + i)
+                        for time in timeset:
+                            res = datetime.datetime.combine(date, time)
+                            if until and res > until:
+                                self._len = total
+                                return
+                            elif res >= self._dtstart:
+                                total += 1
+                                yield res
+                                if count:
+                                    count -= 1
+                                    if not count:
+                                        self._len = total
+                                        return
+
+            # Handle frequency and interval
+            fixday = False
+            if freq == YEARLY:
+                year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
+                ii.rebuild(year, month)
+            elif freq == MONTHLY:
+                month += interval
+                if month > 12:
+                    div, mod = divmod(month, 12)
+                    month = mod
+                    year += div
+                    if month == 0:
+                        month = 12
+                        year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
+                ii.rebuild(year, month)
+            elif freq == WEEKLY:
+                if wkst > weekday:
+                    day += -(weekday+1+(6-wkst))+self._interval*7
+                else:
+                    day += -(weekday-wkst)+self._interval*7
+                weekday = wkst
+                fixday = True
+            elif freq == DAILY:
+                day += interval
+                fixday = True
+            elif freq == HOURLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    hour += ((23-hour)//interval)*interval
+
+                if byhour:
+                    ndays, hour = self.__mod_distance(value=hour,
+                                                      byxxx=self._byhour,
+                                                      base=24)
+                else:
+                    ndays, hour = divmod(hour+interval, 24)
+
+                if ndays:
+                    day += ndays
+                    fixday = True
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == MINUTELY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    minute += ((1439-(hour*60+minute))//interval)*interval
+
+                valid = False
+                rep_rate = (24*60)
+                for j in range(rep_rate // gcd(interval, rep_rate)):
+                    if byminute:
+                        nhours, minute = \
+                            self.__mod_distance(value=minute,
+                                                byxxx=self._byminute,
+                                                base=60)
+                    else:
+                        nhours, minute = divmod(minute+interval, 60)
+
+                    div, hour = divmod(hour+nhours, 24)
+                    if div:
+                        day += div
+                        fixday = True
+                        filtered = False
+
+                    if not byhour or hour in byhour:
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval and ' +
+                                     'byhour resulting in empty rule.')
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == SECONDLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    second += (((86399 - (hour * 3600 + minute * 60 + second))
+                                // interval) * interval)
+
+                rep_rate = (24 * 3600)
+                valid = False
+                for j in range(0, rep_rate // gcd(interval, rep_rate)):
+                    if bysecond:
+                        nminutes, second = \
+                            self.__mod_distance(value=second,
+                                                byxxx=self._bysecond,
+                                                base=60)
+                    else:
+                        nminutes, second = divmod(second+interval, 60)
+
+                    div, minute = divmod(minute+nminutes, 60)
+                    if div:
+                        hour += div
+                        div, hour = divmod(hour, 24)
+                        if div:
+                            day += div
+                            fixday = True
+
+                    if ((not byhour or hour in byhour) and
+                            (not byminute or minute in byminute) and
+                            (not bysecond or second in bysecond)):
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval, ' +
+                                     'byhour and byminute resulting in empty' +
+                                     ' rule.')
+
+                timeset = gettimeset(hour, minute, second)
+
+            if fixday and day > 28:
+                daysinmonth = calendar.monthrange(year, month)[1]
+                if day > daysinmonth:
+                    while day > daysinmonth:
+                        day -= daysinmonth
+                        month += 1
+                        if month == 13:
+                            month = 1
+                            year += 1
+                            if year > datetime.MAXYEAR:
+                                self._len = total
+                                return
+                        daysinmonth = calendar.monthrange(year, month)[1]
+                    ii.rebuild(year, month)
+
+    def __construct_byset(self, start, byxxx, base):
+        """
+        If a `BYXXX` sequence is passed to the constructor at the same level as
+        `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
+        specifications which cannot be reached given some starting conditions.
+
+        This occurs whenever the interval is not coprime with the base of a
+        given unit and the difference between the starting position and the
+        ending position is not coprime with the greatest common denominator
+        between the interval and the base. For example, with a FREQ of hourly
+        starting at 17:00 and an interval of 4, the only valid values for
+        BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
+        coprime.
+
+        :param start:
+            Specifies the starting position.
+        :param byxxx:
+            An iterable containing the list of allowed values.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        This does not preserve the type of the iterable, returning a set, since
+        the values should be unique and the order is irrelevant, this will
+        speed up later lookups.
+
+        In the event of an empty set, raises a :exception:`ValueError`, as this
+        results in an empty rrule.
+        """
+
+        cset = set()
+
+        # Support a single byxxx value.
+        if isinstance(byxxx, integer_types):
+            byxxx = (byxxx, )
+
+        for num in byxxx:
+            i_gcd = gcd(self._interval, base)
+            # Use divmod rather than % because we need to wrap negative nums.
+            if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
+                cset.add(num)
+
+        if len(cset) == 0:
+            raise ValueError("Invalid rrule byxxx generates an empty set.")
+
+        return cset
+
+    def __mod_distance(self, value, byxxx, base):
+        """
+        Calculates the next value in a sequence where the `FREQ` parameter is
+        specified along with a `BYXXX` parameter at the same "level"
+        (e.g. `HOURLY` specified with `BYHOUR`).
+
+        :param value:
+            The old value of the component.
+        :param byxxx:
+            The `BYXXX` set, which should have been generated by
+            `rrule._construct_byset`, or something else which checks that a
+            valid rule is present.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        If a valid value is not found after `base` iterations (the maximum
+        number before the sequence would start to repeat), this raises a
+        :exception:`ValueError`, as no valid values were found.
+
+        This returns a tuple of `divmod(n*interval, base)`, where `n` is the
+        smallest number of `interval` repetitions until the next specified
+        value in `byxxx` is found.
+        """
+        accumulator = 0
+        for ii in range(1, base + 1):
+            # Using divmod() over % to account for negative intervals
+            div, value = divmod(value + self._interval, base)
+            accumulator += div
+            if value in byxxx:
+                return (accumulator, value)
+
+
+class _iterinfo(object):
+    __slots__ = ["rrule", "lastyear", "lastmonth",
+                 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
+                 "mmask", "mrange", "mdaymask", "nmdaymask",
+                 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
+
+    def __init__(self, rrule):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+        self.rrule = rrule
+
+    def rebuild(self, year, month):
+        # Every mask is 7 days longer to handle cross-year weekly periods.
+        rr = self.rrule
+        if year != self.lastyear:
+            self.yearlen = 365 + calendar.isleap(year)
+            self.nextyearlen = 365 + calendar.isleap(year + 1)
+            firstyday = datetime.date(year, 1, 1)
+            self.yearordinal = firstyday.toordinal()
+            self.yearweekday = firstyday.weekday()
+
+            wday = datetime.date(year, 1, 1).weekday()
+            if self.yearlen == 365:
+                self.mmask = M365MASK
+                self.mdaymask = MDAY365MASK
+                self.nmdaymask = NMDAY365MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M365RANGE
+            else:
+                self.mmask = M366MASK
+                self.mdaymask = MDAY366MASK
+                self.nmdaymask = NMDAY366MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M366RANGE
+
+            if not rr._byweekno:
+                self.wnomask = None
+            else:
+                self.wnomask = [0]*(self.yearlen+7)
+                # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
+                no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
+                if no1wkst >= 4:
+                    no1wkst = 0
+                    # Number of days in the year, plus the days we got
+                    # from last year.
+                    wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
+                else:
+                    # Number of days in the year, minus the days we
+                    # left in last year.
+                    wyearlen = self.yearlen-no1wkst
+                div, mod = divmod(wyearlen, 7)
+                numweeks = div+mod//4
+                for n in rr._byweekno:
+                    if n < 0:
+                        n += numweeks+1
+                    if not (0 < n <= numweeks):
+                        continue
+                    if n > 1:
+                        i = no1wkst+(n-1)*7
+                        if no1wkst != firstwkst:
+                            i -= 7-firstwkst
+                    else:
+                        i = no1wkst
+                    for j in range(7):
+                        self.wnomask[i] = 1
+                        i += 1
+                        if self.wdaymask[i] == rr._wkst:
+                            break
+                if 1 in rr._byweekno:
+                    # Check week number 1 of next year as well
+                    # TODO: Check -numweeks for next year.
+                    i = no1wkst+numweeks*7
+                    if no1wkst != firstwkst:
+                        i -= 7-firstwkst
+                    if i < self.yearlen:
+                        # If week starts in next year, we
+                        # don't care about it.
+                        for j in range(7):
+                            self.wnomask[i] = 1
+                            i += 1
+                            if self.wdaymask[i] == rr._wkst:
+                                break
+                if no1wkst:
+                    # Check last week number of last year as
+                    # well. If no1wkst is 0, either the year
+                    # started on week start, or week number 1
+                    # got days from last year, so there are no
+                    # days from last year's last week number in
+                    # this year.
+                    if -1 not in rr._byweekno:
+                        lyearweekday = datetime.date(year-1, 1, 1).weekday()
+                        lno1wkst = (7-lyearweekday+rr._wkst) % 7
+                        lyearlen = 365+calendar.isleap(year-1)
+                        if lno1wkst >= 4:
+                            lno1wkst = 0
+                            lnumweeks = 52+(lyearlen +
+                                            (lyearweekday-rr._wkst) % 7) % 7//4
+                        else:
+                            lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
+                    else:
+                        lnumweeks = -1
+                    if lnumweeks in rr._byweekno:
+                        for i in range(no1wkst):
+                            self.wnomask[i] = 1
+
+        if (rr._bynweekday and (month != self.lastmonth or
+                                year != self.lastyear)):
+            ranges = []
+            if rr._freq == YEARLY:
+                if rr._bymonth:
+                    for month in rr._bymonth:
+                        ranges.append(self.mrange[month-1:month+1])
+                else:
+                    ranges = [(0, self.yearlen)]
+            elif rr._freq == MONTHLY:
+                ranges = [self.mrange[month-1:month+1]]
+            if ranges:
+                # Weekly frequency won't get here, so we may not
+                # care about cross-year weekly periods.
+                self.nwdaymask = [0]*self.yearlen
+                for first, last in ranges:
+                    last -= 1
+                    for wday, n in rr._bynweekday:
+                        if n < 0:
+                            i = last+(n+1)*7
+                            i -= (self.wdaymask[i]-wday) % 7
+                        else:
+                            i = first+(n-1)*7
+                            i += (7-self.wdaymask[i]+wday) % 7
+                        if first <= i <= last:
+                            self.nwdaymask[i] = 1
+
+        if rr._byeaster:
+            self.eastermask = [0]*(self.yearlen+7)
+            eyday = easter.easter(year).toordinal()-self.yearordinal
+            for offset in rr._byeaster:
+                self.eastermask[eyday+offset] = 1
+
+        self.lastyear = year
+        self.lastmonth = month
+
+    def ydayset(self, year, month, day):
+        return list(range(self.yearlen)), 0, self.yearlen
+
+    def mdayset(self, year, month, day):
+        dset = [None]*self.yearlen
+        start, end = self.mrange[month-1:month+1]
+        for i in range(start, end):
+            dset[i] = i
+        return dset, start, end
+
+    def wdayset(self, year, month, day):
+        # We need to handle cross-year weeks here.
+        dset = [None]*(self.yearlen+7)
+        i = datetime.date(year, month, day).toordinal()-self.yearordinal
+        start = i
+        for j in range(7):
+            dset[i] = i
+            i += 1
+            # if (not (0 <= i < self.yearlen) or
+            #    self.wdaymask[i] == self.rrule._wkst):
+            # This will cross the year boundary, if necessary.
+            if self.wdaymask[i] == self.rrule._wkst:
+                break
+        return dset, start, i
+
+    def ddayset(self, year, month, day):
+        dset = [None] * self.yearlen
+        i = datetime.date(year, month, day).toordinal() - self.yearordinal
+        dset[i] = i
+        return dset, i, i + 1
+
+    def htimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for minute in rr._byminute:
+            for second in rr._bysecond:
+                tset.append(datetime.time(hour, minute, second,
+                                          tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def mtimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for second in rr._bysecond:
+            tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def stimeset(self, hour, minute, second):
+        return (datetime.time(hour, minute, second,
+                tzinfo=self.rrule._tzinfo),)
+
+
+class rruleset(rrulebase):
+    """ The rruleset type allows more complex recurrence setups, mixing
+    multiple rules, dates, exclusion rules, and exclusion dates. The type
+    constructor takes the following keyword arguments:
+
+    :param cache: If True, caching of results will be enabled, improving
+                  performance of multiple queries considerably. """
+
+    class _genitem(object):
+        def __init__(self, genlist, gen):
+            try:
+                self.dt = advance_iterator(gen)
+                genlist.append(self)
+            except StopIteration:
+                pass
+            self.genlist = genlist
+            self.gen = gen
+
+        def __next__(self):
+            try:
+                self.dt = advance_iterator(self.gen)
+            except StopIteration:
+                if self.genlist[0] is self:
+                    heapq.heappop(self.genlist)
+                else:
+                    self.genlist.remove(self)
+                    heapq.heapify(self.genlist)
+
+        next = __next__
+
+        def __lt__(self, other):
+            return self.dt < other.dt
+
+        def __gt__(self, other):
+            return self.dt > other.dt
+
+        def __eq__(self, other):
+            return self.dt == other.dt
+
+        def __ne__(self, other):
+            return self.dt != other.dt
+
+    def __init__(self, cache=False):
+        super(rruleset, self).__init__(cache)
+        self._rrule = []
+        self._rdate = []
+        self._exrule = []
+        self._exdate = []
+
+    @_invalidates_cache
+    def rrule(self, rrule):
+        """ Include the given :py:class:`rrule` instance in the recurrence set
+            generation. """
+        self._rrule.append(rrule)
+
+    @_invalidates_cache
+    def rdate(self, rdate):
+        """ Include the given :py:class:`datetime` instance in the recurrence
+            set generation. """
+        self._rdate.append(rdate)
+
+    @_invalidates_cache
+    def exrule(self, exrule):
+        """ Include the given rrule instance in the recurrence set exclusion
+            list. Dates which are part of the given recurrence rules will not
+            be generated, even if some inclusive rrule or rdate matches them.
+        """
+        self._exrule.append(exrule)
+
+    @_invalidates_cache
+    def exdate(self, exdate):
+        """ Include the given datetime instance in the recurrence set
+            exclusion list. Dates included that way will not be generated,
+            even if some inclusive rrule or rdate matches them. """
+        self._exdate.append(exdate)
+
+    def _iter(self):
+        rlist = []
+        self._rdate.sort()
+        self._genitem(rlist, iter(self._rdate))
+        for gen in [iter(x) for x in self._rrule]:
+            self._genitem(rlist, gen)
+        exlist = []
+        self._exdate.sort()
+        self._genitem(exlist, iter(self._exdate))
+        for gen in [iter(x) for x in self._exrule]:
+            self._genitem(exlist, gen)
+        lastdt = None
+        total = 0
+        heapq.heapify(rlist)
+        heapq.heapify(exlist)
+        while rlist:
+            ritem = rlist[0]
+            if not lastdt or lastdt != ritem.dt:
+                while exlist and exlist[0] < ritem:
+                    exitem = exlist[0]
+                    advance_iterator(exitem)
+                    if exlist and exlist[0] is exitem:
+                        heapq.heapreplace(exlist, exitem)
+                if not exlist or ritem != exlist[0]:
+                    total += 1
+                    yield ritem.dt
+                lastdt = ritem.dt
+            advance_iterator(ritem)
+            if rlist and rlist[0] is ritem:
+                heapq.heapreplace(rlist, ritem)
+        self._len = total
+
+
+class _rrulestr(object):
+
+    _freq_map = {"YEARLY": YEARLY,
+                 "MONTHLY": MONTHLY,
+                 "WEEKLY": WEEKLY,
+                 "DAILY": DAILY,
+                 "HOURLY": HOURLY,
+                 "MINUTELY": MINUTELY,
+                 "SECONDLY": SECONDLY}
+
+    _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
+                    "FR": 4, "SA": 5, "SU": 6}
+
+    def _handle_int(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = int(value)
+
+    def _handle_int_list(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
+
+    _handle_INTERVAL = _handle_int
+    _handle_COUNT = _handle_int
+    _handle_BYSETPOS = _handle_int_list
+    _handle_BYMONTH = _handle_int_list
+    _handle_BYMONTHDAY = _handle_int_list
+    _handle_BYYEARDAY = _handle_int_list
+    _handle_BYEASTER = _handle_int_list
+    _handle_BYWEEKNO = _handle_int_list
+    _handle_BYHOUR = _handle_int_list
+    _handle_BYMINUTE = _handle_int_list
+    _handle_BYSECOND = _handle_int_list
+
+    def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["freq"] = self._freq_map[value]
+
+    def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
+        global parser
+        if not parser:
+            from dateutil import parser
+        try:
+            rrkwargs["until"] = parser.parse(value,
+                                             ignoretz=kwargs.get("ignoretz"),
+                                             tzinfos=kwargs.get("tzinfos"))
+        except ValueError:
+            raise ValueError("invalid until date")
+
+    def _handle_WKST(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["wkst"] = self._weekday_map[value]
+
+    def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
+        """
+        Two ways to specify this: +1MO or MO(+1)
+        """
+        l = []
+        for wday in value.split(','):
+            if '(' in wday:
+                # If it's of the form TH(+1), etc.
+                splt = wday.split('(')
+                w = splt[0]
+                n = int(splt[1][:-1])
+            elif len(wday):
+                # If it's of the form +1MO
+                for i in range(len(wday)):
+                    if wday[i] not in '+-0123456789':
+                        break
+                n = wday[:i] or None
+                w = wday[i:]
+                if n:
+                    n = int(n)
+            else:
+                raise ValueError("Invalid (empty) BYDAY specification.")
+
+            l.append(weekdays[self._weekday_map[w]](n))
+        rrkwargs["byweekday"] = l
+
+    _handle_BYDAY = _handle_BYWEEKDAY
+
+    def _parse_rfc_rrule(self, line,
+                         dtstart=None,
+                         cache=False,
+                         ignoretz=False,
+                         tzinfos=None):
+        if line.find(':') != -1:
+            name, value = line.split(':')
+            if name != "RRULE":
+                raise ValueError("unknown parameter name")
+        else:
+            value = line
+        rrkwargs = {}
+        for pair in value.split(';'):
+            name, value = pair.split('=')
+            name = name.upper()
+            value = value.upper()
+            try:
+                getattr(self, "_handle_"+name)(rrkwargs, name, value,
+                                               ignoretz=ignoretz,
+                                               tzinfos=tzinfos)
+            except AttributeError:
+                raise ValueError("unknown parameter '%s'" % name)
+            except (KeyError, ValueError):
+                raise ValueError("invalid '%s': %s" % (name, value))
+        return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+
+    def _parse_rfc(self, s,
+                   dtstart=None,
+                   cache=False,
+                   unfold=False,
+                   forceset=False,
+                   compatible=False,
+                   ignoretz=False,
+                   tzinfos=None):
+        global parser
+        if compatible:
+            forceset = True
+            unfold = True
+        s = s.upper()
+        if not s.strip():
+            raise ValueError("empty string")
+        if unfold:
+            lines = s.splitlines()
+            i = 0
+            while i < len(lines):
+                line = lines[i].rstrip()
+                if not line:
+                    del lines[i]
+                elif i > 0 and line[0] == " ":
+                    lines[i-1] += line[1:]
+                    del lines[i]
+                else:
+                    i += 1
+        else:
+            lines = s.split()
+        if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
+                                                  s.startswith('RRULE:'))):
+            return self._parse_rfc_rrule(lines[0], cache=cache,
+                                         dtstart=dtstart, ignoretz=ignoretz,
+                                         tzinfos=tzinfos)
+        else:
+            rrulevals = []
+            rdatevals = []
+            exrulevals = []
+            exdatevals = []
+            for line in lines:
+                if not line:
+                    continue
+                if line.find(':') == -1:
+                    name = "RRULE"
+                    value = line
+                else:
+                    name, value = line.split(':', 1)
+                parms = name.split(';')
+                if not parms:
+                    raise ValueError("empty property name")
+                name = parms[0]
+                parms = parms[1:]
+                if name == "RRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported RRULE parm: "+parm)
+                    rrulevals.append(value)
+                elif name == "RDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError("unsupported RDATE parm: "+parm)
+                    rdatevals.append(value)
+                elif name == "EXRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported EXRULE parm: "+parm)
+                    exrulevals.append(value)
+                elif name == "EXDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError("unsupported RDATE parm: "+parm)
+                    exdatevals.append(value)
+                elif name == "DTSTART":
+                    for parm in parms:
+                        raise ValueError("unsupported DTSTART parm: "+parm)
+                    if not parser:
+                        from dateutil import parser
+                    dtstart = parser.parse(value, ignoretz=ignoretz,
+                                           tzinfos=tzinfos)
+                else:
+                    raise ValueError("unsupported property: "+name)
+            if (forceset or len(rrulevals) > 1 or rdatevals
+                    or exrulevals or exdatevals):
+                if not parser and (rdatevals or exdatevals):
+                    from dateutil import parser
+                rset = rruleset(cache=cache)
+                for value in rrulevals:
+                    rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                     ignoretz=ignoretz,
+                                                     tzinfos=tzinfos))
+                for value in rdatevals:
+                    for datestr in value.split(','):
+                        rset.rdate(parser.parse(datestr,
+                                                ignoretz=ignoretz,
+                                                tzinfos=tzinfos))
+                for value in exrulevals:
+                    rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                      ignoretz=ignoretz,
+                                                      tzinfos=tzinfos))
+                for value in exdatevals:
+                    for datestr in value.split(','):
+                        rset.exdate(parser.parse(datestr,
+                                                 ignoretz=ignoretz,
+                                                 tzinfos=tzinfos))
+                if compatible and dtstart:
+                    rset.rdate(dtstart)
+                return rset
+            else:
+                return self._parse_rfc_rrule(rrulevals[0],
+                                             dtstart=dtstart,
+                                             cache=cache,
+                                             ignoretz=ignoretz,
+                                             tzinfos=tzinfos)
+
+    def __call__(self, s, **kwargs):
+        return self._parse_rfc(s, **kwargs)
+
+rrulestr = _rrulestr()
+
+# vim:ts=4:sw=4:et
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/_common.py
@@ -0,0 +1,288 @@
+from __future__ import unicode_literals
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+import os
+import datetime
+import time
+import subprocess
+import warnings
+import tempfile
+import pickle
+
+
+class WarningTestMixin(object):
+    # Based on https://stackoverflow.com/a/12935176/467366
+    class _AssertWarnsContext(warnings.catch_warnings):
+        def __init__(self, expected_warnings, parent, **kwargs):
+            super(WarningTestMixin._AssertWarnsContext, self).__init__(**kwargs)
+
+            self.parent = parent
+            try:
+                self.expected_warnings = list(expected_warnings)
+            except TypeError:
+                self.expected_warnings = [expected_warnings]
+
+            self._warning_log = []
+
+        def __enter__(self, *args, **kwargs):
+            rv = super(WarningTestMixin._AssertWarnsContext, self).__enter__(*args, **kwargs)
+
+            if self._showwarning is not self._module.showwarning:
+                super_showwarning = self._module.showwarning
+            else:
+                super_showwarning = None
+
+            def showwarning(*args, **kwargs):
+                if super_showwarning is not None:
+                    super_showwarning(*args, **kwargs)
+
+                self._warning_log.append(warnings.WarningMessage(*args, **kwargs))
+
+            self._module.showwarning = showwarning
+            return rv
+
+        def __exit__(self, *args, **kwargs):
+            super(WarningTestMixin._AssertWarnsContext, self).__exit__(self, *args, **kwargs)
+
+            self.parent.assertTrue(any(issubclass(item.category, warning)
+                                       for warning in self.expected_warnings
+                                       for item in self._warning_log))
+
+    def assertWarns(self, warning, callable=None, *args, **kwargs):
+        warnings.simplefilter('always')
+        context = self.__class__._AssertWarnsContext(warning, self)
+        if callable is None:
+            return context
+        else:
+            with context:
+                callable(*args, **kwargs)
+
+
+class PicklableMixin(object):
+    def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs):
+        """
+        Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads``
+        """
+        pkl = pickle.dumps(obj, **dump_kwargs)
+        return pickle.loads(pkl, **load_kwargs)
+
+    def _get_nobj_file(self, obj, dump_kwargs, load_kwargs):
+        """
+        Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on
+        a temporary file.
+        """
+        with tempfile.TemporaryFile('w+b') as pkl:
+            pickle.dump(obj, pkl, **dump_kwargs)
+            pkl.seek(0)         # Reset the file to the beginning to read it
+            nobj = pickle.load(pkl, **load_kwargs)
+
+        return nobj
+
+    def assertPicklable(self, obj, asfile=False,
+                        dump_kwargs=None, load_kwargs=None):
+        """
+        Assert that an object can be pickled and unpickled. This assertion
+        assumes that the desired behavior is that the unpickled object compares
+        equal to the original object, but is not the same object.
+        """
+        get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes
+        dump_kwargs = dump_kwargs or {}
+        load_kwargs = load_kwargs or {}
+
+        nobj = get_nobj(obj, dump_kwargs, load_kwargs)
+        self.assertIsNot(obj, nobj)
+        self.assertEqual(obj, nobj)
+
+
+class TZContextBase(object):
+    """
+    Base class for a context manager which allows changing of time zones.
+
+    Subclasses may define a guard variable to either block or or allow time
+    zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``.
+    The default is that the guard variable must be affirmatively set.
+
+    Subclasses must define ``get_current_tz`` and ``set_current_tz``.
+    """
+    _guard_var_name = "DATEUTIL_MAY_CHANGE_TZ"
+    _guard_allows_change = True
+
+    def __init__(self, tzval):
+        self.tzval = tzval
+        self._old_tz = None
+
+    @classmethod
+    def tz_change_allowed(cls):
+        """
+        Class method used to query whether or not this class allows time zone
+        changes.
+        """
+        guard = bool(os.environ.get(cls._guard_var_name, False))
+
+        # _guard_allows_change gives the "default" behavior - if True, the
+        # guard is overcoming a block. If false, the guard is causing a block.
+        # Whether tz_change is allowed is therefore the XNOR of the two.
+        return guard == cls._guard_allows_change
+
+    @classmethod
+    def tz_change_disallowed_message(cls):
+        """ Generate instructions on how to allow tz changes """
+        msg = ('Changing time zone not allowed. Set {envar} to {gval} '
+               'if you would like to allow this behavior')
+
+        return msg.format(envar=cls._guard_var_name,
+                          gval=cls._guard_allows_change)
+
+    def __enter__(self):
+        if not self.tz_change_allowed():
+            raise ValueError(self.tz_change_disallowed_message())
+
+        self._old_tz = self.get_current_tz()
+        self.set_current_tz(self.tzval)
+
+    def __exit__(self, type, value, traceback):
+        if self._old_tz is not None:
+            self.set_current_tz(self._old_tz)
+
+        self._old_tz = None
+
+    def get_current_tz(self):
+        raise NotImplementedError
+
+    def set_current_tz(self):
+        raise NotImplementedError
+
+
+class TZEnvContext(TZContextBase):
+    """
+    Context manager that temporarily sets the `TZ` variable (for use on
+    *nix-like systems). Because the effect is local to the shell anyway, this
+    will apply *unless* a guard is set.
+
+    If you do not want the TZ environment variable set, you may set the
+    ``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value.
+    """
+    _guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR"
+    _guard_allows_change = False
+
+    def get_current_tz(self):
+        return os.environ.get('TZ', UnsetTz)
+
+    def set_current_tz(self, tzval):
+        if tzval is UnsetTz and 'TZ' in os.environ:
+            del os.environ['TZ']
+        else:
+            os.environ['TZ'] = tzval
+
+        time.tzset()
+
+
+class TZWinContext(TZContextBase):
+    """
+    Context manager for changing local time zone on Windows.
+
+    Because the effect of this is system-wide and global, it may have
+    unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment
+    variable to a truthy value before using this context manager.
+    """
+    def get_current_tz(self):
+        p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE)
+
+        ctzname, err = p.communicate()
+        ctzname = ctzname.decode()     # Popen returns 
+
+        if p.returncode:
+            raise OSError('Failed to get current time zone: ' + err)
+
+        return ctzname
+
+    def set_current_tz(self, tzname):
+        p = subprocess.Popen('tzutil /s "' + tzname + '"')
+
+        out, err = p.communicate()
+
+        if p.returncode:
+            raise OSError('Failed to set current time zone: ' +
+                          (err or 'Unknown error.'))
+
+
+###
+# Compatibility functions
+
+def _total_seconds(td):
+    # Python 2.6 doesn't have a total_seconds() method on timedelta objects
+    return ((td.seconds + td.days * 86400) * 1000000 +
+            td.microseconds) // 1000000
+
+total_seconds = getattr(datetime.timedelta, 'total_seconds', _total_seconds)
+
+
+###
+# Utility classes
+class NotAValueClass(object):
+    """
+    A class analogous to NaN that has operations defined for any type.
+    """
+    def _op(self, other):
+        return self             # Operation with NotAValue returns NotAValue
+
+    def _cmp(self, other):
+        return False
+
+    __add__ = __radd__ = _op
+    __sub__ = __rsub__ = _op
+    __mul__ = __rmul__ = _op
+    __div__ = __rdiv__ = _op
+    __truediv__ = __rtruediv__ = _op
+    __floordiv__ = __rfloordiv__ = _op
+
+    __lt__ = __rlt__ = _op
+    __gt__ = __rgt__ = _op
+    __eq__ = __req__ = _op
+    __le__ = __rle__ = _op
+    __ge__ = __rge__ = _op
+
+NotAValue = NotAValueClass()
+
+
+class ComparesEqualClass(object):
+    """
+    A class that is always equal to whatever you compare it to.
+    """
+
+    def __eq__(self, other):
+        return True
+
+    def __ne__(self, other):
+        return False
+
+    def __le__(self, other):
+        return True
+
+    def __ge__(self, other):
+        return True
+
+    def __lt__(self, other):
+        return False
+
+    def __gt__(self, other):
+        return False
+
+    __req__ = __eq__
+    __rne__ = __ne__
+    __rle__ = __le__
+    __rge__ = __ge__
+    __rlt__ = __lt__
+    __rgt__ = __gt__
+
+ComparesEqual = ComparesEqualClass()
+
+class UnsetTzClass(object):
+    """ Sentinel class for unset time zone variable """
+    pass
+
+UnsetTz = UnsetTzClass()
+
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_easter.py
@@ -0,0 +1,99 @@
+from dateutil.easter import easter
+from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN
+
+from datetime import date
+
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+# List of easters between 1990 and 2050
+western_easter_dates = [
+    date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11),
+    date(1994, 4,  3), date(1995, 4, 16), date(1996, 4,  7), date(1997, 3, 30),
+    date(1998, 4, 12), date(1999, 4,  4),
+
+    date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20),
+    date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4,  8),
+    date(2008, 3, 23), date(2009, 4, 12),
+
+    date(2010, 4,  4), date(2011, 4, 24), date(2012, 4,  8), date(2013, 3, 31),
+    date(2014, 4, 20), date(2015, 4,  5), date(2016, 3, 27), date(2017, 4, 16),
+    date(2018, 4,  1), date(2019, 4, 21),
+
+    date(2020, 4, 12), date(2021, 4,  4), date(2022, 4, 17), date(2023, 4,  9),
+    date(2024, 3, 31), date(2025, 4, 20), date(2026, 4,  5), date(2027, 3, 28),
+    date(2028, 4, 16), date(2029, 4,  1),
+
+    date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17),
+    date(2034, 4,  9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4,  5),
+    date(2038, 4, 25), date(2039, 4, 10),
+
+    date(2040, 4,  1), date(2041, 4, 21), date(2042, 4,  6), date(2043, 3, 29),
+    date(2044, 4, 17), date(2045, 4,  9), date(2046, 3, 25), date(2047, 4, 14),
+    date(2048, 4,  5), date(2049, 4, 18), date(2050, 4, 10)
+    ]
+
+orthodox_easter_dates = [
+    date(1990, 4, 15), date(1991, 4,  7), date(1992, 4, 26), date(1993, 4, 18),
+    date(1994, 5,  1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27),
+    date(1998, 4, 19), date(1999, 4, 11),
+
+    date(2000, 4, 30), date(2001, 4, 15), date(2002, 5,  5), date(2003, 4, 27),
+    date(2004, 4, 11), date(2005, 5,  1), date(2006, 4, 23), date(2007, 4,  8),
+    date(2008, 4, 27), date(2009, 4, 19),
+
+    date(2010, 4,  4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5,  5),
+    date(2014, 4, 20), date(2015, 4, 12), date(2016, 5,  1), date(2017, 4, 16),
+    date(2018, 4,  8), date(2019, 4, 28),
+
+    date(2020, 4, 19), date(2021, 5,  2), date(2022, 4, 24), date(2023, 4, 16),
+    date(2024, 5,  5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5,  2),
+    date(2028, 4, 16), date(2029, 4,  8),
+
+    date(2030, 4, 28), date(2031, 4, 13), date(2032, 5,  2), date(2033, 4, 24),
+    date(2034, 4,  9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4,  5),
+    date(2038, 4, 25), date(2039, 4, 17),
+
+    date(2040, 5,  6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5,  3),
+    date(2044, 4, 24), date(2045, 4,  9), date(2046, 4, 29), date(2047, 4, 21),
+    date(2048, 4,  5), date(2049, 4, 25), date(2050, 4, 17)
+]
+
+# A random smattering of Julian dates.
+# Pulled values from http://www.kevinlaughery.com/east4099.html
+julian_easter_dates = [
+    date( 326, 4,  3), date( 375, 4,  5), date( 492, 4,  5), date( 552, 3, 31),
+    date( 562, 4,  9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19),
+    date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4,  8),
+    date( 750, 3, 29), date( 782, 4,  7), date( 835, 4, 18), date( 849, 4, 14),
+    date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4,  6),
+    date(1049, 3, 26), date(1058, 4, 19), date(1113, 4,  6), date(1119, 3, 30),
+    date(1242, 4, 20), date(1255, 3, 28), date(1257, 4,  8), date(1258, 3, 24),
+    date(1261, 4, 24), date(1278, 4, 17), date(1333, 4,  4), date(1351, 4, 17),
+    date(1371, 4,  6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4,  3),
+    date(1439, 4,  5), date(1445, 3, 28), date(1531, 4,  9), date(1555, 4, 14)
+]
+
+
+class EasterTest(unittest.TestCase):
+    def testEasterWestern(self):
+        for easter_date in western_easter_dates:
+            self.assertEqual(easter_date,
+                             easter(easter_date.year, EASTER_WESTERN))
+
+    def testEasterOrthodox(self):
+        for easter_date in orthodox_easter_dates:
+            self.assertEqual(easter_date,
+                             easter(easter_date.year, EASTER_ORTHODOX))
+
+    def testEasterJulian(self):
+        for easter_date in julian_easter_dates:
+            self.assertEqual(easter_date,
+                             easter(easter_date.year, EASTER_JULIAN))
+
+    def testEasterBadMethod(self):
+        # Invalid methods raise ValueError
+        with self.assertRaises(ValueError):
+            easter(1975, 4)
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_imports.py
@@ -0,0 +1,149 @@
+import sys
+
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+
+class ImportEasterTest(unittest.TestCase):
+    """ Test that dateutil.easter-related imports work properly """
+
+    def testEasterDirect(self):
+        import dateutil.easter
+
+    def testEasterFrom(self):
+        from dateutil import easter
+
+    def testEasterStar(self):
+        from dateutil.easter import easter
+
+
+class ImportParserTest(unittest.TestCase):
+    """ Test that dateutil.parser-related imports work properly """
+    def testParserDirect(self):
+        import dateutil.parser
+
+    def testParserFrom(self):
+        from dateutil import parser
+
+    def testParserAll(self):
+        # All interface
+        from dateutil.parser import parse
+        from dateutil.parser import parserinfo
+
+        # Other public classes
+        from dateutil.parser import parser
+
+        for var in (parse, parserinfo, parser):
+            self.assertIsNot(var, None)
+
+
+class ImportRelativeDeltaTest(unittest.TestCase):
+    """ Test that dateutil.relativedelta-related imports work properly """
+    def testRelativeDeltaDirect(self):
+        import dateutil.relativedelta
+
+    def testRelativeDeltaFrom(self):
+        from dateutil import relativedelta
+
+    def testRelativeDeltaAll(self):
+        from dateutil.relativedelta import relativedelta
+        from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
+
+        for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU):
+            self.assertIsNot(var, None)
+
+        # In the public interface but not in all
+        from dateutil.relativedelta import weekday
+        self.assertIsNot(weekday, None)
+
+
+class ImportRRuleTest(unittest.TestCase):
+    """ Test that dateutil.rrule related imports work properly """
+    def testRRuleDirect(self):
+        import dateutil.rrule
+
+    def testRRuleFrom(self):
+        from dateutil import rrule
+
+    def testRRuleAll(self):
+        from dateutil.rrule import rrule
+        from dateutil.rrule import rruleset
+        from dateutil.rrule import rrulestr
+        from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY
+        from dateutil.rrule import HOURLY, MINUTELY, SECONDLY
+        from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU
+
+        rr_all = (rrule, rruleset, rrulestr,
+                  YEARLY, MONTHLY, WEEKLY, DAILY,
+                  HOURLY, MINUTELY, SECONDLY,
+                  MO, TU, WE, TH, FR, SA, SU)
+
+        for var in rr_all:
+            self.assertIsNot(var, None)
+
+        # In the public interface but not in all
+        from dateutil.rrule import weekday
+        self.assertIsNot(weekday, None)
+
+
+class ImportTZTest(unittest.TestCase):
+    """ Test that dateutil.tz related imports work properly """
+    def testTzDirect(self):
+        import dateutil.tz
+
+    def testTzFrom(self):
+        from dateutil import tz
+
+    def testTzAll(self):
+        from dateutil.tz import tzutc
+        from dateutil.tz import tzoffset
+        from dateutil.tz import tzlocal
+        from dateutil.tz import tzfile
+        from dateutil.tz import tzrange
+        from dateutil.tz import tzstr
+        from dateutil.tz import tzical
+        from dateutil.tz import gettz
+        from dateutil.tz import tzwin
+        from dateutil.tz import tzwinlocal
+
+        tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+                  "tzstr", "tzical", "gettz"]
+
+        tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else []
+        lvars = locals()
+
+        for var in tz_all:
+            self.assertIsNot(lvars[var], None)
+
+
+@unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows")
+class ImportTZWinTest(unittest.TestCase):
+    """ Test that dateutil.tzwin related imports work properly """
+    def testTzwinDirect(self):
+        import dateutil.tzwin
+
+    def testTzwinFrom(self):
+        from dateutil import tzwin
+
+    def testTzwinStar(self):
+        tzwin_all = ["tzwin", "tzwinlocal"]
+
+
+class ImportZoneInfoTest(unittest.TestCase):
+    def testZoneinfoDirect(self):
+        import dateutil.zoneinfo
+
+    def testZoneinfoFrom(self):
+        from dateutil import zoneinfo
+
+    def testZoneinfoStar(self):
+        from dateutil.zoneinfo import gettz
+        from dateutil.zoneinfo import gettz_db_metadata
+        from dateutil.zoneinfo import rebuild
+
+        zi_all = (gettz, gettz_db_metadata, rebuild)
+
+        for var in zi_all:
+            self.assertIsNot(var, None)
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_parser.py
@@ -0,0 +1,861 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import unittest
+
+from datetime import datetime, timedelta, date
+
+from dateutil.tz import tzoffset
+from dateutil.parser import *
+
+import six
+from six import assertRaisesRegex, PY3
+from six.moves import StringIO
+
+class ParserTest(unittest.TestCase):
+
+    def setUp(self):
+        self.tzinfos = {"BRST": -10800}
+        self.brsttz = tzoffset("BRST", -10800)
+        self.default = datetime(2003, 9, 25)
+
+        # Parser should be able to handle bytestring and unicode
+        base_str = '2014-05-01 08:00:00'
+        try:
+            # Python 2.x
+            self.uni_str = unicode(base_str)
+            self.str_str = str(base_str)
+        except NameError:
+            self.uni_str = str(base_str)
+            self.str_str = bytes(base_str.encode())
+
+    def testEmptyString(self):
+        with self.assertRaises(ValueError):
+            parse('')
+
+    def testNone(self):
+        with self.assertRaises(TypeError):
+            parse(None)
+
+    def testInvalidType(self):
+        with self.assertRaises(TypeError):
+            parse(13)
+
+    def testDuckTyping(self):
+        # We want to support arbitrary classes that implement the stream
+        # interface.
+
+        class StringPassThrough(object):
+            def __init__(self, stream):
+                self.stream = stream
+
+            def read(self, *args, **kwargs):
+                return self.stream.read(*args, **kwargs)
+
+
+        dstr = StringPassThrough(StringIO('2014 January 19'))
+
+        self.assertEqual(parse(dstr), datetime(2014, 1, 19))
+
+    def testParseStream(self):
+        dstr = StringIO('2014 January 19')
+
+        self.assertEqual(parse(dstr), datetime(2014, 1, 19))
+
+    def testParseStr(self):
+        self.assertEqual(parse(self.str_str),
+                         parse(self.uni_str))
+
+    def testParserParseStr(self):
+        from dateutil.parser import parser
+
+        self.assertEqual(parser().parse(self.str_str),
+                         parser().parse(self.uni_str))
+
+    def testParseUnicodeWords(self):
+
+        class rus_parserinfo(parserinfo):
+            MONTHS = [("янв", "Январь"),
+                      ("фев", "Февраль"),
+                      ("мар", "Март"),
+                      ("апр", "Апрель"),
+                      ("май", "Май"),
+                      ("июн", "Июнь"),
+                      ("июл", "Июль"),
+                      ("авг", "Август"),
+                      ("сен", "Сентябрь"),
+                      ("окт", "Октябрь"),
+                      ("ноя", "Ноябрь"),
+                      ("дек", "Декабрь")]
+
+        self.assertEqual(parse('10 Сентябрь 2015 10:20',
+                               parserinfo=rus_parserinfo()),
+                         datetime(2015, 9, 10, 10, 20))
+
+    def testParseWithNulls(self):
+        # This relies on the from __future__ import unicode_literals, because
+        # explicitly specifying a unicode literal is a syntax error in Py 3.2
+        # May want to switch to u'...' if we ever drop Python 3.2 support.
+        pstring = '\x00\x00August 29, 1924'
+
+        self.assertEqual(parse(pstring),
+                         datetime(1924, 8, 29))
+
+    def testDateCommandFormat(self):
+        self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+                               tzinfos=self.tzinfos),
+                         datetime(2003, 9, 25, 10, 36, 28,
+                                  tzinfo=self.brsttz))
+
+    def testDateCommandFormatUnicode(self):
+        self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+                               tzinfos=self.tzinfos),
+                         datetime(2003, 9, 25, 10, 36, 28,
+                                  tzinfo=self.brsttz))
+
+
+    def testDateCommandFormatReversed(self):
+        self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu",
+                               tzinfos=self.tzinfos),
+                         datetime(2003, 9, 25, 10, 36, 28,
+                                  tzinfo=self.brsttz))
+
+    def testDateCommandFormatWithLong(self):
+        if not PY3:
+            self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+                                   tzinfos={"BRST": long(-10800)}),
+                             datetime(2003, 9, 25, 10, 36, 28,
+                                      tzinfo=self.brsttz))
+    def testDateCommandFormatIgnoreTz(self):
+        self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
+                               ignoretz=True),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip1(self):
+        self.assertEqual(parse("Thu Sep 25 10:36:28 2003"),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip2(self):
+        self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip3(self):
+        self.assertEqual(parse("Thu Sep 10:36:28", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip4(self):
+        self.assertEqual(parse("Thu 10:36:28", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip5(self):
+        self.assertEqual(parse("Sep 10:36:28", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip6(self):
+        self.assertEqual(parse("10:36:28", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testDateCommandFormatStrip7(self):
+        self.assertEqual(parse("10:36", default=self.default),
+                         datetime(2003, 9, 25, 10, 36))
+
+    def testDateCommandFormatStrip8(self):
+        self.assertEqual(parse("Thu Sep 25 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateCommandFormatStrip9(self):
+        self.assertEqual(parse("Sep 25 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateCommandFormatStrip10(self):
+        self.assertEqual(parse("Sep 2003", default=self.default),
+                         datetime(2003, 9, 25))
+
+    def testDateCommandFormatStrip11(self):
+        self.assertEqual(parse("Sep", default=self.default),
+                         datetime(2003, 9, 25))
+
+    def testDateCommandFormatStrip12(self):
+        self.assertEqual(parse("2003", default=self.default),
+                         datetime(2003, 9, 25))
+
+    def testDateRCommandFormat(self):
+        self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"),
+                         datetime(2003, 9, 25, 10, 49, 41,
+                                  tzinfo=self.brsttz))
+
+    def testISOFormat(self):
+        self.assertEqual(parse("2003-09-25T10:49:41.5-03:00"),
+                         datetime(2003, 9, 25, 10, 49, 41, 500000,
+                                  tzinfo=self.brsttz))
+
+    def testISOFormatStrip1(self):
+        self.assertEqual(parse("2003-09-25T10:49:41-03:00"),
+                         datetime(2003, 9, 25, 10, 49, 41,
+                                  tzinfo=self.brsttz))
+
+    def testISOFormatStrip2(self):
+        self.assertEqual(parse("2003-09-25T10:49:41"),
+                         datetime(2003, 9, 25, 10, 49, 41))
+
+    def testISOFormatStrip3(self):
+        self.assertEqual(parse("2003-09-25T10:49"),
+                         datetime(2003, 9, 25, 10, 49))
+
+    def testISOFormatStrip4(self):
+        self.assertEqual(parse("2003-09-25T10"),
+                         datetime(2003, 9, 25, 10))
+
+    def testISOFormatStrip5(self):
+        self.assertEqual(parse("2003-09-25"),
+                         datetime(2003, 9, 25))
+
+    def testISOStrippedFormat(self):
+        self.assertEqual(parse("20030925T104941.5-0300"),
+                         datetime(2003, 9, 25, 10, 49, 41, 500000,
+                                  tzinfo=self.brsttz))
+
+    def testISOStrippedFormatStrip1(self):
+        self.assertEqual(parse("20030925T104941-0300"),
+                         datetime(2003, 9, 25, 10, 49, 41,
+                                  tzinfo=self.brsttz))
+
+    def testISOStrippedFormatStrip2(self):
+        self.assertEqual(parse("20030925T104941"),
+                         datetime(2003, 9, 25, 10, 49, 41))
+
+    def testISOStrippedFormatStrip3(self):
+        self.assertEqual(parse("20030925T1049"),
+                         datetime(2003, 9, 25, 10, 49, 0))
+
+    def testISOStrippedFormatStrip4(self):
+        self.assertEqual(parse("20030925T10"),
+                         datetime(2003, 9, 25, 10))
+
+    def testISOStrippedFormatStrip5(self):
+        self.assertEqual(parse("20030925"),
+                         datetime(2003, 9, 25))
+
+    def testPythonLoggerFormat(self):
+        self.assertEqual(parse("2003-09-25 10:49:41,502"),
+                         datetime(2003, 9, 25, 10, 49, 41, 502000))
+
+    def testNoSeparator1(self):
+        self.assertEqual(parse("199709020908"),
+                         datetime(1997, 9, 2, 9, 8))
+
+    def testNoSeparator2(self):
+        self.assertEqual(parse("19970902090807"),
+                         datetime(1997, 9, 2, 9, 8, 7))
+
+    def testDateWithDash1(self):
+        self.assertEqual(parse("2003-09-25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash2(self):
+        self.assertEqual(parse("2003-Sep-25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash3(self):
+        self.assertEqual(parse("25-Sep-2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash4(self):
+        self.assertEqual(parse("25-Sep-2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash5(self):
+        self.assertEqual(parse("Sep-25-2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash6(self):
+        self.assertEqual(parse("09-25-2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash7(self):
+        self.assertEqual(parse("25-09-2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDash8(self):
+        self.assertEqual(parse("10-09-2003", dayfirst=True),
+                         datetime(2003, 9, 10))
+
+    def testDateWithDash9(self):
+        self.assertEqual(parse("10-09-2003"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithDash10(self):
+        self.assertEqual(parse("10-09-03"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithDash11(self):
+        self.assertEqual(parse("10-09-03", yearfirst=True),
+                         datetime(2010, 9, 3))
+
+    def testDateWithDot1(self):
+        self.assertEqual(parse("2003.09.25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot2(self):
+        self.assertEqual(parse("2003.Sep.25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot3(self):
+        self.assertEqual(parse("25.Sep.2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot4(self):
+        self.assertEqual(parse("25.Sep.2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot5(self):
+        self.assertEqual(parse("Sep.25.2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot6(self):
+        self.assertEqual(parse("09.25.2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot7(self):
+        self.assertEqual(parse("25.09.2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithDot8(self):
+        self.assertEqual(parse("10.09.2003", dayfirst=True),
+                         datetime(2003, 9, 10))
+
+    def testDateWithDot9(self):
+        self.assertEqual(parse("10.09.2003"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithDot10(self):
+        self.assertEqual(parse("10.09.03"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithDot11(self):
+        self.assertEqual(parse("10.09.03", yearfirst=True),
+                         datetime(2010, 9, 3))
+
+    def testDateWithSlash1(self):
+        self.assertEqual(parse("2003/09/25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash2(self):
+        self.assertEqual(parse("2003/Sep/25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash3(self):
+        self.assertEqual(parse("25/Sep/2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash4(self):
+        self.assertEqual(parse("25/Sep/2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash5(self):
+        self.assertEqual(parse("Sep/25/2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash6(self):
+        self.assertEqual(parse("09/25/2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash7(self):
+        self.assertEqual(parse("25/09/2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSlash8(self):
+        self.assertEqual(parse("10/09/2003", dayfirst=True),
+                         datetime(2003, 9, 10))
+
+    def testDateWithSlash9(self):
+        self.assertEqual(parse("10/09/2003"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithSlash10(self):
+        self.assertEqual(parse("10/09/03"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithSlash11(self):
+        self.assertEqual(parse("10/09/03", yearfirst=True),
+                         datetime(2010, 9, 3))
+
+    def testDateWithSpace1(self):
+        self.assertEqual(parse("2003 09 25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace2(self):
+        self.assertEqual(parse("2003 Sep 25"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace3(self):
+        self.assertEqual(parse("25 Sep 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace4(self):
+        self.assertEqual(parse("25 Sep 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace5(self):
+        self.assertEqual(parse("Sep 25 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace6(self):
+        self.assertEqual(parse("09 25 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace7(self):
+        self.assertEqual(parse("25 09 2003"),
+                         datetime(2003, 9, 25))
+
+    def testDateWithSpace8(self):
+        self.assertEqual(parse("10 09 2003", dayfirst=True),
+                         datetime(2003, 9, 10))
+
+    def testDateWithSpace9(self):
+        self.assertEqual(parse("10 09 2003"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithSpace10(self):
+        self.assertEqual(parse("10 09 03"),
+                         datetime(2003, 10, 9))
+
+    def testDateWithSpace11(self):
+        self.assertEqual(parse("10 09 03", yearfirst=True),
+                         datetime(2010, 9, 3))
+
+    def testDateWithSpace12(self):
+        self.assertEqual(parse("25 09 03"),
+                         datetime(2003, 9, 25))
+
+    def testStrangelyOrderedDate1(self):
+        self.assertEqual(parse("03 25 Sep"),
+                         datetime(2003, 9, 25))
+
+    def testStrangelyOrderedDate2(self):
+        self.assertEqual(parse("2003 25 Sep"),
+                         datetime(2003, 9, 25))
+
+    def testStrangelyOrderedDate3(self):
+        self.assertEqual(parse("25 03 Sep"),
+                         datetime(2025, 9, 3))
+
+    def testHourWithLetters(self):
+        self.assertEqual(parse("10h36m28.5s", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28, 500000))
+
+    def testHourWithLettersStrip1(self):
+        self.assertEqual(parse("10h36m28s", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 28))
+
+    def testHourWithLettersStrip2(self):
+        self.assertEqual(parse("10h36m", default=self.default),
+                         datetime(2003, 9, 25, 10, 36))
+
+    def testHourWithLettersStrip3(self):
+        self.assertEqual(parse("10h", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourWithLettersStrip4(self):
+        self.assertEqual(parse("10 h 36", default=self.default),
+                         datetime(2003, 9, 25, 10, 36))
+
+    def testAMPMNoHour(self):
+        with self.assertRaises(ValueError):
+            parse("AM")
+
+        with self.assertRaises(ValueError):
+            parse("Jan 20, 2015 PM")
+
+    def testHourAmPm1(self):
+        self.assertEqual(parse("10h am", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm2(self):
+        self.assertEqual(parse("10h pm", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testHourAmPm3(self):
+        self.assertEqual(parse("10am", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm4(self):
+        self.assertEqual(parse("10pm", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testHourAmPm5(self):
+        self.assertEqual(parse("10:00 am", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm6(self):
+        self.assertEqual(parse("10:00 pm", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testHourAmPm7(self):
+        self.assertEqual(parse("10:00am", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm8(self):
+        self.assertEqual(parse("10:00pm", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testHourAmPm9(self):
+        self.assertEqual(parse("10:00a.m", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm10(self):
+        self.assertEqual(parse("10:00p.m", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testHourAmPm11(self):
+        self.assertEqual(parse("10:00a.m.", default=self.default),
+                         datetime(2003, 9, 25, 10))
+
+    def testHourAmPm12(self):
+        self.assertEqual(parse("10:00p.m.", default=self.default),
+                         datetime(2003, 9, 25, 22))
+
+    def testAMPMRange(self):
+        with self.assertRaises(ValueError):
+            parse("13:44 AM")
+
+        with self.assertRaises(ValueError):
+            parse("January 25, 1921 23:13 PM")
+
+    def testPertain(self):
+        self.assertEqual(parse("Sep 03", default=self.default),
+                         datetime(2003, 9, 3))
+        self.assertEqual(parse("Sep of 03", default=self.default),
+                         datetime(2003, 9, 25))
+
+    def testWeekdayAlone(self):
+        self.assertEqual(parse("Wed", default=self.default),
+                         datetime(2003, 10, 1))
+
+    def testLongWeekday(self):
+        self.assertEqual(parse("Wednesday", default=self.default),
+                         datetime(2003, 10, 1))
+
+    def testLongMonth(self):
+        self.assertEqual(parse("October", default=self.default),
+                         datetime(2003, 10, 25))
+
+    def testZeroYear(self):
+        self.assertEqual(parse("31-Dec-00", default=self.default),
+                         datetime(2000, 12, 31))
+
+    def testFuzzy(self):
+        s = "Today is 25 of September of 2003, exactly " \
+            "at 10:49:41 with timezone -03:00."
+        self.assertEqual(parse(s, fuzzy=True),
+                         datetime(2003, 9, 25, 10, 49, 41,
+                                  tzinfo=self.brsttz))
+
+    def testFuzzyWithTokens(self):
+        s = "Today is 25 of September of 2003, exactly " \
+            "at 10:49:41 with timezone -03:00."
+        self.assertEqual(parse(s, fuzzy_with_tokens=True),
+                         (datetime(2003, 9, 25, 10, 49, 41,
+                                   tzinfo=self.brsttz),
+                         ('Today is ', 'of ', ', exactly at ',
+                          ' with timezone ', '.')))
+
+    def testFuzzyAMPMProblem(self):
+        # Sometimes fuzzy parsing results in AM/PM flag being set without
+        # hours - if it's fuzzy it should ignore that.
+        s1 = "I have a meeting on March 1, 1974."
+        s2 = "On June 8th, 2020, I am going to be the first man on Mars"
+
+        # Also don't want any erroneous AM or PMs changing the parsed time
+        s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003"
+        s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset"
+
+        self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1))
+        self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8))
+        self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3))
+        self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3))
+
+    def testFuzzyIgnoreAMPM(self):
+        s1 = "Jan 29, 1945 14:45 AM I going to see you there?"
+
+        self.assertEqual(parse(s1, fuzzy=True), datetime(1945, 1, 29, 14, 45))
+
+    def testExtraSpace(self):
+        self.assertEqual(parse("  July   4 ,  1976   12:01:02   am  "),
+                         datetime(1976, 7, 4, 0, 1, 2))
+
+    def testRandomFormat1(self):
+        self.assertEqual(parse("Wed, July 10, '96"),
+                         datetime(1996, 7, 10, 0, 0))
+
+    def testRandomFormat2(self):
+        self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT",
+                               ignoretz=True),
+                         datetime(1996, 7, 10, 15, 8, 56))
+
+    def testRandomFormat3(self):
+        self.assertEqual(parse("1996.July.10 AD 12:08 PM"),
+                         datetime(1996, 7, 10, 12, 8))
+
+    def testRandomFormat4(self):
+        self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST",
+                               ignoretz=True),
+                         datetime(1952, 4, 12, 15, 30, 42))
+
+    def testRandomFormat5(self):
+        self.assertEqual(parse("November 5, 1994, 8:15:30 am EST",
+                               ignoretz=True),
+                         datetime(1994, 11, 5, 8, 15, 30))
+
+    def testRandomFormat6(self):
+        self.assertEqual(parse("1994-11-05T08:15:30-05:00",
+                               ignoretz=True),
+                         datetime(1994, 11, 5, 8, 15, 30))
+
+    def testRandomFormat7(self):
+        self.assertEqual(parse("1994-11-05T08:15:30Z",
+                               ignoretz=True),
+                         datetime(1994, 11, 5, 8, 15, 30))
+
+    def testRandomFormat8(self):
+        self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4))
+
+    def testRandomFormat9(self):
+        self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4))
+
+    def testRandomFormat10(self):
+        self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4))
+
+    def testRandomFormat11(self):
+        self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4))
+
+    def testRandomFormat12(self):
+        self.assertEqual(parse("19760704"), datetime(1976, 7, 4))
+
+    def testRandomFormat13(self):
+        self.assertEqual(parse("0:01:02", default=self.default),
+                         datetime(2003, 9, 25, 0, 1, 2))
+
+    def testRandomFormat14(self):
+        self.assertEqual(parse("12h 01m02s am", default=self.default),
+                         datetime(2003, 9, 25, 0, 1, 2))
+
+    def testRandomFormat15(self):
+        self.assertEqual(parse("0:01:02 on July 4, 1976"),
+                         datetime(1976, 7, 4, 0, 1, 2))
+
+    def testRandomFormat16(self):
+        self.assertEqual(parse("0:01:02 on July 4, 1976"),
+                         datetime(1976, 7, 4, 0, 1, 2))
+
+    def testRandomFormat17(self):
+        self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True),
+                         datetime(1976, 7, 4, 0, 1, 2))
+
+    def testRandomFormat18(self):
+        self.assertEqual(parse("July 4, 1976 12:01:02 am"),
+                         datetime(1976, 7, 4, 0, 1, 2))
+
+    def testRandomFormat19(self):
+        self.assertEqual(parse("Mon Jan  2 04:24:27 1995"),
+                         datetime(1995, 1, 2, 4, 24, 27))
+
+    def testRandomFormat20(self):
+        self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True),
+                         datetime(1995, 4, 4, 0, 22, 12))
+
+    def testRandomFormat21(self):
+        self.assertEqual(parse("04.04.95 00:22"),
+                         datetime(1995, 4, 4, 0, 22))
+
+    def testRandomFormat22(self):
+        self.assertEqual(parse("Jan 1 1999 11:23:34.578"),
+                         datetime(1999, 1, 1, 11, 23, 34, 578000))
+
+    def testRandomFormat23(self):
+        self.assertEqual(parse("950404 122212"),
+                         datetime(1995, 4, 4, 12, 22, 12))
+
+    def testRandomFormat24(self):
+        self.assertEqual(parse("0:00 PM, PST", default=self.default,
+                               ignoretz=True),
+                         datetime(2003, 9, 25, 12, 0))
+
+    def testRandomFormat25(self):
+        self.assertEqual(parse("12:08 PM", default=self.default),
+                         datetime(2003, 9, 25, 12, 8))
+
+    def testRandomFormat26(self):
+        self.assertEqual(parse("5:50 A.M. on June 13, 1990"),
+                         datetime(1990, 6, 13, 5, 50))
+
+    def testRandomFormat27(self):
+        self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3))
+
+    def testRandomFormat28(self):
+        self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5))
+
+    def testRandomFormat29(self):
+        self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1))
+
+    def testRandomFormat30(self):
+        self.assertEqual(parse("01h02m03", default=self.default),
+                         datetime(2003, 9, 25, 1, 2, 3))
+
+    def testRandomFormat31(self):
+        self.assertEqual(parse("01h02", default=self.default),
+                         datetime(2003, 9, 25, 1, 2))
+
+    def testRandomFormat32(self):
+        self.assertEqual(parse("01h02s", default=self.default),
+                         datetime(2003, 9, 25, 1, 0, 2))
+
+    def testRandomFormat33(self):
+        self.assertEqual(parse("01m02", default=self.default),
+                         datetime(2003, 9, 25, 0, 1, 2))
+
+    def testRandomFormat34(self):
+        self.assertEqual(parse("01m02h", default=self.default),
+                         datetime(2003, 9, 25, 2, 1))
+
+    def testRandomFormat35(self):
+        self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default),
+                         datetime(2004, 4, 10, 11, 30))
+
+    def test_99_ad(self):
+        self.assertEqual(parse('0099-01-01T00:00:00'),
+                         datetime(99, 1, 1, 0, 0))
+
+    def test_31_ad(self):
+        self.assertEqual(parse('0031-01-01T00:00:00'),
+                         datetime(31, 1, 1, 0, 0))
+
+    def testInvalidDay(self):
+        with self.assertRaises(ValueError):
+            parse("Feb 30, 2007")
+
+    def testUnspecifiedDayFallback(self):
+        # Test that for an unspecified day, the fallback behavior is correct.
+        self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)),
+                         datetime(2009, 4, 30))
+
+    def testUnspecifiedDayFallbackFebNoLeapYear(self):        
+        self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)),
+                         datetime(2007, 2, 28))
+
+    def testUnspecifiedDayFallbackFebLeapYear(self):        
+        self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)),
+                         datetime(2008, 2, 29))
+
+    def testErrorType01(self):
+        self.assertRaises(ValueError,
+                          parse, 'shouldfail')
+
+    def testCorrectErrorOnFuzzyWithTokens(self):
+        assertRaisesRegex(self, ValueError, 'Unknown string format',
+                          parse, '04/04/32/423', fuzzy_with_tokens=True)
+        assertRaisesRegex(self, ValueError, 'Unknown string format',
+                          parse, '04/04/04 +32423', fuzzy_with_tokens=True)
+        assertRaisesRegex(self, ValueError, 'Unknown string format',
+                          parse, '04/04/0d4', fuzzy_with_tokens=True)
+
+    def testIncreasingCTime(self):
+        # This test will check 200 different years, every month, every day,
+        # every hour, every minute, every second, and every weekday, using
+        # a delta of more or less 1 year, 1 month, 1 day, 1 minute and
+        # 1 second.
+        delta = timedelta(days=365+31+1, seconds=1+60+60*60)
+        dt = datetime(1900, 1, 1, 0, 0, 0, 0)
+        for i in range(200):
+            self.assertEqual(parse(dt.ctime()), dt)
+            dt += delta
+
+    def testIncreasingISOFormat(self):
+        delta = timedelta(days=365+31+1, seconds=1+60+60*60)
+        dt = datetime(1900, 1, 1, 0, 0, 0, 0)
+        for i in range(200):
+            self.assertEqual(parse(dt.isoformat()), dt)
+            dt += delta
+
+    def testMicrosecondsPrecisionError(self):
+        # Skip found out that sad precision problem. :-(
+        dt1 = parse("00:11:25.01")
+        dt2 = parse("00:12:10.01")
+        self.assertEqual(dt1.microsecond, 10000)
+        self.assertEqual(dt2.microsecond, 10000)
+
+    def testMicrosecondPrecisionErrorReturns(self):
+        # One more precision issue, discovered by Eric Brown.  This should
+        # be the last one, as we're no longer using floating points.
+        for ms in [100001, 100000, 99999, 99998,
+                    10001,  10000,  9999,  9998,
+                     1001,   1000,   999,   998,
+                      101,    100,    99,    98]:
+            dt = datetime(2008, 2, 27, 21, 26, 1, ms)
+            self.assertEqual(parse(dt.isoformat()), dt)
+
+    def testHighPrecisionSeconds(self):
+        self.assertEqual(parse("20080227T21:26:01.123456789"),
+                          datetime(2008, 2, 27, 21, 26, 1, 123456))
+
+    def testCustomParserInfo(self):
+        # Custom parser info wasn't working, as Michael Elsdörfer discovered.
+        from dateutil.parser import parserinfo, parser
+
+        class myparserinfo(parserinfo):
+            MONTHS = parserinfo.MONTHS[:]
+            MONTHS[0] = ("Foo", "Foo")
+        myparser = parser(myparserinfo())
+        dt = myparser.parse("01/Foo/2007")
+        self.assertEqual(dt, datetime(2007, 1, 1))
+
+    def testNoYearFirstNoDayFirst(self):
+        dtstr = '090107'
+
+        # Should be MMDDYY
+        self.assertEqual(parse(dtstr),
+                         datetime(2007, 9, 1))
+
+        self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False),
+                         datetime(2007, 9, 1))
+
+    def testYearFirst(self):
+        dtstr = '090107'
+
+        # Should be MMDDYY
+        self.assertEqual(parse(dtstr, yearfirst=True),
+                         datetime(2009, 1, 7))
+
+        self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False),
+                         datetime(2009, 1, 7))
+
+    def testDayFirst(self):
+        dtstr = '090107'
+
+        # Should be DDMMYY
+        self.assertEqual(parse(dtstr, dayfirst=True),
+                         datetime(2007, 1, 9))
+
+        self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True),
+                         datetime(2007, 1, 9))
+
+    def testDayFirstYearFirst(self):
+        dtstr = '090107'
+        # Should be YYDDMM
+        self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True),
+                         datetime(2009, 7, 1))
+
+    def testUnambiguousYearFirst(self):
+        dtstr = '2015 09 25'
+        self.assertEqual(parse(dtstr, yearfirst=True),
+                         datetime(2015, 9, 25))
+
+    def testUnambiguousDayFirst(self):
+        dtstr = '2015 09 25'
+        self.assertEqual(parse(dtstr, dayfirst=True), 
+                         datetime(2015, 9, 25))
+
+    def testUnambiguousDayFirstYearFirst(self):
+        dtstr = '2015 09 25'
+        self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True),
+                         datetime(2015, 9, 25))
+
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_relativedelta.py
@@ -0,0 +1,571 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import unittest, WarningTestMixin, NotAValue
+
+import calendar
+from datetime import datetime, date, timedelta
+
+from dateutil.relativedelta import *
+
+
+class RelativeDeltaTest(WarningTestMixin, unittest.TestCase):
+    now = datetime(2003, 9, 17, 20, 54, 47, 282310)
+    today = date(2003, 9, 17)
+
+    def testInheritance(self):
+        # Ensure that relativedelta is inheritance-friendly.
+        class rdChildClass(relativedelta):
+            pass
+
+        ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1,
+                            hours=1, minutes=1, seconds=1, microseconds=1)
+
+        rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1,
+                           hours=1, minutes=1, seconds=1, microseconds=1)
+
+        self.assertEqual(type(ccRD + rd), type(ccRD),
+                         msg='Addition does not inherit type.')
+
+        self.assertEqual(type(ccRD - rd), type(ccRD),
+                         msg='Subtraction does not inherit type.')
+
+        self.assertEqual(type(-ccRD), type(ccRD),
+                         msg='Negation does not inherit type.')
+
+        self.assertEqual(type(ccRD * 5.0), type(ccRD),
+                         msg='Multiplication does not inherit type.')
+
+        self.assertEqual(type(ccRD / 5.0), type(ccRD),
+                         msg='Division does not inherit type.')
+
+    def testMonthEndMonthBeginning(self):
+        self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59),
+                                       datetime(2003, 3, 1, 0, 0, 0)),
+                         relativedelta(months=-1, seconds=-1))
+
+        self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
+                                       datetime(2003, 1, 31, 23, 59, 59)),
+                         relativedelta(months=1, seconds=1))
+
+    def testMonthEndMonthBeginningLeapYear(self):
+        self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59),
+                                       datetime(2012, 3, 1, 0, 0, 0)),
+                         relativedelta(months=-1, seconds=-1))
+
+        self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
+                                       datetime(2003, 1, 31, 23, 59, 59)),
+                         relativedelta(months=1, seconds=1))
+
+    def testNextMonth(self):
+        self.assertEqual(self.now+relativedelta(months=+1),
+                         datetime(2003, 10, 17, 20, 54, 47, 282310))
+
+    def testNextMonthPlusOneWeek(self):
+        self.assertEqual(self.now+relativedelta(months=+1, weeks=+1),
+                         datetime(2003, 10, 24, 20, 54, 47, 282310))
+
+    def testNextMonthPlusOneWeek10am(self):
+        self.assertEqual(self.today +
+                         relativedelta(months=+1, weeks=+1, hour=10),
+                         datetime(2003, 10, 24, 10, 0))
+
+    def testNextMonthPlusOneWeek10amDiff(self):
+        self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0),
+                                       self.today),
+                         relativedelta(months=+1, days=+7, hours=+10))
+
+    def testOneMonthBeforeOneYear(self):
+        self.assertEqual(self.now+relativedelta(years=+1, months=-1),
+                         datetime(2004, 8, 17, 20, 54, 47, 282310))
+
+    def testMonthsOfDiffNumOfDays(self):
+        self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1),
+                         date(2003, 2, 27))
+        self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1),
+                         date(2003, 2, 28))
+        self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2),
+                         date(2003, 3, 31))
+
+    def testMonthsOfDiffNumOfDaysWithYears(self):
+        self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1),
+                         date(2001, 2, 28))
+        self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1),
+                         date(2001, 2, 28))
+
+        self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1),
+                         date(2000, 2, 28))
+        self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
+                         date(2000, 3, 1))
+        self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
+                         date(2000, 3, 1))
+
+        self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1),
+                         date(2000, 2, 28))
+        self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1),
+                         date(2000, 3, 1))
+
+    def testNextFriday(self):
+        self.assertEqual(self.today+relativedelta(weekday=FR),
+                         date(2003, 9, 19))
+
+    def testNextFridayInt(self):
+        self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY),
+                         date(2003, 9, 19))
+
+    def testLastFridayInThisMonth(self):
+        self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)),
+                         date(2003, 9, 26))
+
+    def testNextWednesdayIsToday(self):
+        self.assertEqual(self.today+relativedelta(weekday=WE),
+                         date(2003, 9, 17))
+
+    def testNextWenesdayNotToday(self):
+        self.assertEqual(self.today+relativedelta(days=+1, weekday=WE),
+                         date(2003, 9, 24))
+
+    def test15thISOYearWeek(self):
+        self.assertEqual(date(2003, 1, 1) +
+                         relativedelta(day=4, weeks=+14, weekday=MO(-1)),
+                         date(2003, 4, 7))
+
+    def testMillenniumAge(self):
+        self.assertEqual(relativedelta(self.now, date(2001, 1, 1)),
+                         relativedelta(years=+2, months=+8, days=+16,
+                                       hours=+20, minutes=+54, seconds=+47,
+                                       microseconds=+282310))
+
+    def testJohnAge(self):
+        self.assertEqual(relativedelta(self.now,
+                                       datetime(1978, 4, 5, 12, 0)),
+                         relativedelta(years=+25, months=+5, days=+12,
+                                       hours=+8, minutes=+54, seconds=+47,
+                                       microseconds=+282310))
+
+    def testJohnAgeWithDate(self):
+        self.assertEqual(relativedelta(self.today,
+                                       datetime(1978, 4, 5, 12, 0)),
+                         relativedelta(years=+25, months=+5, days=+11,
+                                       hours=+12))
+
+    def testYearDay(self):
+        self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260),
+                         date(2003, 9, 17))
+        self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260),
+                         date(2002, 9, 17))
+        self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260),
+                         date(2000, 9, 16))
+        self.assertEqual(self.today+relativedelta(yearday=261),
+                         date(2003, 9, 18))
+
+    def testYearDayBug(self):
+        # Tests a problem reported by Adam Ryan.
+        self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15),
+                         date(2010, 1, 15))
+
+    def testNonLeapYearDay(self):
+        self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260),
+                         date(2003, 9, 17))
+        self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260),
+                         date(2002, 9, 17))
+        self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260),
+                         date(2000, 9, 17))
+        self.assertEqual(self.today+relativedelta(yearday=261),
+                         date(2003, 9, 18))
+
+    def testAddition(self):
+        self.assertEqual(relativedelta(days=10) +
+                         relativedelta(years=1, months=2, days=3, hours=4,
+                                       minutes=5, microseconds=6),
+                         relativedelta(years=1, months=2, days=13, hours=4,
+                                       minutes=5, microseconds=6))
+
+    def testAdditionToDatetime(self):
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1),
+                         datetime(2000, 1, 2))
+
+    def testRightAdditionToDatetime(self):
+        self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1),
+                         datetime(2000, 1, 2))
+
+    def testAdditionInvalidType(self):
+        with self.assertRaises(TypeError):
+            relativedelta(days=3) + 9
+
+    def testAdditionUnsupportedType(self):
+        # For unsupported types that define their own comparators, etc.
+        self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
+
+    def testSubtraction(self):
+        self.assertEqual(relativedelta(days=10) -
+                         relativedelta(years=1, months=2, days=3, hours=4,
+                                       minutes=5, microseconds=6),
+                         relativedelta(years=-1, months=-2, days=7, hours=-4,
+                                       minutes=-5, microseconds=-6))
+
+    def testRightSubtractionFromDatetime(self):
+        self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1),
+                         datetime(2000, 1, 1))
+
+    def testSubractionWithDatetime(self):
+        self.assertRaises(TypeError, lambda x, y: x - y,
+                          (relativedelta(days=1), datetime(2000, 1, 1)))
+
+    def testSubtractionInvalidType(self):
+        with self.assertRaises(TypeError):
+            relativedelta(hours=12) - 14
+
+    def testSubtractionUnsupportedType(self):
+        self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
+
+    def testMultiplication(self):
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28,
+                         datetime(2000, 1, 29))
+        self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1),
+                         datetime(2000, 1, 29))
+
+    def testMultiplicationUnsupportedType(self):
+        self.assertIs(relativedelta(days=1) * NotAValue, NotAValue)
+
+    def testDivision(self):
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28,
+                         datetime(2000, 1, 2))
+
+    def testDivisionUnsupportedType(self):
+        self.assertIs(relativedelta(days=1) / NotAValue, NotAValue)
+
+    def testBoolean(self):
+        self.assertFalse(relativedelta(days=0))
+        self.assertTrue(relativedelta(days=1))
+
+    def testComparison(self):
+        d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+                           minutes=1, seconds=1, microseconds=1)
+        d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+                           minutes=1, seconds=1, microseconds=1)
+        d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
+                           minutes=1, seconds=1, microseconds=2)
+
+        self.assertEqual(d1, d2)
+        self.assertNotEqual(d1, d3)
+
+    def testInequalityTypeMismatch(self):
+        # Different type
+        self.assertFalse(relativedelta(year=1) == 19)
+
+    def testInequalityUnsupportedType(self):
+        self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue)
+
+    def testInequalityWeekdays(self):
+        # Different weekdays
+        no_wday = relativedelta(year=1997, month=4)
+        wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1))
+        wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2))
+        wday_tu = relativedelta(year=1997, month=4, weekday=TU)
+
+        self.assertTrue(wday_mo_1 == wday_mo_1)
+
+        self.assertFalse(no_wday == wday_mo_1)
+        self.assertFalse(wday_mo_1 == no_wday)
+
+        self.assertFalse(wday_mo_1 == wday_mo_2)
+        self.assertFalse(wday_mo_2 == wday_mo_1)
+
+        self.assertFalse(wday_mo_1 == wday_tu)
+        self.assertFalse(wday_tu == wday_mo_1)
+
+    def testMonthOverflow(self):
+        self.assertEqual(relativedelta(months=273),
+                         relativedelta(years=22, months=9))
+
+    def testWeeks(self):
+        # Test that the weeks property is working properly.
+        rd = relativedelta(years=4, months=2, weeks=8, days=6)
+        self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6))
+
+        rd.weeks = 3
+        self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6))
+
+    def testRelativeDeltaRepr(self):
+        self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)),
+                         'relativedelta(years=+1, months=-1, days=+15)')
+
+        self.assertEqual(repr(relativedelta(months=14, seconds=-25)),
+                         'relativedelta(years=+1, months=+2, seconds=-25)')
+
+        self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))),
+                         'relativedelta(month=3, weekday=SU(+3), hour=3)')
+
+    def testRelativeDeltaFractionalYear(self):
+        with self.assertRaises(ValueError):
+            relativedelta(years=1.5)
+
+    def testRelativeDeltaFractionalMonth(self):
+        with self.assertRaises(ValueError):
+            relativedelta(months=1.5)
+
+    def testRelativeDeltaFractionalAbsolutes(self):
+        # Fractional absolute values will soon be unsupported,
+        # check for the deprecation warning.
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(year=2.86)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(month=1.29)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(day=0.44)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(hour=23.98)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(minute=45.21)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(second=13.2)
+
+        with self.assertWarns(DeprecationWarning):
+            relativedelta(microsecond=157221.93)
+
+    def testRelativeDeltaFractionalRepr(self):
+        rd = relativedelta(years=3, months=-2, days=1.25)
+
+        self.assertEqual(repr(rd),
+                         'relativedelta(years=+3, months=-2, days=+1.25)')
+
+        rd = relativedelta(hours=0.5, seconds=9.22)
+        self.assertEqual(repr(rd),
+                         'relativedelta(hours=+0.5, seconds=+9.22)')
+
+    def testRelativeDeltaFractionalWeeks(self):
+        # Equivalent to days=8, hours=18
+        rd = relativedelta(weeks=1.25)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd,
+                         datetime(2009, 9, 11, 18))
+
+    def testRelativeDeltaFractionalDays(self):
+        rd1 = relativedelta(days=1.48)
+
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd1,
+                         datetime(2009, 9, 4, 11, 31, 12))
+
+        rd2 = relativedelta(days=1.5)
+        self.assertEqual(d1 + rd2,
+                         datetime(2009, 9, 4, 12, 0, 0))
+
+    def testRelativeDeltaFractionalHours(self):
+        rd = relativedelta(days=1, hours=12.5)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd,
+                         datetime(2009, 9, 4, 12, 30, 0))
+
+    def testRelativeDeltaFractionalMinutes(self):
+        rd = relativedelta(hours=1, minutes=30.5)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd,
+                         datetime(2009, 9, 3, 1, 30, 30))
+
+    def testRelativeDeltaFractionalSeconds(self):
+        rd = relativedelta(hours=5, minutes=30, seconds=30.5)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd,
+                         datetime(2009, 9, 3, 5, 30, 30, 500000))
+
+    def testRelativeDeltaFractionalPositiveOverflow(self):
+        # Equivalent to (days=1, hours=14)
+        rd1 = relativedelta(days=1.5, hours=2)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd1,
+                         datetime(2009, 9, 4, 14, 0, 0))
+
+        # Equivalent to (days=1, hours=14, minutes=45)
+        rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
+        d1 = datetime(2009, 9, 3, 0, 0)
+        self.assertEqual(d1 + rd2,
+                         datetime(2009, 9, 4, 14, 45))
+
+        # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1)
+        rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31)
+        self.assertEqual(d1 + rd3,
+                         datetime(2009, 9, 5, 2, 0, 1))
+
+    def testRelativeDeltaFractionalNegativeDays(self):
+        # Equivalent to (days=-1, hours=-1)
+        rd1 = relativedelta(days=-1.5, hours=11)
+        d1 = datetime(2009, 9, 3, 12, 0)
+        self.assertEqual(d1 + rd1,
+                         datetime(2009, 9, 2, 11, 0, 0))
+
+        # Equivalent to (days=-1, hours=-9)
+        rd2 = relativedelta(days=-1.25, hours=-3)
+        self.assertEqual(d1 + rd2,
+            datetime(2009, 9, 2, 3))
+
+    def testRelativeDeltaNormalizeFractionalDays(self):
+        # Equivalent to (days=2, hours=18)
+        rd1 = relativedelta(days=2.75)
+
+        self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18))
+
+        # Equvalent to (days=1, hours=11, minutes=31, seconds=12)
+        rd2 = relativedelta(days=1.48)
+
+        self.assertEqual(rd2.normalized(),
+            relativedelta(days=1, hours=11, minutes=31, seconds=12))
+
+    def testRelativeDeltaNormalizeFractionalDays(self):
+        # Equivalent to (hours=1, minutes=30)
+        rd1 = relativedelta(hours=1.5)
+
+        self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30))
+
+        # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100)
+        rd2 = relativedelta(hours=3.28472225)
+
+        self.assertEqual(rd2.normalized(),
+            relativedelta(hours=3, minutes=17, seconds=5, microseconds=100))
+
+    def testRelativeDeltaNormalizeFractionalMinutes(self):
+        # Equivalent to (minutes=15, seconds=36)
+        rd1 = relativedelta(minutes=15.6)
+
+        self.assertEqual(rd1.normalized(),
+            relativedelta(minutes=15, seconds=36))
+
+        # Equivalent to (minutes=25, seconds=20, microseconds=25000)
+        rd2 = relativedelta(minutes=25.33375)
+
+        self.assertEqual(rd2.normalized(),
+            relativedelta(minutes=25, seconds=20, microseconds=25000))
+
+    def testRelativeDeltaNormalizeFractionalSeconds(self):
+        # Equivalent to (seconds=45, microseconds=25000)
+        rd1 = relativedelta(seconds=45.025)
+        self.assertEqual(rd1.normalized(),
+            relativedelta(seconds=45, microseconds=25000))
+
+    def testRelativeDeltaFractionalPositiveOverflow(self):
+        # Equivalent to (days=1, hours=14)
+        rd1 = relativedelta(days=1.5, hours=2)
+        self.assertEqual(rd1.normalized(),
+            relativedelta(days=1, hours=14))
+
+        # Equivalent to (days=1, hours=14, minutes=45)
+        rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
+        self.assertEqual(rd2.normalized(),
+            relativedelta(days=1, hours=14, minutes=45))
+
+        # Carry back up - equivalent to:
+        # (days=2, hours=2, minutes=0, seconds=2, microseconds=3)
+        rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045,
+                            seconds=31.473, microseconds=500003)
+        self.assertEqual(rd3.normalized(),
+            relativedelta(days=2, hours=2, minutes=0,
+                          seconds=2, microseconds=3))
+
+    def testRelativeDeltaFractionalNegativeOverflow(self):
+        # Equivalent to (days=-1)
+        rd1 = relativedelta(days=-0.5, hours=-12)
+        self.assertEqual(rd1.normalized(),
+            relativedelta(days=-1))
+
+        # Equivalent to (days=-1)
+        rd2 = relativedelta(days=-1.5, hours=12)
+        self.assertEqual(rd2.normalized(),
+            relativedelta(days=-1))
+
+        # Equivalent to (days=-1, hours=-14, minutes=-45)
+        rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15)
+        self.assertEqual(rd3.normalized(),
+            relativedelta(days=-1, hours=-14, minutes=-45))
+
+        # Equivalent to (days=-1, hours=-14, minutes=+15)
+        rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45)
+        self.assertEqual(rd4.normalized(),
+            relativedelta(days=-1, hours=-14, minutes=+15))
+
+        # Carry back up - equivalent to:
+        # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3)
+        rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045,
+                            seconds=-31.473, microseconds=-500003)
+        self.assertEqual(rd3.normalized(),
+            relativedelta(days=-2, hours=-2, minutes=0,
+                          seconds=-2, microseconds=-3))
+
+    def testInvalidYearDay(self):
+        with self.assertRaises(ValueError):
+            relativedelta(yearday=367)
+
+    def testAddTimedeltaToUnpopulatedRelativedelta(self):
+        td = timedelta(
+            days=1,
+            seconds=1,
+            microseconds=1,
+            milliseconds=1,
+            minutes=1,
+            hours=1,
+            weeks=1
+        )
+
+        expected = relativedelta(
+            weeks=1,
+            days=1,
+            hours=1,
+            minutes=1,
+            seconds=1,
+            microseconds=1001
+        )
+
+        self.assertEqual(expected, relativedelta() + td)
+
+    def testAddTimedeltaToPopulatedRelativeDelta(self):
+        td = timedelta(
+            days=1,
+            seconds=1,
+            microseconds=1,
+            milliseconds=1,
+            minutes=1,
+            hours=1,
+            weeks=1
+        )
+
+        rd = relativedelta(
+            year=1,
+            month=1,
+            day=1,
+            hour=1,
+            minute=1,
+            second=1,
+            microsecond=1,
+            years=1,
+            months=1,
+            days=1,
+            weeks=1,
+            hours=1,
+            minutes=1,
+            seconds=1,
+            microseconds=1
+        )
+
+        expected = relativedelta(
+            year=1,
+            month=1,
+            day=1,
+            hour=1,
+            minute=1,
+            second=1,
+            microsecond=1,
+            years=1,
+            months=1,
+            weeks=2,
+            days=2,
+            hours=2,
+            minutes=2,
+            seconds=2,
+            microseconds=1002,
+        )
+
+        self.assertEqual(expected, rd + td)
new file mode 100644
--- /dev/null
+++ b/lib/python/vendor/python-dateutil-2.6.0/dateutil/test/test_rrule.py
@@ -0,0 +1,4701 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import WarningTestMixin, unittest
+
+import calendar
+from datetime import datetime, date
+from six import PY3
+
+from dateutil.rrule import *
+
+
+class RRuleTest(WarningTestMixin, unittest.TestCase):
+    def _rrulestr_reverse_test(self, rule):
+        """
+        Call with an `rrule` and it will test that `str(rrule)` generates a
+        string which generates the same `rrule` as the input when passed to
+        `rrulestr()`
+        """
+        rr_str = str(rule)
+        rrulestr_rrule = rrulestr(rr_str)
+
+        self.assertEqual(list(rule), list(rrulestr_rrule))
+
+    def testYearly(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0)])
+
+    def testYearlyInterval(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0),
+                          datetime(2001, 9, 2, 9, 0)])
+
+    def testYearlyIntervalLarge(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              interval=100,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(2097, 9, 2, 9, 0),
+                          datetime(2197, 9, 2, 9, 0)])
+
+    def testYearlyByMonth(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 2, 9, 0),
+                          datetime(1998, 3, 2, 9, 0),
+                          datetime(1999, 1, 2, 9, 0)])
+
+    def testYearlyByMonthDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 10, 1, 9, 0),
+                          datetime(1997, 10, 3, 9, 0)])
+
+    def testYearlyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 9, 0),
+                          datetime(1998, 1, 7, 9, 0),
+                          datetime(1998, 3, 5, 9, 0)])
+
+    def testYearlyByWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testYearlyByNWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 25, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 12, 31, 9, 0)])
+
+    def testYearlyByNWeekDayLarge(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 11, 9, 0),
+                          datetime(1998, 1, 20, 9, 0),
+                          datetime(1998, 12, 17, 9, 0)])
+
+    def testYearlyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testYearlyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 29, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testYearlyByMonthAndNWeekDayLarge(self):
+        # This is interesting because the TH(-3) ends up before
+        # the TU(3).
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 15, 9, 0),
+                          datetime(1998, 1, 20, 9, 0),
+                          datetime(1998, 3, 12, 9, 0)])
+
+    def testYearlyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 2, 3, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testYearlyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 3, 3, 9, 0),
+                          datetime(2001, 3, 1, 9, 0)])
+
+    def testYearlyByYearDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testYearlyByYearDayNeg(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testYearlyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 4, 10, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testYearlyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 4, 10, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testYearlyByWeekNo(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 9, 0),
+                          datetime(1998, 5, 12, 9, 0),
+                          datetime(1998, 5, 13, 9, 0)])
+
+    def testYearlyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 9, 0),
+                          datetime(1999, 1, 4, 9, 0),
+                          datetime(2000, 1, 3, 9, 0)])
+
+    def testYearlyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1998, 12, 27, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testYearlyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1999, 1, 3, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testYearlyByEaster(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 9, 0),
+                          datetime(1999, 4, 4, 9, 0),
+                          datetime(2000, 4, 23, 9, 0)])
+
+    def testYearlyByEasterPos(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 9, 0),
+                          datetime(1999, 4, 5, 9, 0),
+                          datetime(2000, 4, 24, 9, 0)])
+
+    def testYearlyByEasterNeg(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 9, 0),
+                          datetime(1999, 4, 3, 9, 0),
+                          datetime(2000, 4, 22, 9, 0)])
+
+    def testYearlyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 9, 0),
+                          datetime(2004, 12, 27, 9, 0),
+                          datetime(2009, 12, 28, 9, 0)])
+
+    def testYearlyByHour(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1998, 9, 2, 6, 0),
+                          datetime(1998, 9, 2, 18, 0)])
+
+    def testYearlyByMinute(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1998, 9, 2, 9, 6)])
+
+    def testYearlyBySecond(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1998, 9, 2, 9, 0, 6)])
+
+    def testYearlyByHourAndMinute(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1998, 9, 2, 6, 6)])
+
+    def testYearlyByHourAndSecond(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1998, 9, 2, 6, 0, 6)])
+
+    def testYearlyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testYearlyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testYearlyBySetPos(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonthday=15,
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 11, 15, 18, 0),
+                          datetime(1998, 2, 15, 6, 0),
+                          datetime(1998, 11, 15, 18, 0)])
+
+    def testMonthly(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 10, 2, 9, 0),
+                          datetime(1997, 11, 2, 9, 0)])
+
+    def testMonthlyInterval(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 11, 2, 9, 0),
+                          datetime(1998, 1, 2, 9, 0)])
+
+    def testMonthlyIntervalLarge(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              interval=18,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1999, 3, 2, 9, 0),
+                          datetime(2000, 9, 2, 9, 0)])
+
+    def testMonthlyByMonth(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 2, 9, 0),
+                          datetime(1998, 3, 2, 9, 0),
+                          datetime(1999, 1, 2, 9, 0)])
+
+    def testMonthlyByMonthDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 10, 1, 9, 0),
+                          datetime(1997, 10, 3, 9, 0)])
+
+    def testMonthlyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 9, 0),
+                          datetime(1998, 1, 7, 9, 0),
+                          datetime(1998, 3, 5, 9, 0)])
+
+    def testMonthlyByWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+        # Third Monday of the month
+        self.assertEqual(rrule(MONTHLY,
+                         byweekday=(MO(+3)),
+                         dtstart=datetime(1997, 9, 1)).between(datetime(1997, 9, 1),
+                                                               datetime(1997, 12, 1)),
+                         [datetime(1997, 9, 15, 0, 0),
+                          datetime(1997, 10, 20, 0, 0),
+                          datetime(1997, 11, 17, 0, 0)])
+
+    def testMonthlyByNWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 25, 9, 0),
+                          datetime(1997, 10, 7, 9, 0)])
+
+    def testMonthlyByNWeekDayLarge(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 16, 9, 0),
+                          datetime(1997, 10, 16, 9, 0)])
+
+    def testMonthlyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testMonthlyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 29, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testMonthlyByMonthAndNWeekDayLarge(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 15, 9, 0),
+                          datetime(1998, 1, 20, 9, 0),
+                          datetime(1998, 3, 12, 9, 0)])
+
+    def testMonthlyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 2, 3, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testMonthlyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 3, 3, 9, 0),
+                          datetime(2001, 3, 1, 9, 0)])
+
+    def testMonthlyByYearDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testMonthlyByYearDayNeg(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testMonthlyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 4, 10, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testMonthlyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 4, 10, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testMonthlyByWeekNo(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 9, 0),
+                          datetime(1998, 5, 12, 9, 0),
+                          datetime(1998, 5, 13, 9, 0)])
+
+    def testMonthlyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 9, 0),
+                          datetime(1999, 1, 4, 9, 0),
+                          datetime(2000, 1, 3, 9, 0)])
+
+    def testMonthlyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1998, 12, 27, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testMonthlyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1999, 1, 3, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testMonthlyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 9, 0),
+                          datetime(2004, 12, 27, 9, 0),
+                          datetime(2009, 12, 28, 9, 0)])
+
+    def testMonthlyByEaster(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 9, 0),
+                          datetime(1999, 4, 4, 9, 0),
+                          datetime(2000, 4, 23, 9, 0)])
+
+    def testMonthlyByEasterPos(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 9, 0),
+                          datetime(1999, 4, 5, 9, 0),
+                          datetime(2000, 4, 24, 9, 0)])
+
+    def testMonthlyByEasterNeg(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 9, 0),
+                          datetime(1999, 4, 3, 9, 0),
+                          datetime(2000, 4, 22, 9, 0)])
+
+    def testMonthlyByHour(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 10, 2, 6, 0),
+                          datetime(1997, 10, 2, 18, 0)])
+
+    def testMonthlyByMinute(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1997, 10, 2, 9, 6)])
+
+    def testMonthlyBySecond(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1997, 10, 2, 9, 0, 6)])
+
+    def testMonthlyByHourAndMinute(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1997, 10, 2, 6, 6)])
+
+    def testMonthlyByHourAndSecond(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1997, 10, 2, 6, 0, 6)])
+
+    def testMonthlyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testMonthlyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testMonthlyBySetPos(self):
+        self.assertEqual(list(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(13, 17),
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 13, 18, 0),
+                          datetime(1997, 9, 17, 6, 0),
+                          datetime(1997, 10, 13, 18, 0)])
+
+    def testWeekly(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testWeeklyInterval(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 16, 9, 0),
+                          datetime(1997, 9, 30, 9, 0)])
+
+    def testWeeklyIntervalLarge(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              interval=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 1, 20, 9, 0),
+                          datetime(1998, 6, 9, 9, 0)])
+
+    def testWeeklyByMonth(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 13, 9, 0),
+                          datetime(1998, 1, 20, 9, 0)])
+
+    def testWeeklyByMonthDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 10, 1, 9, 0),
+                          datetime(1997, 10, 3, 9, 0)])
+
+    def testWeeklyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 9, 0),
+                          datetime(1998, 1, 7, 9, 0),
+                          datetime(1998, 3, 5, 9, 0)])
+
+    def testWeeklyByWeekDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testWeeklyByNWeekDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testWeeklyByMonthAndWeekDay(self):
+        # This test is interesting, because it crosses the year
+        # boundary in a weekly period to find day '1' as a
+        # valid recurrence.
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testWeeklyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testWeeklyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 2, 3, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testWeeklyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 3, 3, 9, 0),
+                          datetime(2001, 3, 1, 9, 0)])
+
+    def testWeeklyByYearDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testWeeklyByYearDayNeg(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testWeeklyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 1, 1, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testWeeklyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 1, 1, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testWeeklyByWeekNo(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 9, 0),
+                          datetime(1998, 5, 12, 9, 0),
+                          datetime(1998, 5, 13, 9, 0)])
+
+    def testWeeklyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 9, 0),
+                          datetime(1999, 1, 4, 9, 0),
+                          datetime(2000, 1, 3, 9, 0)])
+
+    def testWeeklyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1998, 12, 27, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testWeeklyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1999, 1, 3, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testWeeklyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 9, 0),
+                          datetime(2004, 12, 27, 9, 0),
+                          datetime(2009, 12, 28, 9, 0)])
+
+    def testWeeklyByEaster(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 9, 0),
+                          datetime(1999, 4, 4, 9, 0),
+                          datetime(2000, 4, 23, 9, 0)])
+
+    def testWeeklyByEasterPos(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 9, 0),
+                          datetime(1999, 4, 5, 9, 0),
+                          datetime(2000, 4, 24, 9, 0)])
+
+    def testWeeklyByEasterNeg(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 9, 0),
+                          datetime(1999, 4, 3, 9, 0),
+                          datetime(2000, 4, 22, 9, 0)])
+
+    def testWeeklyByHour(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 9, 9, 6, 0),
+                          datetime(1997, 9, 9, 18, 0)])
+
+    def testWeeklyByMinute(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1997, 9, 9, 9, 6)])
+
+    def testWeeklyBySecond(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1997, 9, 9, 9, 0, 6)])
+
+    def testWeeklyByHourAndMinute(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1997, 9, 9, 6, 6)])
+
+    def testWeeklyByHourAndSecond(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1997, 9, 9, 6, 0, 6)])
+
+    def testWeeklyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testWeeklyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testWeeklyBySetPos(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 9, 4, 6, 0),
+                          datetime(1997, 9, 9, 18, 0)])
+
+    def testDaily(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testDailyInterval(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 6, 9, 0)])
+
+    def testDailyIntervalLarge(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              interval=92,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 12, 3, 9, 0),
+                          datetime(1998, 3, 5, 9, 0)])
+
+    def testDailyByMonth(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 2, 9, 0),
+                          datetime(1998, 1, 3, 9, 0)])
+
+    def testDailyByMonthDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 10, 1, 9, 0),
+                          datetime(1997, 10, 3, 9, 0)])
+
+    def testDailyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 9, 0),
+                          datetime(1998, 1, 7, 9, 0),
+                          datetime(1998, 3, 5, 9, 0)])
+
+    def testDailyByWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testDailyByNWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testDailyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testDailyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 1, 8, 9, 0)])
+
+    def testDailyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 2, 3, 9, 0),
+                          datetime(1998, 3, 3, 9, 0)])
+
+    def testDailyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 3, 3, 9, 0),
+                          datetime(2001, 3, 1, 9, 0)])
+
+    def testDailyByYearDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testDailyByYearDayNeg(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 9, 0),
+                          datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 4, 10, 9, 0),
+                          datetime(1998, 7, 19, 9, 0)])
+
+    def testDailyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 1, 1, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testDailyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 9, 0),
+                          datetime(1998, 7, 19, 9, 0),
+                          datetime(1999, 1, 1, 9, 0),
+                          datetime(1999, 7, 19, 9, 0)])
+
+    def testDailyByWeekNo(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 9, 0),
+                          datetime(1998, 5, 12, 9, 0),
+                          datetime(1998, 5, 13, 9, 0)])
+
+    def testDailyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 9, 0),
+                          datetime(1999, 1, 4, 9, 0),
+                          datetime(2000, 1, 3, 9, 0)])
+
+    def testDailyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1998, 12, 27, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testDailyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 9, 0),
+                          datetime(1999, 1, 3, 9, 0),
+                          datetime(2000, 1, 2, 9, 0)])
+
+    def testDailyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 9, 0),
+                          datetime(2004, 12, 27, 9, 0),
+                          datetime(2009, 12, 28, 9, 0)])
+
+    def testDailyByEaster(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 9, 0),
+                          datetime(1999, 4, 4, 9, 0),
+                          datetime(2000, 4, 23, 9, 0)])
+
+    def testDailyByEasterPos(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 9, 0),
+                          datetime(1999, 4, 5, 9, 0),
+                          datetime(2000, 4, 24, 9, 0)])
+
+    def testDailyByEasterNeg(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 9, 0),
+                          datetime(1999, 4, 3, 9, 0),
+                          datetime(2000, 4, 22, 9, 0)])
+
+    def testDailyByHour(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 9, 3, 6, 0),
+                          datetime(1997, 9, 3, 18, 0)])
+
+    def testDailyByMinute(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1997, 9, 3, 9, 6)])
+
+    def testDailyBySecond(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1997, 9, 3, 9, 0, 6)])
+
+    def testDailyByHourAndMinute(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1997, 9, 3, 6, 6)])
+
+    def testDailyByHourAndSecond(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1997, 9, 3, 6, 0, 6)])
+
+    def testDailyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testDailyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testDailyBySetPos(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(15, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 15),
+                          datetime(1997, 9, 3, 6, 45),
+                          datetime(1997, 9, 3, 18, 15)])
+
+    def testHourly(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 10, 0),
+                          datetime(1997, 9, 2, 11, 0)])
+
+    def testHourlyInterval(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 11, 0),
+                          datetime(1997, 9, 2, 13, 0)])
+
+    def testHourlyIntervalLarge(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              interval=769,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 10, 4, 10, 0),
+                          datetime(1997, 11, 5, 11, 0)])
+
+    def testHourlyByMonth(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 1, 0),
+                          datetime(1998, 1, 1, 2, 0)])
+
+    def testHourlyByMonthDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 0, 0),
+                          datetime(1997, 9, 3, 1, 0),
+                          datetime(1997, 9, 3, 2, 0)])
+
+    def testHourlyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 0, 0),
+                          datetime(1998, 1, 5, 1, 0),
+                          datetime(1998, 1, 5, 2, 0)])
+
+    def testHourlyByWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 10, 0),
+                          datetime(1997, 9, 2, 11, 0)])
+
+    def testHourlyByNWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 10, 0),
+                          datetime(1997, 9, 2, 11, 0)])
+
+    def testHourlyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 1, 0),
+                          datetime(1998, 1, 1, 2, 0)])
+
+    def testHourlyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 1, 0),
+                          datetime(1998, 1, 1, 2, 0)])
+
+    def testHourlyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 1, 0),
+                          datetime(1998, 1, 1, 2, 0)])
+
+    def testHourlyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 1, 0),
+                          datetime(1998, 1, 1, 2, 0)])
+
+    def testHourlyByYearDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0),
+                          datetime(1997, 12, 31, 1, 0),
+                          datetime(1997, 12, 31, 2, 0),
+                          datetime(1997, 12, 31, 3, 0)])
+
+    def testHourlyByYearDayNeg(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0),
+                          datetime(1997, 12, 31, 1, 0),
+                          datetime(1997, 12, 31, 2, 0),
+                          datetime(1997, 12, 31, 3, 0)])
+
+    def testHourlyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0),
+                          datetime(1998, 4, 10, 1, 0),
+                          datetime(1998, 4, 10, 2, 0),
+                          datetime(1998, 4, 10, 3, 0)])
+
+    def testHourlyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0),
+                          datetime(1998, 4, 10, 1, 0),
+                          datetime(1998, 4, 10, 2, 0),
+                          datetime(1998, 4, 10, 3, 0)])
+
+    def testHourlyByWeekNo(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 0, 0),
+                          datetime(1998, 5, 11, 1, 0),
+                          datetime(1998, 5, 11, 2, 0)])
+
+    def testHourlyByWeekNoAndWeekDay(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 0, 0),
+                          datetime(1997, 12, 29, 1, 0),
+                          datetime(1997, 12, 29, 2, 0)])
+
+    def testHourlyByWeekNoAndWeekDayLarge(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0),
+                          datetime(1997, 12, 28, 1, 0),
+                          datetime(1997, 12, 28, 2, 0)])
+
+    def testHourlyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0),
+                          datetime(1997, 12, 28, 1, 0),
+                          datetime(1997, 12, 28, 2, 0)])
+
+    def testHourlyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 0, 0),
+                          datetime(1998, 12, 28, 1, 0),
+                          datetime(1998, 12, 28, 2, 0)])
+
+    def testHourlyByEaster(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 0, 0),
+                          datetime(1998, 4, 12, 1, 0),
+                          datetime(1998, 4, 12, 2, 0)])
+
+    def testHourlyByEasterPos(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 0, 0),
+                          datetime(1998, 4, 13, 1, 0),
+                          datetime(1998, 4, 13, 2, 0)])
+
+    def testHourlyByEasterNeg(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 0, 0),
+                          datetime(1998, 4, 11, 1, 0),
+                          datetime(1998, 4, 11, 2, 0)])
+
+    def testHourlyByHour(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 9, 3, 6, 0),
+                          datetime(1997, 9, 3, 18, 0)])
+
+    def testHourlyByMinute(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1997, 9, 2, 10, 6)])
+
+    def testHourlyBySecond(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1997, 9, 2, 10, 0, 6)])
+
+    def testHourlyByHourAndMinute(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1997, 9, 3, 6, 6)])
+
+    def testHourlyByHourAndSecond(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1997, 9, 3, 6, 0, 6)])
+
+    def testHourlyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testHourlyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testHourlyBySetPos(self):
+        self.assertEqual(list(rrule(HOURLY,
+                              count=3,
+                              byminute=(15, 45),
+                              bysecond=(15, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 15, 45),
+                          datetime(1997, 9, 2, 9, 45, 15),
+                          datetime(1997, 9, 2, 10, 15, 45)])
+
+    def testMinutely(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 9, 1),
+                          datetime(1997, 9, 2, 9, 2)])
+
+    def testMinutelyInterval(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 9, 2),
+                          datetime(1997, 9, 2, 9, 4)])
+
+    def testMinutelyIntervalLarge(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              interval=1501,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 10, 1),
+                          datetime(1997, 9, 4, 11, 2)])
+
+    def testMinutelyByMonth(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 0, 1),
+                          datetime(1998, 1, 1, 0, 2)])
+
+    def testMinutelyByMonthDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 0, 0),
+                          datetime(1997, 9, 3, 0, 1),
+                          datetime(1997, 9, 3, 0, 2)])
+
+    def testMinutelyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 0, 0),
+                          datetime(1998, 1, 5, 0, 1),
+                          datetime(1998, 1, 5, 0, 2)])
+
+    def testMinutelyByWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 9, 1),
+                          datetime(1997, 9, 2, 9, 2)])
+
+    def testMinutelyByNWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 2, 9, 1),
+                          datetime(1997, 9, 2, 9, 2)])
+
+    def testMinutelyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 0, 1),
+                          datetime(1998, 1, 1, 0, 2)])
+
+    def testMinutelyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 0, 1),
+                          datetime(1998, 1, 1, 0, 2)])
+
+    def testMinutelyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 0, 1),
+                          datetime(1998, 1, 1, 0, 2)])
+
+    def testMinutelyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0),
+                          datetime(1998, 1, 1, 0, 1),
+                          datetime(1998, 1, 1, 0, 2)])
+
+    def testMinutelyByYearDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0),
+                          datetime(1997, 12, 31, 0, 1),
+                          datetime(1997, 12, 31, 0, 2),
+                          datetime(1997, 12, 31, 0, 3)])
+
+    def testMinutelyByYearDayNeg(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0),
+                          datetime(1997, 12, 31, 0, 1),
+                          datetime(1997, 12, 31, 0, 2),
+                          datetime(1997, 12, 31, 0, 3)])
+
+    def testMinutelyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0),
+                          datetime(1998, 4, 10, 0, 1),
+                          datetime(1998, 4, 10, 0, 2),
+                          datetime(1998, 4, 10, 0, 3)])
+
+    def testMinutelyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0),
+                          datetime(1998, 4, 10, 0, 1),
+                          datetime(1998, 4, 10, 0, 2),
+                          datetime(1998, 4, 10, 0, 3)])
+
+    def testMinutelyByWeekNo(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 0, 0),
+                          datetime(1998, 5, 11, 0, 1),
+                          datetime(1998, 5, 11, 0, 2)])
+
+    def testMinutelyByWeekNoAndWeekDay(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 0, 0),
+                          datetime(1997, 12, 29, 0, 1),
+                          datetime(1997, 12, 29, 0, 2)])
+
+    def testMinutelyByWeekNoAndWeekDayLarge(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0),
+                          datetime(1997, 12, 28, 0, 1),
+                          datetime(1997, 12, 28, 0, 2)])
+
+    def testMinutelyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0),
+                          datetime(1997, 12, 28, 0, 1),
+                          datetime(1997, 12, 28, 0, 2)])
+
+    def testMinutelyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 0, 0),
+                          datetime(1998, 12, 28, 0, 1),
+                          datetime(1998, 12, 28, 0, 2)])
+
+    def testMinutelyByEaster(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 0, 0),
+                          datetime(1998, 4, 12, 0, 1),
+                          datetime(1998, 4, 12, 0, 2)])
+
+    def testMinutelyByEasterPos(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 0, 0),
+                          datetime(1998, 4, 13, 0, 1),
+                          datetime(1998, 4, 13, 0, 2)])
+
+    def testMinutelyByEasterNeg(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 0, 0),
+                          datetime(1998, 4, 11, 0, 1),
+                          datetime(1998, 4, 11, 0, 2)])
+
+    def testMinutelyByHour(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0),
+                          datetime(1997, 9, 2, 18, 1),
+                          datetime(1997, 9, 2, 18, 2)])
+
+    def testMinutelyByMinute(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6),
+                          datetime(1997, 9, 2, 9, 18),
+                          datetime(1997, 9, 2, 10, 6)])
+
+    def testMinutelyBySecond(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 6),
+                          datetime(1997, 9, 2, 9, 0, 18),
+                          datetime(1997, 9, 2, 9, 1, 6)])
+
+    def testMinutelyByHourAndMinute(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6),
+                          datetime(1997, 9, 2, 18, 18),
+                          datetime(1997, 9, 3, 6, 6)])
+
+    def testMinutelyByHourAndSecond(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 6),
+                          datetime(1997, 9, 2, 18, 0, 18),
+                          datetime(1997, 9, 2, 18, 1, 6)])
+
+    def testMinutelyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 6),
+                          datetime(1997, 9, 2, 9, 6, 18),
+                          datetime(1997, 9, 2, 9, 18, 6)])
+
+    def testMinutelyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 6),
+                          datetime(1997, 9, 2, 18, 6, 18),
+                          datetime(1997, 9, 2, 18, 18, 6)])
+
+    def testMinutelyBySetPos(self):
+        self.assertEqual(list(rrule(MINUTELY,
+                              count=3,
+                              bysecond=(15, 30, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 15),
+                          datetime(1997, 9, 2, 9, 0, 45),
+                          datetime(1997, 9, 2, 9, 1, 15)])
+
+    def testSecondly(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 0),
+                          datetime(1997, 9, 2, 9, 0, 1),
+                          datetime(1997, 9, 2, 9, 0, 2)])
+
+    def testSecondlyInterval(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 0),
+                          datetime(1997, 9, 2, 9, 0, 2),
+                          datetime(1997, 9, 2, 9, 0, 4)])
+
+    def testSecondlyIntervalLarge(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              interval=90061,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 0),
+                          datetime(1997, 9, 3, 10, 1, 1),
+                          datetime(1997, 9, 4, 11, 2, 2)])
+
+    def testSecondlyByMonth(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0, 0),
+                          datetime(1998, 1, 1, 0, 0, 1),
+                          datetime(1998, 1, 1, 0, 0, 2)])
+
+    def testSecondlyByMonthDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 3, 0, 0, 0),
+                          datetime(1997, 9, 3, 0, 0, 1),
+                          datetime(1997, 9, 3, 0, 0, 2)])
+
+    def testSecondlyByMonthAndMonthDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 5, 0, 0, 0),
+                          datetime(1998, 1, 5, 0, 0, 1),
+                          datetime(1998, 1, 5, 0, 0, 2)])
+
+    def testSecondlyByWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 0),
+                          datetime(1997, 9, 2, 9, 0, 1),
+                          datetime(1997, 9, 2, 9, 0, 2)])
+
+    def testSecondlyByNWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0, 0),
+                          datetime(1997, 9, 2, 9, 0, 1),
+                          datetime(1997, 9, 2, 9, 0, 2)])
+
+    def testSecondlyByMonthAndWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0, 0),
+                          datetime(1998, 1, 1, 0, 0, 1),
+                          datetime(1998, 1, 1, 0, 0, 2)])
+
+    def testSecondlyByMonthAndNWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0, 0),
+                          datetime(1998, 1, 1, 0, 0, 1),
+                          datetime(1998, 1, 1, 0, 0, 2)])
+
+    def testSecondlyByMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0, 0),
+                          datetime(1998, 1, 1, 0, 0, 1),
+                          datetime(1998, 1, 1, 0, 0, 2)])
+
+    def testSecondlyByMonthAndMonthDayAndWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 1, 1, 0, 0, 0),
+                          datetime(1998, 1, 1, 0, 0, 1),
+                          datetime(1998, 1, 1, 0, 0, 2)])
+
+    def testSecondlyByYearDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0, 0),
+                          datetime(1997, 12, 31, 0, 0, 1),
+                          datetime(1997, 12, 31, 0, 0, 2),
+                          datetime(1997, 12, 31, 0, 0, 3)])
+
+    def testSecondlyByYearDayNeg(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 31, 0, 0, 0),