diff --git a/resources/lib/libraries/__init__.py b/resources/lib/libraries/__init__.py
index 20b15530..a81e44db 100644
--- a/resources/lib/libraries/__init__.py
+++ b/resources/lib/libraries/__init__.py
@@ -1 +1,2 @@
 import requests
+import dateutil
diff --git a/resources/lib/libraries/dateutil/LICENSE b/resources/lib/libraries/dateutil/LICENSE
new file mode 100644
index 00000000..1e65815c
--- /dev/null
+++ b/resources/lib/libraries/dateutil/LICENSE
@@ -0,0 +1,54 @@
+Copyright 2017- Paul Ganssle <paul@ganssle.io>
+Copyright 2017- dateutil contributors (see AUTHORS file)
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+The above license applies to all contributions after 2017-12-01, as well as
+all contributions that have been re-licensed (see AUTHORS file for the list of
+contributors who have re-licensed their code).
+--------------------------------------------------------------------------------
+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-2016 - Yaron de Leeuw <me@jarondl.net>
+Copyright (c) 2015-     - Paul Ganssle <paul@ganssle.io>
+Copyright (c) 2015-     - dateutil contributors (see AUTHORS file)
+
+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.
+
+The above BSD License Applies to all code, even that also covered by Apache 2.0.
\ No newline at end of file
diff --git a/resources/lib/libraries/dateutil/NEWS b/resources/lib/libraries/dateutil/NEWS
new file mode 100644
index 00000000..a30cdaab
--- /dev/null
+++ b/resources/lib/libraries/dateutil/NEWS
@@ -0,0 +1,701 @@
+Version 2.7.3 (2018-05-09)
+==========================
+
+Data updates
+------------
+
+- Update tzdata to 2018e. (gh pr #710)
+
+
+Bugfixes
+--------
+
+- Fixed an issue where decimal.Decimal would cast `NaN` or infinite value in a
+  parser.parse, which will raise decimal.Decimal-specific errors. Reported and
+  fixed by @amureki (gh issue #662, gh pr #679).
+- Fixed a ValueError being thrown if tzinfos call explicity returns ``None``.
+  Reported by @pganssle (gh issue #661) Fixed by @parsethis (gh pr #681)
+- Fixed incorrect parsing of certain dates earlier than 100 AD when repesented
+  in the form "%B.%Y.%d", e.g. "December.0031.30". (gh issue #687, pr #700)
+- Fixed a bug where automatically generated DTSTART was naive even if a
+  specified UNTIL had a time zone. Automatically generated DTSTART will now
+  take on the timezone of an UNTIL date, if provided. Reported by @href (gh
+  issue #652). Fixed by @absreim (gh pr #693).
+
+
+Documentation changes
+---------------------
+
+- Corrected link syntax and updated URL to https for ISO year week number
+  notation in relativedelta examples. (gh issue #670, pr #711)
+- Add doctest examples to tzfile documentation. Done by @weatherpattern and
+  @pganssle (gh pr #671)
+- Updated the documentation for relativedelta. Removed references to tuple
+  arguments for weekday, explained effect of weekday(_, 1) and better explained
+  the order of operations that relativedelta applies. Fixed by @kvn219
+  @huangy22 and @ElliotJH (gh pr #673)
+- Added changelog to documentation. (gh issue #692, gh pr #707)
+- Changed order of keywords in rrule docstring. Reported and fixed by
+  @rmahajan14 (gh issue #686, gh pr #695).
+- Added documentation for ``dateutil.tz.gettz``. Reported by @pganssle (gh
+  issue #647). Fixed by @weatherpattern (gh pr #704)
+- Cleaned up malformed RST in the ``tz`` documentation. (gh issue #702, gh pr
+  #706)
+- Changed the default theme to sphinx_rtd_theme, and changed the sphinx
+  configuration to go along with that. (gh pr #707)
+- Reorganized ``dateutil.tz`` documentation and fixed issue with the
+  ``dateutil.tz`` docstring. (gh pr #714)
+
+
+Misc
+----
+
+- GH #674, GH #688, GH #699
+
+
+Version 2.7.2 (2018-03-26)
+==========================
+
+Bugfixes
+--------
+
+- Fixed an issue with the setup script running in non-UTF-8 environment.
+  Reported and fixed by @gergondet (gh pr #651)
+
+
+Misc
+----
+
+- GH #655
+
+
+Version 2.7.1 (2018-03-24)
+===========================
+
+Data updates
+------------
+
+- Updated tzdata version to 2018d.
+
+
+Bugfixes
+--------
+
+- Fixed issue where parser.parse would occasionally raise
+  decimal.Decimal-specific error types rather than ValueError. Reported by
+  @amureki (gh issue #632). Fixed by @pganssle (gh pr #636).
+- Improve error message when rrule's dtstart and until are not both naive or
+  both aware. Reported and fixed by @ryanpetrello (gh issue #633, gh pr #634)
+
+
+Misc
+----
+
+- GH #644, GH #648
+
+
+Version 2.7.0
+=============
+- Dropped support for Python 2.6 (gh pr #362 by @jdufresne)
+- Dropped support for Python 3.2 (gh pr #626)
+- Updated zoneinfo file to 2018c (gh pr #616)
+- Changed licensing scheme so all new contributions are dual licensed under
+  Apache 2.0 and BSD. (gh pr #542, issue #496)
+- Added __all__ variable to the root package. Reported by @tebriel
+  (gh issue #406), fixed by @mariocj89 (gh pr #494)
+- Added python_requires to setup.py so that pip will distribute the right
+  version of dateutil. Fixed by @jakec-github (gh issue #537, pr #552)
+- Added the utils submodule, for miscellaneous utilities.
+- Added within_delta function to utils - added by @justanr (gh issue #432,
+  gh pr #437)
+- Added today function to utils (gh pr #474)
+- Added default_tzinfo function to utils (gh pr #475), solving an issue
+  reported by @nealmcb (gh issue #94)
+- Added dedicated ISO 8601 parsing function isoparse (gh issue #424).
+  Initial implementation by @pganssle in gh pr #489 and #622, with a
+  pre-release fix by @kirit93 (gh issue #546, gh pr #573). 
+- Moved parser module into parser/_parser.py and officially deprecated the use
+  of several private functions and classes from that module. (gh pr #501, #515)
+- Tweaked parser error message to include rejected string format, added by
+  @pbiering (gh pr #300)
+- Add support for parsing bytesarray, reported by @uckelman (gh issue #417) and
+  fixed by @uckelman and @pganssle (gh pr #514)
+- Started raising a warning when the parser finds a timezone string that it
+  cannot construct a tzinfo instance for (rather than succeeding with no
+  indication of an error). Reported and fixed by @jbrockmendel (gh pr #540)
+- Dropped the use of assert in the parser. Fixed by @jbrockmendel (gh pr #502)
+- Fixed to assertion logic in parser to support dates like '2015-15-May',
+  reported and fixed by @jbrockmendel (gh pr #409)
+- Fixed IndexError in parser on dates with trailing colons, reported and fixed
+  by @jbrockmendel (gh pr #420)
+- Fixed bug where hours were not validated, leading to improper parse. Reported
+  by @heappro (gh pr #353), fixed by @jbrockmendel (gh pr #482)
+- Fixed problem parsing strings in %b-%Y-%d format. Reported and fixed by
+  @jbrockmendel (gh pr #481)
+- Fixed problem parsing strings in the %d%B%y format. Reported by @asishm
+  (gh issue #360), fixed by @jbrockmendel (gh pr #483)
+- Fixed problem parsing certain unambiguous strings when year <99 (gh pr #510).
+  Reported by @alexwlchan (gh issue #293).
+- Fixed issue with parsing an unambiguous string representation of an ambiguous
+  datetime such that if possible the correct value for fold is set. Fixes
+  issue reported by @JordonPhillips and @pganssle (gh issue #318, #320,
+  gh pr #517)
+- Fixed issue with improper rounding of fractional components. Reported by
+  @dddmello (gh issue #427), fixed by @m-dz (gh pr #570)
+- Performance improvement to parser from removing certain min() calls. Reported
+  and fixed by @jbrockmendel (gh pr #589)
+- Significantly refactored parser code by @jbrockmendel (gh prs #419, #436,
+  #490, #498, #539) and @pganssle (gh prs #435, #468)
+- Implementated of __hash__ for relativedelta and weekday, reported and fixed
+  by @mrigor (gh pr #389) 
+- Implemented __abs__ for relativedelta. Reported by @binnisb and @pferreir
+  (gh issue #350, pr #472)
+- Fixed relativedelta.weeks property getter and setter to work for both
+  negative and positive values. Reported and fixed by @souliane (gh issue #459,
+  pr #460)
+- Fixed issue where passing whole number floats to the months or years
+  arguments of the relativedelta constructor would lead to errors during
+  addition. Reported by @arouanet (gh pr #411), fixed by @lkollar (gh pr #553)
+- Added a pre-built tz.UTC object representing UTC (gh pr #497)
+- Added a cache to tz.gettz so that by default it will return the same object
+  for identical inputs. This will change the semantics of certain operations
+  between datetimes constructed with tzinfo=tz.gettz(...). (gh pr #628)
+- Changed the behavior of tz.tzutc to return a singleton (gh pr #497, #504)
+- Changed the behavior of tz.tzoffset to return the same object when passed the
+  same inputs, with a corresponding performance improvement (gh pr #504)
+- Changed the behavior of tz.tzstr to return the same object when passed the
+  same inputs. (gh pr #628)
+- Added .instance alternate constructors for tz.tzoffset and tz.tzstr, to
+  allow the construction of a new instance if desired. (gh pr #628)
+- Added the tz.gettz.nocache function to allow explicit retrieval of a new
+  instance of the relevant tzinfo. (gh pr #628)
+- Expand definition of tz.tzlocal equality so that the local zone is allow
+  equality with tzoffset and tzutc. (gh pr #598)
+- Deprecated the idiosyncratic tzstr format mentioned in several examples but
+  evidently designed exclusively for dateutil, and very likely not used by
+  any current users. (gh issue #595, gh pr #606)
+- Added the tz.resolve_imaginary function, which generates a real date from
+  an imaginary one, if necessary. Implemented by @Cheukting (gh issue #339,
+  gh pr #607)
+- Fixed issue where the tz.tzstr constructor would erroneously succeed if
+  passed an invalid value for tzstr. Fixed by @pablogsal (gh issue #259,
+  gh pr #581)
+- Fixed issue with tz.gettz for TZ variables that start with a colon. Reported
+  and fixed by @lapointexavier (gh pr #601)
+- Added a lock to tz.tzical's cache. Reported and fixed by @Unrud (gh pr #430) 
+- Fixed an issue with fold support on certain Python 3 implementations that
+  used the pre-3.6 pure Python implementation of datetime.replace, most
+  notably pypy3 (gh pr #446).
+- Added support for VALUE=DATE-TIME for DTSTART in rrulestr. Reported by @potuz
+  (gh issue #401) and fixed by @Unrud (gh pr #429)
+- Started enforcing that within VTIMEZONE, the VALUE parameter can only be
+  omitted or DATE-TIME, per RFC 5545. Reported by @Unrud (gh pr #439)
+- Added support for TZID parameter for DTSTART in rrulestr. Reported and
+  fixed by @ryanpetrello (gh issue #614, gh pr #624)
+- Added 'RRULE:' prefix to rrule strings generated by rrule.__str__, in
+  compliance with the RFC. Reported by @AndrewPashkin (gh issue #86), fixed by
+  @jarondl and @mlorant (gh pr #450)
+- Switched to setuptools_scm for version management, automatically calculating
+  a version number from the git metadata. Reported by @jreback (gh issue #511),
+  implemented by @Sulley38 (gh pr #564)
+- Switched setup.py to use find_packages, and started testing against pip
+  installed versions of dateutil in CI. Fixed issue with parser import
+  discovered by @jreback in pandas-dev/pandas#18141. (gh issue #507, pr #509)
+- Switched test suite to using pytest (gh pr #495)
+- Switched CI over to use tox. Fixed by @gaborbernat (gh pr #549)
+- Added a test-only dependency on freezegun. (gh pr #474)
+- Reduced number of CI builds on Appveyor. Fixed by @kirit93 (gh issue #529,
+  gh pr #579)
+- Made xfails strict by default, so that an xpass is a failure. (gh pr #567)
+- Added a documentation generation stage to tox and CI. (gh pr #568)
+- Added an explicit warning when running python setup.py explaining how to run
+  the test suites with pytest. Fixed by @lkollar. (gh issue #544, gh pr #548)
+- Added requirements-dev.txt for test dependency management (gh pr #499, #516)
+- Fixed code coverage metrics to account for Windows builds (gh pr #526)
+- Fixed code coverage metrics to NOT count xfails. Fixed by @gaborbernat
+  (gh issue #519, gh pr #563)
+- Style improvement to zoneinfo.tzfile that was confusing to static type
+  checkers. Reported and fixed by @quodlibetor (gh pr #485)
+- Several unused imports were removed by @jdufresne. (gh pr #486)
+- Switched ``isinstance(*, collections.Callable)`` to callable, which is available
+  on all supported Python versions. Implemented by @jdufresne (gh pr #612)
+- Added CONTRIBUTING.md (gh pr #533)
+- Added AUTHORS.md (gh pr #542)
+- Corrected setup.py metadata to reflect author vs. maintainer, (gh issue #477,
+  gh pr #538)
+- Corrected README to reflect that tests are now run in pytest. Reported and
+  fixed by @m-dz (gh issue #556, gh pr #557)
+- Updated all references to RFC 2445 (iCalendar) to point to RFC 5545. Fixed
+  by @mariocj89 (gh issue #543, gh pr #555)
+- Corrected parse documentation to reflect proper integer offset units,
+  reported and fixed by @abrugh (gh pr #458)
+- Fixed dangling parenthesis in tzoffset documentation (gh pr #461)
+- Started including the license file in wheels. Reported and fixed by
+  @jdufresne (gh pr #476)
+- Indendation fixes to parser docstring by @jbrockmendel (gh pr #492)
+- Moved many examples from the "examples" documentation into their appropriate
+  module documentation pages. Fixed by @Tomasz-Kluczkowski and @jakec-github
+  (gh pr #558, #561)
+- Fixed documentation so that the parser.isoparse documentation displays.
+  Fixed by @alexchamberlain (gh issue #545, gh pr #560)
+- Refactored build and release sections and added setup instructions to
+  CONTRIBUTING. Reported and fixed by @kynan (gh pr #562)
+- Cleaned up various dead links in the documentation. (gh pr #602, #608, #618)
+
+Version 2.6.1
+=============
+- Updated zoneinfo file to 2017b. (gh pr #395)
+- Added Python 3.6 to CI testing (gh pr #365)
+- Removed duplicate test name that was preventing a test from being run.
+  Reported and fixed by @jdufresne (gh pr #371)
+- Fixed testing of folds and gaps, particularly on Windows (gh pr #392)
+- Fixed deprecated escape characters in regular expressions. Reported by
+  @nascheme and @thierryba (gh issue #361), fixed by @thierryba (gh pr #358)
+- Many PEP8 style violations and other code smells were fixed by @jdufresne
+  (gh prs #358, #363, #364, #366, #367, #368, #372, #374, #379, #380, #398)
+- Improved performance of tzutc and tzoffset objects. (gh pr #391)
+- Fixed issue with several time zone classes around DST transitions in any
+  zones with +0 standard offset (e.g. Europe/London) (gh issue #321, pr #390)
+- Fixed issue with fuzzy parsing where tokens similar to AM/PM that are in the
+  end skipped were dropped in the fuzzy_with_tokens list. Reported and fixed
+  by @jbrockmendel (gh pr #332).
+- Fixed issue with parsing dates of the form X m YY. Reported by @jbrockmendel.
+  (gh issue #333, pr #393)
+- Added support for parser weekdays with less than 3 characters. Reported by
+  @arcadefoam (gh issue #343), fixed by @jonemo (gh pr #382)
+- Fixed issue with the addition and subtraction of certain relativedeltas.
+  Reported and fixed by @kootenpv (gh issue #346, pr #347)
+- Fixed issue where the COUNT parameter of rrules was ignored if 0. Fixed by
+  @mshenfield (gh pr #330), reported by @vaultah (gh issue #329).
+- Updated documentation to include the new tz methods. (gh pr #324)
+- Update documentation to reflect that the parser can raise TypeError, reported
+  and fixed by @tomchuk (gh issue #336, pr #337)
+- Fixed an incorrect year in a parser doctest. Fixed by @xlotlu (gh pr #357)
+- Moved version information into _version.py and set up the versions more
+  granularly.
+
+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. 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.
+
+
+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.
diff --git a/resources/lib/libraries/dateutil/README.rst b/resources/lib/libraries/dateutil/README.rst
new file mode 100644
index 00000000..7a37552e
--- /dev/null
+++ b/resources/lib/libraries/dateutil/README.rst
@@ -0,0 +1,158 @@
+dateutil - powerful extensions to datetime
+==========================================
+
+|pypi| |support| |licence| 
+
+|gitter| |readthedocs|
+
+|travis| |appveyor| |coverage|
+
+.. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square
+    :target: https://pypi.org/project/python-dateutil/
+    :alt: pypi version
+
+.. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square
+    :target: https://pypi.org/project/python-dateutil/
+    :alt: supported Python version
+
+.. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build
+    :target: https://travis-ci.org/dateutil/dateutil
+    :alt: travis build status
+
+.. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor
+    :target: https://ci.appveyor.com/project/dateutil/dateutil
+    :alt: appveyor build status
+
+.. |coverage| image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master
+    :target: https://codecov.io/github/dateutil/dateutil?branch=master
+    :alt: Code coverage
+
+.. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg
+   :alt: Join the chat at https://gitter.im/dateutil/dateutil
+   :target: https://gitter.im/dateutil/dateutil
+
+.. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square
+    :target: https://pypi.org/project/python-dateutil/
+    :alt: licence
+
+.. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs
+   :alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/
+   :target: https://dateutil.readthedocs.io/en/latest/
+
+The `dateutil` module provides powerful extensions to
+the standard `datetime` module, available in Python.
+
+
+Download
+========
+dateutil is available on PyPI
+https://pypi.org/project/python-dateutil/
+
+The documentation is hosted at:
+https://dateutil.readthedocs.io/en/stable/
+
+Code
+====
+The code and issue tracker are hosted on Github:
+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 :)
+
+Contributing
+============
+
+We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository.
+
+
+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-2016
+* Paul Ganssle <paul@ganssle.io> 2015-
+
+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`_ (|pgp_mirror|_)
+===========  ============================
+
+
+Contact
+=======
+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/>`_.
+
+License
+=======
+
+All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License.
+
+
+.. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB:
+   https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
+
+.. |pgp_mirror| replace:: mirror
+.. _pgp_mirror: https://sks-keyservers.net/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
diff --git a/resources/lib/libraries/dateutil/__init__.py b/resources/lib/libraries/dateutil/__init__.py
new file mode 100644
index 00000000..0defb82e
--- /dev/null
+++ b/resources/lib/libraries/dateutil/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+try:
+    from ._version import version as __version__
+except ImportError:
+    __version__ = 'unknown'
+
+__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
+           'utils', 'zoneinfo']
diff --git a/resources/lib/libraries/dateutil/_common.py b/resources/lib/libraries/dateutil/_common.py
new file mode 100644
index 00000000..4eb2659b
--- /dev/null
+++ b/resources/lib/libraries/dateutil/_common.py
@@ -0,0 +1,43 @@
+"""
+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
+
+    def __hash__(self):
+        return hash((
+          self.weekday,
+          self.n,
+        ))
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    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)
+
+# vim:ts=4:sw=4:et
diff --git a/resources/lib/libraries/dateutil/easter.py b/resources/lib/libraries/dateutil/easter.py
new file mode 100644
index 00000000..53b7c789
--- /dev/null
+++ b/resources/lib/libraries/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:
+
+    `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
+
+    and
+
+    `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
+
+    """
+
+    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))
diff --git a/resources/lib/libraries/dateutil/parser/__init__.py b/resources/lib/libraries/dateutil/parser/__init__.py
new file mode 100644
index 00000000..216762c0
--- /dev/null
+++ b/resources/lib/libraries/dateutil/parser/__init__.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from ._parser import parse, parser, parserinfo
+from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
+from ._parser import UnknownTimezoneWarning
+
+from ._parser import __doc__
+
+from .isoparser import isoparser, isoparse
+
+__all__ = ['parse', 'parser', 'parserinfo',
+           'isoparse', 'isoparser',
+           'UnknownTimezoneWarning']
+
+
+###
+# Deprecate portions of the private interface so that downstream code that
+# is improperly relying on it is given *some* notice.
+
+
+def __deprecated_private_func(f):
+    from functools import wraps
+    import warnings
+
+    msg = ('{name} is a private function and may break without warning, '
+           'it will be moved and or renamed in future versions.')
+    msg = msg.format(name=f.__name__)
+
+    @wraps(f)
+    def deprecated_func(*args, **kwargs):
+        warnings.warn(msg, DeprecationWarning)
+        return f(*args, **kwargs)
+
+    return deprecated_func
+
+def __deprecate_private_class(c):
+    import warnings
+
+    msg = ('{name} is a private class and may break without warning, '
+           'it will be moved and or renamed in future versions.')
+    msg = msg.format(name=c.__name__)
+
+    class private_class(c):
+        __doc__ = c.__doc__
+
+        def __init__(self, *args, **kwargs):
+            warnings.warn(msg, DeprecationWarning)
+            super(private_class, self).__init__(*args, **kwargs)
+
+    private_class.__name__ = c.__name__
+
+    return private_class
+
+
+from ._parser import _timelex, _resultbase
+from ._parser import _tzparser, _parsetz
+
+_timelex = __deprecate_private_class(_timelex)
+_tzparser = __deprecate_private_class(_tzparser)
+_resultbase = __deprecate_private_class(_resultbase)
+_parsetz = __deprecated_private_func(_parsetz)
diff --git a/resources/lib/libraries/dateutil/parser/_parser.py b/resources/lib/libraries/dateutil/parser/_parser.py
new file mode 100644
index 00000000..e8a522c9
--- /dev/null
+++ b/resources/lib/libraries/dateutil/parser/_parser.py
@@ -0,0 +1,1578 @@
+# -*- coding: utf-8 -*-
+"""
+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) <https://pds-rings.seti.org:443/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 re
+import string
+import time
+import warnings
+
+from calendar import monthrange
+from io import StringIO
+
+from .. import six
+from ..six import binary_type, integer_types, text_type
+
+from decimal import Decimal
+
+from warnings import warn
+
+from .. import relativedelta
+from .. import tz
+
+__all__ = ["parse", "parserinfo"]
+
+
+# TODO: pandas.core.tools.datetimes imports this explicitly.  Might be worth
+# making public and/or figuring out if there is something we can
+# take off their plate.
+class _timelex(object):
+    # Fractional seconds are sometimes split by a comma
+    _split_decimal = re.compile("([.,])")
+
+    def __init__(self, instream):
+        if six.PY2:
+            # In Python 2, we can't duck type properly because unicode has
+            # a 'decode' function, and we'd be double-decoding
+            if isinstance(instream, (binary_type, bytearray)):
+                instream = instream.decode()
+        else:
+            if getattr(instream, 'decode', None) is not None:
+                instream = instream.decode()
+
+        if isinstance(instream, text_type):
+            instream = StringIO(instream)
+        elif 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"),     # TODO: "Tues"
+                ("Wed", "Wednesday"),
+                ("Thu", "Thursday"),    # TODO: "Thurs"
+                ("Fri", "Friday"),
+                ("Sat", "Saturday"),
+                ("Sun", "Sunday")]
+    MONTHS = [("Jan", "January"),
+              ("Feb", "February"),      # TODO: "Febr"
+              ("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 = {}
+    # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
+    #              "Anno Domini", "Year of Our Lord"]
+
+    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):
+        try:
+            return self._weekdays[name.lower()]
+        except KeyError:
+            pass
+        return None
+
+    def month(self, name):
+        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):
+        """
+        Converts two-digit years to year within [-50, 49]
+        range of self._year (current local time)
+        """
+
+        # Function contract is that the year is always positive
+        assert year >= 0
+
+        if year < 100 and not century_specified:
+            # assume current century to start
+            year += self._century
+
+            if year >= self._year + 50:  # if too far in future
+                year -= 100
+            elif year < self._year - 50:  # if too far in past
+                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, *args, **kwargs):
+        super(self.__class__, self).__init__(*args, **kwargs)
+        self.century_specified = False
+        self.dstridx = None
+        self.mstridx = None
+        self.ystridx = None
+
+    @property
+    def has_year(self):
+        return self.ystridx is not None
+
+    @property
+    def has_month(self):
+        return self.mstridx is not None
+
+    @property
+    def has_day(self):
+        return self.dstridx is not None
+
+    def could_be_day(self, value):
+        if self.has_day:
+            return False
+        elif not self.has_month:
+            return 1 <= value <= 31
+        elif not self.has_year:
+            # Be permissive, assume leapyear
+            month = self[self.mstridx]
+            return 1 <= value <= monthrange(2000, month)[1]
+        else:
+            month = self[self.mstridx]
+            year = self[self.ystridx]
+            return 1 <= value <= monthrange(year, month)[1]
+
+    def append(self, val, label=None):
+        if hasattr(val, '__len__'):
+            if val.isdigit() and len(val) > 2:
+                self.century_specified = True
+                if label not in [None, 'Y']:  # pragma: no cover
+                    raise ValueError(label)
+                label = 'Y'
+        elif val > 100:
+            self.century_specified = True
+            if label not in [None, 'Y']:  # pragma: no cover
+                raise ValueError(label)
+            label = 'Y'
+
+        super(self.__class__, self).append(int(val))
+
+        if label == 'M':
+            if self.has_month:
+                raise ValueError('Month is already set')
+            self.mstridx = len(self) - 1
+        elif label == 'D':
+            if self.has_day:
+                raise ValueError('Day is already set')
+            self.dstridx = len(self) - 1
+        elif label == 'Y':
+            if self.has_year:
+                raise ValueError('Year is already set')
+            self.ystridx = len(self) - 1
+
+    def _resolve_from_stridxs(self, strids):
+        """
+        Try to resolve the identities of year/month/day elements using
+        ystridx, mstridx, and dstridx, if enough of these are specified.
+        """
+        if len(self) == 3 and len(strids) == 2:
+            # we can back out the remaining stridx value
+            missing = [x for x in range(3) if x not in strids.values()]
+            key = [x for x in ['y', 'm', 'd'] if x not in strids]
+            assert len(missing) == len(key) == 1
+            key = key[0]
+            val = missing[0]
+            strids[key] = val
+
+        assert len(self) == len(strids)  # otherwise this should not be called
+        out = {key: self[strids[key]] for key in strids}
+        return (out.get('y'), out.get('m'), out.get('d'))
+
+    def resolve_ymd(self, yearfirst, dayfirst):
+        len_ymd = len(self)
+        year, month, day = (None, None, None)
+
+        strids = (('y', self.ystridx),
+                  ('m', self.mstridx),
+                  ('d', self.dstridx))
+
+        strids = {key: val for key, val in strids if val is not None}
+        if (len(self) == len(strids) > 0 or
+                (len(self) == 3 and len(strids) == 2)):
+            return self._resolve_from_stridxs(strids)
+
+        mstridx = self.mstridx
+
+        if len_ymd > 3:
+            raise ValueError("More than three YMD values")
+        elif len_ymd == 1 or (mstridx is not None and len_ymd == 2):
+            # One member, or two members with a month string
+            if mstridx is not None:
+                month = self[mstridx]
+                # since mstridx is 0 or 1, self[mstridx-1] always
+                # looks up the other element
+                other = self[mstridx - 1]
+            else:
+                other = self[0]
+
+            if len_ymd > 1 or mstridx is None:
+                if other > 31:
+                    year = other
+                else:
+                    day = other
+
+        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:
+                if self[1] > 31:
+                    # Apr-2003-25
+                    month, year, day = self
+                else:
+                    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.ystridx == 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 seconds or a :class:`tzinfo` object.
+
+            .. doctest::
+               :options: +NORMALIZE_WHITESPACE
+
+                >>> from dateutil.parser import parse
+                >>> from dateutil.tz import gettz
+                >>> tzinfos = {"BRST": -7200, "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', -7200))
+                >>> 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 TypeError:
+            Raised for non-string or character stream input.
+
+        :raises OverflowError:
+            Raised if the parsed date exceeds the largest valid C integer on
+            your system.
+        """
+
+        if default is None:
+            default = datetime.datetime.now().replace(hour=0, minute=0,
+                                                      second=0, microsecond=0)
+
+        res, skipped_tokens = self._parse(timestr, **kwargs)
+
+        if res is None:
+            raise ValueError("Unknown string format:", timestr)
+
+        if len(res) == 0:
+            raise ValueError("String does not contain a date:", timestr)
+
+        ret = self._build_naive(res, default)
+
+        if not ignoretz:
+            ret = self._build_tzaware(ret, res, tzinfos)
+
+        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","any_unused_tokens"]
+
+    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
+
+        skipped_idxs = []
+
+        # year/month/day list
+        ymd = _ymd()
+
+        len_l = len(l)
+        i = 0
+        try:
+            while i < len_l:
+
+                # Check if it's a number
+                value_repr = l[i]
+                try:
+                    value = float(value_repr)
+                except ValueError:
+                    value = None
+
+                if value is not None:
+                    # Numeric token
+                    i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy)
+
+                # Check weekday
+                elif info.weekday(l[i]) is not None:
+                    value = info.weekday(l[i])
+                    res.weekday = value
+
+                # Check month name
+                elif info.month(l[i]) is not None:
+                    value = info.month(l[i])
+                    ymd.append(value, 'M')
+
+                    if i + 1 < len_l:
+                        if l[i + 1] in ('-', '/'):
+                            # Jan-01[-99]
+                            sep = l[i + 1]
+                            ymd.append(l[i + 2])
+
+                            if i + 3 < len_l and l[i + 3] == sep:
+                                # Jan-01-99
+                                ymd.append(l[i + 4])
+                                i += 2
+
+                            i += 2
+
+                        elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and
+                              info.pertain(l[i + 2])):
+                            # Jan of 01
+                            # In this case, 01 is clearly year
+                            if l[i + 4].isdigit():
+                                # Convert it here to become unambiguous
+                                value = int(l[i + 4])
+                                year = str(info.convertyear(value))
+                                ymd.append(year, 'Y')
+                            else:
+                                # Wrong guess
+                                pass
+                                # TODO: not hit in tests
+                            i += 4
+
+                # Check am/pm
+                elif info.ampm(l[i]) is not None:
+                    value = info.ampm(l[i])
+                    val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy)
+
+                    if val_is_ampm:
+                        res.hour = self._adjust_ampm(res.hour, value)
+                        res.ampm = value
+
+                    elif fuzzy:
+                        skipped_idxs.append(i)
+
+                # Check for a timezone name
+                elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]):
+                    res.tzname = l[i]
+                    res.tzoffset = info.tzoffset(res.tzname)
+
+                    # 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 + 1 < len_l and l[i + 1] in ('+', '-'):
+                        l[i + 1] = ('+', '-')[l[i + 1] == '+']
+                        res.tzoffset = None
+                        if info.utczone(res.tzname):
+                            # With something like GMT+3, the timezone
+                            # is *not* GMT.
+                            res.tzname = None
+
+                # Check for a numbered timezone
+                elif res.hour is not None and l[i] in ('+', '-'):
+                    signal = (-1, 1)[l[i] == '+']
+                    len_li = len(l[i + 1])
+
+                    # TODO: check that l[i + 1] is integer?
+                    if len_li == 4:
+                        # -0300
+                        hour_offset = int(l[i + 1][:2])
+                        min_offset = int(l[i + 1][2:])
+                    elif i + 2 < len_l and l[i + 2] == ':':
+                        # -03:00
+                        hour_offset = int(l[i + 1])
+                        min_offset = int(l[i + 3])  # TODO: Check that l[i+3] is minute-like?
+                        i += 2
+                    elif len_li <= 2:
+                        # -[0]3
+                        hour_offset = int(l[i + 1][:2])
+                        min_offset = 0
+                    else:
+                        raise ValueError(timestr)
+
+                    res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60)
+
+                    # Look for a timezone name between parenthesis
+                    if (i + 5 < len_l and
+                            info.jump(l[i + 2]) and l[i + 3] == '(' and
+                            l[i + 5] == ')' and
+                            3 <= len(l[i + 4]) and
+                            self._could_be_tzname(res.hour, res.tzname,
+                                                  None, l[i + 4])):
+                        # -0300 (BRST)
+                        res.tzname = l[i + 4]
+                        i += 4
+
+                    i += 1
+
+                # Check jumps
+                elif not (info.jump(l[i]) or fuzzy):
+                    raise ValueError(timestr)
+
+                else:
+                    skipped_idxs.append(i)
+                i += 1
+
+            # Process year/month/day
+            year, month, day = ymd.resolve_ymd(yearfirst, dayfirst)
+
+            res.century_specified = ymd.century_specified
+            res.year = year
+            res.month = month
+            res.day = day
+
+        except (IndexError, ValueError):
+            return None, None
+
+        if not info.validate(res):
+            return None, None
+
+        if fuzzy_with_tokens:
+            skipped_tokens = self._recombine_skipped(l, skipped_idxs)
+            return res, tuple(skipped_tokens)
+        else:
+            return res, None
+
+    def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy):
+        # Token is a number
+        value_repr = tokens[idx]
+        try:
+            value = self._to_decimal(value_repr)
+        except Exception as e:
+            six.raise_from(ValueError('Unknown numeric token'), e)
+
+        len_li = len(value_repr)
+
+        len_l = len(tokens)
+
+        if (len(ymd) == 3 and len_li in (2, 4) and
+            res.hour is None and
+            (idx + 1 >= len_l or
+             (tokens[idx + 1] != ':' and
+              info.hms(tokens[idx + 1]) is None))):
+            # 19990101T23[59]
+            s = tokens[idx]
+            res.hour = int(s[:2])
+
+            if len_li == 4:
+                res.minute = int(s[2:])
+
+        elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6):
+            # YYMMDD or HHMMSS[.ss]
+            s = tokens[idx]
+
+            if not ymd and '.' not in tokens[idx]:
+                ymd.append(s[:2])
+                ymd.append(s[2:4])
+                ymd.append(s[4:])
+            else:
+                # 19990101T235959[.59]
+
+                # TODO: Check if res attributes already set.
+                res.hour = int(s[:2])
+                res.minute = int(s[2:4])
+                res.second, res.microsecond = self._parsems(s[4:])
+
+        elif len_li in (8, 12, 14):
+            # YYYYMMDD
+            s = tokens[idx]
+            ymd.append(s[:4], 'Y')
+            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 self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None:
+            # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+            hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True)
+            (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx)
+            if hms is not None:
+                # TODO: checking that hour/minute/second are not
+                # already set?
+                self._assign_hms(res, value_repr, hms)
+
+        elif idx + 2 < len_l and tokens[idx + 1] == ':':
+            # HH:MM[:SS[.ss]]
+            res.hour = int(value)
+            value = self._to_decimal(tokens[idx + 2])  # TODO: try/except for this?
+            (res.minute, res.second) = self._parse_min_sec(value)
+
+            if idx + 4 < len_l and tokens[idx + 3] == ':':
+                res.second, res.microsecond = self._parsems(tokens[idx + 4])
+
+                idx += 2
+
+            idx += 2
+
+        elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'):
+            sep = tokens[idx + 1]
+            ymd.append(value_repr)
+
+            if idx + 2 < len_l and not info.jump(tokens[idx + 2]):
+                if tokens[idx + 2].isdigit():
+                    # 01-01[-01]
+                    ymd.append(tokens[idx + 2])
+                else:
+                    # 01-Jan[-01]
+                    value = info.month(tokens[idx + 2])
+
+                    if value is not None:
+                        ymd.append(value, 'M')
+                    else:
+                        raise ValueError()
+
+                if idx + 3 < len_l and tokens[idx + 3] == sep:
+                    # We have three members
+                    value = info.month(tokens[idx + 4])
+
+                    if value is not None:
+                        ymd.append(value, 'M')
+                    else:
+                        ymd.append(tokens[idx + 4])
+                    idx += 2
+
+                idx += 1
+            idx += 1
+
+        elif idx + 1 >= len_l or info.jump(tokens[idx + 1]):
+            if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None:
+                # 12 am
+                hour = int(value)
+                res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2]))
+                idx += 1
+            else:
+                # Year, month or day
+                ymd.append(value)
+            idx += 1
+
+        elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24):
+            # 12am
+            hour = int(value)
+            res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1]))
+            idx += 1
+
+        elif ymd.could_be_day(value):
+            ymd.append(value)
+
+        elif not fuzzy:
+            raise ValueError()
+
+        return idx
+
+    def _find_hms_idx(self, idx, tokens, info, allow_jump):
+        len_l = len(tokens)
+
+        if idx+1 < len_l and info.hms(tokens[idx+1]) is not None:
+            # There is an "h", "m", or "s" label following this token.  We take
+            # assign the upcoming label to the current token.
+            # e.g. the "12" in 12h"
+            hms_idx = idx + 1
+
+        elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and
+              info.hms(tokens[idx+2]) is not None):
+            # There is a space and then an "h", "m", or "s" label.
+            # e.g. the "12" in "12 h"
+            hms_idx = idx + 2
+
+        elif idx > 0 and info.hms(tokens[idx-1]) is not None:
+            # There is a "h", "m", or "s" preceeding this token.  Since neither
+            # of the previous cases was hit, there is no label following this
+            # token, so we use the previous label.
+            # e.g. the "04" in "12h04"
+            hms_idx = idx-1
+
+        elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and
+              info.hms(tokens[idx-2]) is not None):
+            # If we are looking at the final token, we allow for a
+            # backward-looking check to skip over a space.
+            # TODO: Are we sure this is the right condition here?
+            hms_idx = idx - 2
+
+        else:
+            hms_idx = None
+
+        return hms_idx
+
+    def _assign_hms(self, res, value_repr, hms):
+        # See GH issue #427, fixing float rounding
+        value = self._to_decimal(value_repr)
+
+        if hms == 0:
+            # Hour
+            res.hour = int(value)
+            if value % 1:
+                res.minute = int(60*(value % 1))
+
+        elif hms == 1:
+            (res.minute, res.second) = self._parse_min_sec(value)
+
+        elif hms == 2:
+            (res.second, res.microsecond) = self._parsems(value_repr)
+
+    def _could_be_tzname(self, hour, tzname, tzoffset, token):
+        return (hour is not None and
+                tzname is None and
+                tzoffset is None and
+                len(token) <= 5 and
+                all(x in string.ascii_uppercase for x in token))
+
+    def _ampm_valid(self, hour, ampm, fuzzy):
+        """
+        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 ampm is not None:
+            val_is_ampm = False
+
+        # If AM/PM is found and hour is not, raise a ValueError
+        if hour is None:
+            if fuzzy:
+                val_is_ampm = False
+            else:
+                raise ValueError('No hour specified with AM or PM flag.')
+        elif not 0 <= 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.')
+
+        return val_is_ampm
+
+    def _adjust_ampm(self, hour, ampm):
+        if hour < 12 and ampm == 1:
+            hour += 12
+        elif hour == 12 and ampm == 0:
+            hour = 0
+        return hour
+
+    def _parse_min_sec(self, value):
+        # TODO: Every usage of this function sets res.second to the return
+        # value. Are there any cases where second will be returned as None and
+        # we *dont* want to set res.second = None?
+        minute = int(value)
+        second = None
+
+        sec_remainder = value % 1
+        if sec_remainder:
+            second = int(60 * sec_remainder)
+        return (minute, second)
+
+    def _parsems(self, 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])
+
+    def _parse_hms(self, idx, tokens, info, hms_idx):
+        # TODO: Is this going to admit a lot of false-positives for when we
+        # just happen to have digits and "h", "m" or "s" characters in non-date
+        # text?  I guess hex hashes won't have that problem, but there's plenty
+        # of random junk out there.
+        if hms_idx is None:
+            hms = None
+            new_idx = idx
+        elif hms_idx > idx:
+            hms = info.hms(tokens[hms_idx])
+            new_idx = hms_idx
+        else:
+            # Looking backwards, increment one.
+            hms = info.hms(tokens[hms_idx]) + 1
+            new_idx = idx
+
+        return (new_idx, hms)
+
+    def _recombine_skipped(self, tokens, skipped_idxs):
+        """
+        >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
+        >>> skipped_idxs = [0, 1, 2, 5]
+        >>> _recombine_skipped(tokens, skipped_idxs)
+        ["foo bar", "baz"]
+        """
+        skipped_tokens = []
+        for i, idx in enumerate(sorted(skipped_idxs)):
+            if i > 0 and idx - 1 == skipped_idxs[i - 1]:
+                skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
+            else:
+                skipped_tokens.append(tokens[idx])
+
+        return skipped_tokens
+
+    def _build_tzinfo(self, tzinfos, tzname, tzoffset):
+        if callable(tzinfos):
+            tzdata = tzinfos(tzname, tzoffset)
+        else:
+            tzdata = tzinfos.get(tzname)
+        # handle case where tzinfo is paased an options that returns None
+        # eg tzinfos = {'BRST' : None}
+        if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
+            tzinfo = tzdata
+        elif isinstance(tzdata, text_type):
+            tzinfo = tz.tzstr(tzdata)
+        elif isinstance(tzdata, integer_types):
+            tzinfo = tz.tzoffset(tzname, tzdata)
+        return tzinfo
+
+    def _build_tzaware(self, naive, res, tzinfos):
+        if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)):
+            tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
+            aware = naive.replace(tzinfo=tzinfo)
+            aware = self._assign_tzname(aware, res.tzname)
+
+        elif res.tzname and res.tzname in time.tzname:
+            aware = naive.replace(tzinfo=tz.tzlocal())
+
+            # Handle ambiguous local datetime
+            aware = self._assign_tzname(aware, res.tzname)
+
+            # This is mostly relevant for winter GMT zones parsed in the UK
+            if (aware.tzname() != res.tzname and
+                    res.tzname in self.info.UTCZONE):
+                aware = aware.replace(tzinfo=tz.tzutc())
+
+        elif res.tzoffset == 0:
+            aware = naive.replace(tzinfo=tz.tzutc())
+
+        elif res.tzoffset:
+            aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+
+        elif not res.tzname and not res.tzoffset:
+            # i.e. no timezone information was found.
+            aware = naive
+
+        elif res.tzname:
+            # tz-like string was parsed but we don't know what to do
+            # with it
+            warnings.warn("tzname {tzname} identified but not understood.  "
+                          "Pass `tzinfos` argument in order to correctly "
+                          "return a timezone-aware datetime.  In a future "
+                          "version, this will raise an "
+                          "exception.".format(tzname=res.tzname),
+                          category=UnknownTimezoneWarning)
+            aware = naive
+
+        return aware
+
+    def _build_naive(self, res, default):
+        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]
+
+        naive = default.replace(**repl)
+
+        if res.weekday is not None and not res.day:
+            naive = naive + relativedelta.relativedelta(weekday=res.weekday)
+
+        return naive
+
+    def _assign_tzname(self, dt, tzname):
+        if dt.tzname() != tzname:
+            new_dt = tz.enfold(dt, fold=1)
+            if new_dt.tzname() == tzname:
+                return new_dt
+
+        return dt
+
+    def _to_decimal(self, val):
+        try:
+            decimal_value = Decimal(val)
+            # See GH 662, edge case, infinite value should not be converted via `_to_decimal`
+            if not decimal_value.is_finite():
+                raise ValueError("Converted decimal value is infinite or NaN")
+        except Exception as e:
+            msg = "Could not convert %s to decimal" % val
+            six.raise_from(ValueError(msg), e)
+        else:
+            return decimal_value
+
+
+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 seconds or a :class:`tzinfo` object.
+
+        .. doctest::
+           :options: +NORMALIZE_WHITESPACE
+
+            >>> from dateutil.parser import parse
+            >>> from dateutil.tz import gettz
+            >>> tzinfos = {"BRST": -7200, "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', -7200))
+            >>> 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(2047, 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 = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x]
+        used_idxs = list()
+        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])
+
+                    for ii in range(j):
+                        used_idxs.append(ii)
+                    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] == '+']
+                            used_idxs.append(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)
+                            used_idxs.append(i)
+                            i += 2
+                        elif len_li <= 2:
+                            # -[0]3
+                            setattr(res, offattr,
+                                    int(l[i][:2]) * 3600 * signal)
+                        else:
+                            return None
+                        used_idxs.append(i)
+                        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])
+                    used_idxs.append(i)
+                    i += 2
+                    if l[i] == '-':
+                        value = int(l[i + 1]) * -1
+                        used_idxs.append(i)
+                        i += 1
+                    else:
+                        value = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                    if value:
+                        x.week = value
+                        x.weekday = (int(l[i]) - 1) % 7
+                    else:
+                        x.day = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                    x.time = int(l[i])
+                    used_idxs.append(i)
+                    i += 2
+                if i < len_l:
+                    if l[i] in ('-', '+'):
+                        signal = (-1, 1)[l[i] == "+"]
+                        used_idxs.append(i)
+                        i += 1
+                    else:
+                        signal = 1
+                    used_idxs.append(i)
+                    res.dstoffset = (res.stdoffset + int(l[i]) * signal)
+
+                # This was a made-up format that is not in normal use
+                warn(('Parsed time zone "%s"' % tzstr) +
+                     'is in a non-standard dateutil-specific format, which ' +
+                     'is now deprecated; support for parsing this format ' +
+                     'will be removed in future versions. It is recommended ' +
+                     'that you switch to a standard format like the GNU ' +
+                     'TZ variable format.', tz.DeprecatedTzFormatWarning)
+            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)
+                        used_idxs.append(i)
+                        i += 1
+                        x.jyday = int(l[i])
+                    elif l[i] == 'M':
+                        # month[-.]week[-.]weekday
+                        used_idxs.append(i)
+                        i += 1
+                        x.month = int(l[i])
+                        used_idxs.append(i)
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        used_idxs.append(i)
+                        i += 1
+                        x.week = int(l[i])
+                        if x.week == 5:
+                            x.week = -1
+                        used_idxs.append(i)
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        used_idxs.append(i)
+                        i += 1
+                        x.weekday = (int(l[i]) - 1) % 7
+                    else:
+                        # year day (zero based)
+                        x.yday = int(l[i]) + 1
+
+                    used_idxs.append(i)
+                    i += 1
+
+                    if i < len_l and l[i] == '/':
+                        used_idxs.append(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
+                            used_idxs.append(i)
+                            i += 2
+                            if i + 1 < len_l and l[i + 1] == ':':
+                                used_idxs.append(i)
+                                i += 2
+                                x.time += int(l[i])
+                        elif len_li <= 2:
+                            # -[0]3
+                            x.time = (int(l[i][:2]) * 3600)
+                        else:
+                            return None
+                        used_idxs.append(i)
+                        i += 1
+
+                    assert i == len_l or l[i] == ','
+
+                    i += 1
+
+                assert i >= len_l
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+
+        unused_idxs = set(range(len_l)).difference(used_idxs)
+        res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"})
+        return res
+
+
+DEFAULTTZPARSER = _tzparser()
+
+
+def _parsetz(tzstr):
+    return DEFAULTTZPARSER.parse(tzstr)
+
+class UnknownTimezoneWarning(RuntimeWarning):
+    """Raised when the parser finds a timezone it cannot parse into a tzinfo"""
+# vim:ts=4:sw=4:et
diff --git a/resources/lib/libraries/dateutil/parser/isoparser.py b/resources/lib/libraries/dateutil/parser/isoparser.py
new file mode 100644
index 00000000..b63ef712
--- /dev/null
+++ b/resources/lib/libraries/dateutil/parser/isoparser.py
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers a parser for ISO-8601 strings
+
+It is intended to support all valid date, time and datetime formats per the
+ISO-8601 specification.
+
+..versionadded:: 2.7.0
+"""
+from datetime import datetime, timedelta, time, date
+import calendar
+from .. import tz
+
+from functools import wraps
+
+import re
+from .. import six
+
+__all__ = ["isoparse", "isoparser"]
+
+
+def _takes_ascii(f):
+    @wraps(f)
+    def func(self, str_in, *args, **kwargs):
+        # If it's a stream, read the whole thing
+        str_in = getattr(str_in, 'read', lambda: str_in)()
+
+        # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
+        if isinstance(str_in, six.text_type):
+            # ASCII is the same in UTF-8
+            try:
+                str_in = str_in.encode('ascii')
+            except UnicodeEncodeError as e:
+                msg = 'ISO-8601 strings should contain only ASCII characters'
+                six.raise_from(ValueError(msg), e)
+
+        return f(self, str_in, *args, **kwargs)
+
+    return func
+
+
+class isoparser(object):
+    def __init__(self, sep=None):
+        """
+        :param sep:
+            A single character that separates date and time portions. If
+            ``None``, the parser will accept any single character.
+            For strict ISO-8601 adherence, pass ``'T'``.
+        """
+        if sep is not None:
+            if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
+                raise ValueError('Separator must be a single, non-numeric ' +
+                                 'ASCII character')
+
+            sep = sep.encode('ascii')
+
+        self._sep = sep
+
+    @_takes_ascii
+    def isoparse(self, dt_str):
+        """
+        Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
+
+        An ISO-8601 datetime string consists of a date portion, followed
+        optionally by a time portion - the date and time portions are separated
+        by a single character separator, which is ``T`` in the official
+        standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
+        combined with a time portion.
+
+        Supported date formats are:
+
+        Common:
+
+        - ``YYYY``
+        - ``YYYY-MM`` or ``YYYYMM``
+        - ``YYYY-MM-DD`` or ``YYYYMMDD``
+
+        Uncommon:
+
+        - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
+        - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
+
+        The ISO week and day numbering follows the same logic as
+        :func:`datetime.date.isocalendar`.
+
+        Supported time formats are:
+
+        - ``hh``
+        - ``hh:mm`` or ``hhmm``
+        - ``hh:mm:ss`` or ``hhmmss``
+        - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits)
+
+        Midnight is a special case for `hh`, as the standard supports both
+        00:00 and 24:00 as a representation.
+
+        .. caution::
+
+            Support for fractional components other than seconds is part of the
+            ISO-8601 standard, but is not currently implemented in this parser.
+
+        Supported time zone offset formats are:
+
+        - `Z` (UTC)
+        - `±HH:MM`
+        - `±HHMM`
+        - `±HH`
+
+        Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
+        with the exception of UTC, which will be represented as
+        :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
+        as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
+
+        :param dt_str:
+            A string or stream containing only an ISO-8601 datetime string
+
+        :return:
+            Returns a :class:`datetime.datetime` representing the string.
+            Unspecified components default to their lowest value.
+
+        .. warning::
+
+            As of version 2.7.0, the strictness of the parser should not be
+            considered a stable part of the contract. Any valid ISO-8601 string
+            that parses correctly with the default settings will continue to
+            parse correctly in future versions, but invalid strings that
+            currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
+            guaranteed to continue failing in future versions if they encode
+            a valid date.
+
+        .. versionadded:: 2.7.0
+        """
+        components, pos = self._parse_isodate(dt_str)
+
+        if len(dt_str) > pos:
+            if self._sep is None or dt_str[pos:pos + 1] == self._sep:
+                components += self._parse_isotime(dt_str[pos + 1:])
+            else:
+                raise ValueError('String contains unknown ISO components')
+
+        return datetime(*components)
+
+    @_takes_ascii
+    def parse_isodate(self, datestr):
+        """
+        Parse the date portion of an ISO string.
+
+        :param datestr:
+            The string portion of an ISO string, without a separator
+
+        :return:
+            Returns a :class:`datetime.date` object
+        """
+        components, pos = self._parse_isodate(datestr)
+        if pos < len(datestr):
+            raise ValueError('String contains unknown ISO ' +
+                             'components: {}'.format(datestr))
+        return date(*components)
+
+    @_takes_ascii
+    def parse_isotime(self, timestr):
+        """
+        Parse the time portion of an ISO string.
+
+        :param timestr:
+            The time portion of an ISO string, without a separator
+
+        :return:
+            Returns a :class:`datetime.time` object
+        """
+        return time(*self._parse_isotime(timestr))
+
+    @_takes_ascii
+    def parse_tzstr(self, tzstr, zero_as_utc=True):
+        """
+        Parse a valid ISO time zone string.
+
+        See :func:`isoparser.isoparse` for details on supported formats.
+
+        :param tzstr:
+            A string representing an ISO time zone offset
+
+        :param zero_as_utc:
+            Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
+
+        :return:
+            Returns :class:`dateutil.tz.tzoffset` for offsets and
+            :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
+            specified) offsets equivalent to UTC.
+        """
+        return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
+
+    # Constants
+    _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+')
+    _DATE_SEP = b'-'
+    _TIME_SEP = b':'
+    _MICRO_SEP = b'.'
+
+    def _parse_isodate(self, dt_str):
+        try:
+            return self._parse_isodate_common(dt_str)
+        except ValueError:
+            return self._parse_isodate_uncommon(dt_str)
+
+    def _parse_isodate_common(self, dt_str):
+        len_str = len(dt_str)
+        components = [1, 1, 1]
+
+        if len_str < 4:
+            raise ValueError('ISO string too short')
+
+        # Year
+        components[0] = int(dt_str[0:4])
+        pos = 4
+        if pos >= len_str:
+            return components, pos
+
+        has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
+        if has_sep:
+            pos += 1
+
+        # Month
+        if len_str - pos < 2:
+            raise ValueError('Invalid common month')
+
+        components[1] = int(dt_str[pos:pos + 2])
+        pos += 2
+
+        if pos >= len_str:
+            if has_sep:
+                return components, pos
+            else:
+                raise ValueError('Invalid ISO format')
+
+        if has_sep:
+            if dt_str[pos:pos + 1] != self._DATE_SEP:
+                raise ValueError('Invalid separator in ISO string')
+            pos += 1
+
+        # Day
+        if len_str - pos < 2:
+            raise ValueError('Invalid common day')
+        components[2] = int(dt_str[pos:pos + 2])
+        return components, pos + 2
+
+    def _parse_isodate_uncommon(self, dt_str):
+        if len(dt_str) < 4:
+            raise ValueError('ISO string too short')
+
+        # All ISO formats start with the year
+        year = int(dt_str[0:4])
+
+        has_sep = dt_str[4:5] == self._DATE_SEP
+
+        pos = 4 + has_sep       # Skip '-' if it's there
+        if dt_str[pos:pos + 1] == b'W':
+            # YYYY-?Www-?D?
+            pos += 1
+            weekno = int(dt_str[pos:pos + 2])
+            pos += 2
+
+            dayno = 1
+            if len(dt_str) > pos:
+                if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
+                    raise ValueError('Inconsistent use of dash separator')
+
+                pos += has_sep
+
+                dayno = int(dt_str[pos:pos + 1])
+                pos += 1
+
+            base_date = self._calculate_weekdate(year, weekno, dayno)
+        else:
+            # YYYYDDD or YYYY-DDD
+            if len(dt_str) - pos < 3:
+                raise ValueError('Invalid ordinal day')
+
+            ordinal_day = int(dt_str[pos:pos + 3])
+            pos += 3
+
+            if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
+                raise ValueError('Invalid ordinal day' +
+                                 ' {} for year {}'.format(ordinal_day, year))
+
+            base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
+
+        components = [base_date.year, base_date.month, base_date.day]
+        return components, pos
+
+    def _calculate_weekdate(self, year, week, day):
+        """
+        Calculate the day of corresponding to the ISO year-week-day calendar.
+
+        This function is effectively the inverse of
+        :func:`datetime.date.isocalendar`.
+
+        :param year:
+            The year in the ISO calendar
+
+        :param week:
+            The week in the ISO calendar - range is [1, 53]
+
+        :param day:
+            The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
+
+        :return:
+            Returns a :class:`datetime.date`
+        """
+        if not 0 < week < 54:
+            raise ValueError('Invalid week: {}'.format(week))
+
+        if not 0 < day < 8:     # Range is 1-7
+            raise ValueError('Invalid weekday: {}'.format(day))
+
+        # Get week 1 for the specific year:
+        jan_4 = date(year, 1, 4)   # Week 1 always has January 4th in it
+        week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
+
+        # Now add the specific number of weeks and days to get what we want
+        week_offset = (week - 1) * 7 + (day - 1)
+        return week_1 + timedelta(days=week_offset)
+
+    def _parse_isotime(self, timestr):
+        len_str = len(timestr)
+        components = [0, 0, 0, 0, None]
+        pos = 0
+        comp = -1
+
+        if len(timestr) < 2:
+            raise ValueError('ISO time too short')
+
+        has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP
+
+        while pos < len_str and comp < 5:
+            comp += 1
+
+            if timestr[pos:pos + 1] in b'-+Z':
+                # Detect time zone boundary
+                components[-1] = self._parse_tzstr(timestr[pos:])
+                pos = len_str
+                break
+
+            if comp < 3:
+                # Hour, minute, second
+                components[comp] = int(timestr[pos:pos + 2])
+                pos += 2
+                if (has_sep and pos < len_str and
+                        timestr[pos:pos + 1] == self._TIME_SEP):
+                    pos += 1
+
+            if comp == 3:
+                # Microsecond
+                if timestr[pos:pos + 1] != self._MICRO_SEP:
+                    continue
+
+                pos += 1
+                us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6],
+                                                           1)[0]
+
+                components[comp] = int(us_str) * 10**(6 - len(us_str))
+                pos += len(us_str)
+
+        if pos < len_str:
+            raise ValueError('Unused components in ISO string')
+
+        if components[0] == 24:
+            # Standard supports 00:00 and 24:00 as representations of midnight
+            if any(component != 0 for component in components[1:4]):
+                raise ValueError('Hour may only be 24 at 24:00:00.000')
+            components[0] = 0
+
+        return components
+
+    def _parse_tzstr(self, tzstr, zero_as_utc=True):
+        if tzstr == b'Z':
+            return tz.tzutc()
+
+        if len(tzstr) not in {3, 5, 6}:
+            raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
+
+        if tzstr[0:1] == b'-':
+            mult = -1
+        elif tzstr[0:1] == b'+':
+            mult = 1
+        else:
+            raise ValueError('Time zone offset requires sign')
+
+        hours = int(tzstr[1:3])
+        if len(tzstr) == 3:
+            minutes = 0
+        else:
+            minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
+
+        if zero_as_utc and hours == 0 and minutes == 0:
+            return tz.tzutc()
+        else:
+            if minutes > 59:
+                raise ValueError('Invalid minutes in time zone offset')
+
+            if hours > 23:
+                raise ValueError('Invalid hours in time zone offset')
+
+            return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
+
+
+DEFAULT_ISOPARSER = isoparser()
+isoparse = DEFAULT_ISOPARSER.isoparse
diff --git a/resources/lib/libraries/dateutil/relativedelta.py b/resources/lib/libraries/dateutil/relativedelta.py
new file mode 100644
index 00000000..1e0d6165
--- /dev/null
+++ b/resources/lib/libraries/dateutil/relativedelta.py
@@ -0,0 +1,590 @@
+# -*- 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 <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ 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 arithmetic
+            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. Notice that
+            if the calculated date is already Monday, for example,
+            using MO(1) or MO(-1) won't change the day.
+
+        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.
+
+    There are relative and absolute forms of the keyword
+    arguments. The plural is relative, and the singular is
+    absolute. For each argument in the order below, the absolute form
+    is applied first (by setting each attribute to that value) and
+    then the relative form (by adding the value to the attribute).
+
+    The order of attributes considered when this relativedelta is
+    added to a datetime is:
+
+    1. Year
+    2. Month
+    3. Day
+    4. Hours
+    5. Minutes
+    6. Seconds
+    7. Microseconds
+
+    Finally, weekday is applied, using the rule described above.
+
+    For example
+
+    >>> dt = datetime(2018, 4, 9, 13, 37, 0)
+    >>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
+    datetime(2018, 4, 2, 14, 37, 0)
+
+    First, the day is set to 1 (the first of the month), then 25 hours
+    are added, to get to the 2nd day and 14th hour, finally the
+    weekday is applied, but since the 2nd is already a Monday there is
+    no effect.
+
+    """
+
+    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):
+
+        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:
+            # 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.")
+
+            # Relative information
+            self.years = int(years)
+            self.months = int(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 int(self.days / 7.0)
+
+    @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 if other.year is not None
+                                       else self.year),
+                                 month=(other.month if other.month is not None
+                                        else self.month),
+                                 day=(other.day if other.day is not None
+                                      else self.day),
+                                 weekday=(other.weekday if other.weekday is not None
+                                          else self.weekday),
+                                 hour=(other.hour if other.hour is not None
+                                       else self.hour),
+                                 minute=(other.minute if other.minute is not None
+                                         else self.minute),
+                                 second=(other.second if other.second is not None
+                                         else self.second),
+                                 microsecond=(other.microsecond if other.microsecond
+                                              is not None else
+                                              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 if self.year is not None
+                                   else other.year),
+                             month=(self.month if self.month is not None else
+                                    other.month),
+                             day=(self.day if self.day is not None else
+                                  other.day),
+                             weekday=(self.weekday if self.weekday is not None else
+                                      other.weekday),
+                             hour=(self.hour if self.hour is not None else
+                                   other.hour),
+                             minute=(self.minute if self.minute is not None else
+                                     other.minute),
+                             second=(self.second if self.second is not None else
+                                     other.second),
+                             microsecond=(self.microsecond if self.microsecond
+                                          is not None else
+                                          other.microsecond))
+
+    def __abs__(self):
+        return self.__class__(years=abs(self.years),
+                              months=abs(self.months),
+                              days=abs(self.days),
+                              hours=abs(self.hours),
+                              minutes=abs(self.minutes),
+                              seconds=abs(self.seconds),
+                              microseconds=abs(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 __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)
+
+    def __hash__(self):
+        return hash((
+            self.weekday,
+            self.years,
+            self.months,
+            self.days,
+            self.hours,
+            self.minutes,
+            self.seconds,
+            self.microseconds,
+            self.leapdays,
+            self.year,
+            self.month,
+            self.day,
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+        ))
+
+    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
diff --git a/resources/lib/libraries/dateutil/rrule.py b/resources/lib/libraries/dateutil/rrule.py
new file mode 100644
index 00000000..8e9c2af1
--- /dev/null
+++ b/resources/lib/libraries/dateutil/rrule.py
@@ -0,0 +1,1672 @@
+# -*- coding: utf-8 -*-
+"""
+The rrule module offers a small, complete, and very fast, implementation of
+the recurrence rules documented in the
+`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
+including support for caching of results.
+"""
+import itertools
+import datetime
+import calendar
+import re
+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
+from .tz import tzutc, tzlocal
+
+# 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):
+                if count is not None:
+                    n += 1
+                    if n > count:
+                        break
+
+                yield d
+
+    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 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-5545 Sec. 3.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-5545 Sec. 3.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 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.
+    :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 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.
+     """
+    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:
+            if until and until.tzinfo:
+                dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+            else:           
+                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 self._dtstart and self._until:
+            if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
+                # According to RFC5545 Section 3.3.10:
+                # https://tools.ietf.org/html/rfc5545#section-3.3.10
+                #
+                # > If the "DTSTART" property is specified as a date with UTC
+                # > time or a date with local time and time zone reference,
+                # > then the UNTIL rule part MUST be specified as a date with
+                # > UTC time.
+                raise ValueError(
+                    'RRULE UNTIL values must be specified in UTC when DTSTART '
+                    'is timezone-aware'
+                )
+
+        if count is not None and until:
+            warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
+                 " 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 = ()
+
+            if self._bynweekday is not None:
+                self._bynweekday = tuple(sorted(self._bynweekday))
+                orig_bynweekday = [weekday(*x) for x in self._bynweekday]
+            else:
+                orig_bynweekday = ()
+
+            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 = {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 = {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 RFC5545, 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 is not None:
+            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
+            # RFC5545-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('RRULE:' + ';'.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:
+                        if count is not None:
+                            count -= 1
+                            if count < 0:
+                                self._len = total
+                                return
+                        total += 1
+                        yield res
+            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:
+                                if count is not None:
+                                    count -= 1
+                                    if count < 0:
+                                        self._len = total
+                                        return
+
+                                total += 1
+                                yield res
+
+            # 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,
+                   tzids=None,
+                   tzinfos=None):
+        global parser
+        if compatible:
+            forceset = True
+            unfold = True
+
+        TZID_NAMES = dict(map(
+            lambda x: (x.upper(), x),
+            re.findall('TZID=(?P<name>[^:]+):', s)
+        ))
+        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 EXDATE parm: "+parm)
+                    exdatevals.append(value)
+                elif name == "DTSTART":
+                    # RFC 5445 3.8.2.4: The VALUE parameter is optional, but
+                    # may be found only once.
+                    value_found = False
+                    TZID = None
+                    valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
+                    for parm in parms:
+                        if parm.startswith("TZID="):
+                            try:
+                                tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
+                            except KeyError:
+                                continue
+                            if tzids is None:
+                                from . import tz
+                                tzlookup = tz.gettz
+                            elif callable(tzids):
+                                tzlookup = tzids
+                            else:
+                                tzlookup = getattr(tzids, 'get', None)
+                                if tzlookup is None:
+                                    msg = ('tzids must be a callable, ' +
+                                           'mapping, or None, ' +
+                                           'not %s' % tzids)
+                                    raise ValueError(msg)
+
+                            TZID = tzlookup(tzkey)
+                            continue
+                        if parm not in valid_values:
+                            raise ValueError("unsupported DTSTART parm: "+parm)
+                        else:
+                            if value_found:
+                                msg = ("Duplicate value parameter found in " +
+                                       "DTSTART: " + parm)
+                                raise ValueError(msg)
+                            value_found = True
+                    if not parser:
+                        from dateutil import parser
+                    dtstart = parser.parse(value, ignoretz=ignoretz,
+                                           tzinfos=tzinfos)
+                    if TZID is not None:
+                        if dtstart.tzinfo is None:
+                            dtstart = dtstart.replace(tzinfo=TZID)
+                        else:
+                            raise ValueError('DTSTART specifies multiple timezones')
+                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
diff --git a/resources/lib/libraries/dateutil/six.py b/resources/lib/libraries/dateutil/six.py
new file mode 100644
index 00000000..6bf4fd38
--- /dev/null
+++ b/resources/lib/libraries/dateutil/six.py
@@ -0,0 +1,891 @@
+# Copyright (c) 2010-2017 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson <benjamin@python.org>"
+__version__ = "1.11.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+    string_types = str,
+    integer_types = int,
+    class_types = type,
+    text_type = str
+    binary_type = bytes
+
+    MAXSIZE = sys.maxsize
+else:
+    string_types = basestring,
+    integer_types = (int, long)
+    class_types = (type, types.ClassType)
+    text_type = unicode
+    binary_type = str
+
+    if sys.platform.startswith("java"):
+        # Jython always uses 32 bits.
+        MAXSIZE = int((1 << 31) - 1)
+    else:
+        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+        class X(object):
+
+            def __len__(self):
+                return 1 << 31
+        try:
+            len(X())
+        except OverflowError:
+            # 32-bit
+            MAXSIZE = int((1 << 31) - 1)
+        else:
+            # 64-bit
+            MAXSIZE = int((1 << 63) - 1)
+        del X
+
+
+def _add_doc(func, doc):
+    """Add documentation to a function."""
+    func.__doc__ = doc
+
+
+def _import_module(name):
+    """Import module, returning the module after the last dot."""
+    __import__(name)
+    return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+    def __init__(self, name):
+        self.name = name
+
+    def __get__(self, obj, tp):
+        result = self._resolve()
+        setattr(obj, self.name, result)  # Invokes __set__.
+        try:
+            # This is a bit ugly, but it avoids running this again by
+            # removing this descriptor.
+            delattr(obj.__class__, self.name)
+        except AttributeError:
+            pass
+        return result
+
+
+class MovedModule(_LazyDescr):
+
+    def __init__(self, name, old, new=None):
+        super(MovedModule, self).__init__(name)
+        if PY3:
+            if new is None:
+                new = name
+            self.mod = new
+        else:
+            self.mod = old
+
+    def _resolve(self):
+        return _import_module(self.mod)
+
+    def __getattr__(self, attr):
+        _module = self._resolve()
+        value = getattr(_module, attr)
+        setattr(self, attr, value)
+        return value
+
+
+class _LazyModule(types.ModuleType):
+
+    def __init__(self, name):
+        super(_LazyModule, self).__init__(name)
+        self.__doc__ = self.__class__.__doc__
+
+    def __dir__(self):
+        attrs = ["__doc__", "__name__"]
+        attrs += [attr.name for attr in self._moved_attributes]
+        return attrs
+
+    # Subclasses should override this
+    _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+        super(MovedAttribute, self).__init__(name)
+        if PY3:
+            if new_mod is None:
+                new_mod = name
+            self.mod = new_mod
+            if new_attr is None:
+                if old_attr is None:
+                    new_attr = name
+                else:
+                    new_attr = old_attr
+            self.attr = new_attr
+        else:
+            self.mod = old_mod
+            if old_attr is None:
+                old_attr = name
+            self.attr = old_attr
+
+    def _resolve(self):
+        module = _import_module(self.mod)
+        return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+
+    """
+    A meta path importer to import six.moves and its submodules.
+
+    This class implements a PEP302 finder and loader. It should be compatible
+    with Python 2.5 and all existing versions of Python3
+    """
+
+    def __init__(self, six_module_name):
+        self.name = six_module_name
+        self.known_modules = {}
+
+    def _add_module(self, mod, *fullnames):
+        for fullname in fullnames:
+            self.known_modules[self.name + "." + fullname] = mod
+
+    def _get_module(self, fullname):
+        return self.known_modules[self.name + "." + fullname]
+
+    def find_module(self, fullname, path=None):
+        if fullname in self.known_modules:
+            return self
+        return None
+
+    def __get_module(self, fullname):
+        try:
+            return self.known_modules[fullname]
+        except KeyError:
+            raise ImportError("This loader does not know module " + fullname)
+
+    def load_module(self, fullname):
+        try:
+            # in case of a reload
+            return sys.modules[fullname]
+        except KeyError:
+            pass
+        mod = self.__get_module(fullname)
+        if isinstance(mod, MovedModule):
+            mod = mod._resolve()
+        else:
+            mod.__loader__ = self
+        sys.modules[fullname] = mod
+        return mod
+
+    def is_package(self, fullname):
+        """
+        Return true, if the named module is a package.
+
+        We need this method to get correct spec objects with
+        Python 3.4 (see PEP451)
+        """
+        return hasattr(self.__get_module(fullname), "__path__")
+
+    def get_code(self, fullname):
+        """Return None
+
+        Required, if is_package is implemented"""
+        self.__get_module(fullname)  # eventually raises ImportError
+        return None
+    get_source = get_code  # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
+    """Lazy loading of moved objects"""
+    __path__ = []  # mark as package
+
+
+_moved_attributes = [
+    MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+    MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+    MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+    MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+    MovedAttribute("intern", "__builtin__", "sys"),
+    MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+    MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+    MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
+    MovedAttribute("getoutput", "commands", "subprocess"),
+    MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+    MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
+    MovedAttribute("reduce", "__builtin__", "functools"),
+    MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+    MovedAttribute("StringIO", "StringIO", "io"),
+    MovedAttribute("UserDict", "UserDict", "collections"),
+    MovedAttribute("UserList", "UserList", "collections"),
+    MovedAttribute("UserString", "UserString", "collections"),
+    MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+    MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+    MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+    MovedModule("builtins", "__builtin__"),
+    MovedModule("configparser", "ConfigParser"),
+    MovedModule("copyreg", "copy_reg"),
+    MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+    MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
+    MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+    MovedModule("http_cookies", "Cookie", "http.cookies"),
+    MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+    MovedModule("html_parser", "HTMLParser", "html.parser"),
+    MovedModule("http_client", "httplib", "http.client"),
+    MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+    MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
+    MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+    MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+    MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+    MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+    MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+    MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+    MovedModule("cPickle", "cPickle", "pickle"),
+    MovedModule("queue", "Queue"),
+    MovedModule("reprlib", "repr"),
+    MovedModule("socketserver", "SocketServer"),
+    MovedModule("_thread", "thread", "_thread"),
+    MovedModule("tkinter", "Tkinter"),
+    MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+    MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+    MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+    MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+    MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+    MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+    MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+    MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+    MovedModule("tkinter_colorchooser", "tkColorChooser",
+                "tkinter.colorchooser"),
+    MovedModule("tkinter_commondialog", "tkCommonDialog",
+                "tkinter.commondialog"),
+    MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+    MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+    MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+    MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+                "tkinter.simpledialog"),
+    MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+    MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+    MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+    MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+    MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+    MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+]
+# Add windows specific modules.
+if sys.platform == "win32":
+    _moved_attributes += [
+        MovedModule("winreg", "_winreg"),
+    ]
+
+for attr in _moved_attributes:
+    setattr(_MovedItems, attr.name, attr)
+    if isinstance(attr, MovedModule):
+        _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+
+    """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+    MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+    MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+    MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+    MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+    MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+    MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+    MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+    MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+    MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+    MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+    MovedAttribute("quote", "urllib", "urllib.parse"),
+    MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+    MovedAttribute("unquote", "urllib", "urllib.parse"),
+    MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+    MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
+    MovedAttribute("urlencode", "urllib", "urllib.parse"),
+    MovedAttribute("splitquery", "urllib", "urllib.parse"),
+    MovedAttribute("splittag", "urllib", "urllib.parse"),
+    MovedAttribute("splituser", "urllib", "urllib.parse"),
+    MovedAttribute("splitvalue", "urllib", "urllib.parse"),
+    MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+    MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+    MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+    MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+    MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+    setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+                      "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+
+    """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+    MovedAttribute("URLError", "urllib2", "urllib.error"),
+    MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+    MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+    setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+                      "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+
+    """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+    MovedAttribute("urlopen", "urllib2", "urllib.request"),
+    MovedAttribute("install_opener", "urllib2", "urllib.request"),
+    MovedAttribute("build_opener", "urllib2", "urllib.request"),
+    MovedAttribute("pathname2url", "urllib", "urllib.request"),
+    MovedAttribute("url2pathname", "urllib", "urllib.request"),
+    MovedAttribute("getproxies", "urllib", "urllib.request"),
+    MovedAttribute("Request", "urllib2", "urllib.request"),
+    MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+    MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+    MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+    MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+    MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+    MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+    MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+    MovedAttribute("URLopener", "urllib", "urllib.request"),
+    MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+    MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+    MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
+    MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+    setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+                      "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+
+    """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+    MovedAttribute("addbase", "urllib", "urllib.response"),
+    MovedAttribute("addclosehook", "urllib", "urllib.response"),
+    MovedAttribute("addinfo", "urllib", "urllib.response"),
+    MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+    setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+                      "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
+    """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+    MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+                      "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+
+    """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+    __path__ = []  # mark as package
+    parse = _importer._get_module("moves.urllib_parse")
+    error = _importer._get_module("moves.urllib_error")
+    request = _importer._get_module("moves.urllib_request")
+    response = _importer._get_module("moves.urllib_response")
+    robotparser = _importer._get_module("moves.urllib_robotparser")
+
+    def __dir__(self):
+        return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+                      "moves.urllib")
+
+
+def add_move(move):
+    """Add an item to six.moves."""
+    setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+    """Remove item from six.moves."""
+    try:
+        delattr(_MovedItems, name)
+    except AttributeError:
+        try:
+            del moves.__dict__[name]
+        except KeyError:
+            raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+    _meth_func = "__func__"
+    _meth_self = "__self__"
+
+    _func_closure = "__closure__"
+    _func_code = "__code__"
+    _func_defaults = "__defaults__"
+    _func_globals = "__globals__"
+else:
+    _meth_func = "im_func"
+    _meth_self = "im_self"
+
+    _func_closure = "func_closure"
+    _func_code = "func_code"
+    _func_defaults = "func_defaults"
+    _func_globals = "func_globals"
+
+
+try:
+    advance_iterator = next
+except NameError:
+    def advance_iterator(it):
+        return it.next()
+next = advance_iterator
+
+
+try:
+    callable = callable
+except NameError:
+    def callable(obj):
+        return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+    def get_unbound_function(unbound):
+        return unbound
+
+    create_bound_method = types.MethodType
+
+    def create_unbound_method(func, cls):
+        return func
+
+    Iterator = object
+else:
+    def get_unbound_function(unbound):
+        return unbound.im_func
+
+    def create_bound_method(func, obj):
+        return types.MethodType(func, obj, obj.__class__)
+
+    def create_unbound_method(func, cls):
+        return types.MethodType(func, None, cls)
+
+    class Iterator(object):
+
+        def next(self):
+            return type(self).__next__(self)
+
+    callable = callable
+_add_doc(get_unbound_function,
+         """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+    def iterkeys(d, **kw):
+        return iter(d.keys(**kw))
+
+    def itervalues(d, **kw):
+        return iter(d.values(**kw))
+
+    def iteritems(d, **kw):
+        return iter(d.items(**kw))
+
+    def iterlists(d, **kw):
+        return iter(d.lists(**kw))
+
+    viewkeys = operator.methodcaller("keys")
+
+    viewvalues = operator.methodcaller("values")
+
+    viewitems = operator.methodcaller("items")
+else:
+    def iterkeys(d, **kw):
+        return d.iterkeys(**kw)
+
+    def itervalues(d, **kw):
+        return d.itervalues(**kw)
+
+    def iteritems(d, **kw):
+        return d.iteritems(**kw)
+
+    def iterlists(d, **kw):
+        return d.iterlists(**kw)
+
+    viewkeys = operator.methodcaller("viewkeys")
+
+    viewvalues = operator.methodcaller("viewvalues")
+
+    viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+         "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+         "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+    def b(s):
+        return s.encode("latin-1")
+
+    def u(s):
+        return s
+    unichr = chr
+    import struct
+    int2byte = struct.Struct(">B").pack
+    del struct
+    byte2int = operator.itemgetter(0)
+    indexbytes = operator.getitem
+    iterbytes = iter
+    import io
+    StringIO = io.StringIO
+    BytesIO = io.BytesIO
+    _assertCountEqual = "assertCountEqual"
+    if sys.version_info[1] <= 1:
+        _assertRaisesRegex = "assertRaisesRegexp"
+        _assertRegex = "assertRegexpMatches"
+    else:
+        _assertRaisesRegex = "assertRaisesRegex"
+        _assertRegex = "assertRegex"
+else:
+    def b(s):
+        return s
+    # Workaround for standalone backslash
+
+    def u(s):
+        return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+    unichr = unichr
+    int2byte = chr
+
+    def byte2int(bs):
+        return ord(bs[0])
+
+    def indexbytes(buf, i):
+        return ord(buf[i])
+    iterbytes = functools.partial(itertools.imap, ord)
+    import StringIO
+    StringIO = BytesIO = StringIO.StringIO
+    _assertCountEqual = "assertItemsEqual"
+    _assertRaisesRegex = "assertRaisesRegexp"
+    _assertRegex = "assertRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+    return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+    return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+    return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+if PY3:
+    exec_ = getattr(moves.builtins, "exec")
+
+    def reraise(tp, value, tb=None):
+        try:
+            if value is None:
+                value = tp()
+            if value.__traceback__ is not tb:
+                raise value.with_traceback(tb)
+            raise value
+        finally:
+            value = None
+            tb = None
+
+else:
+    def exec_(_code_, _globs_=None, _locs_=None):
+        """Execute code in a namespace."""
+        if _globs_ is None:
+            frame = sys._getframe(1)
+            _globs_ = frame.f_globals
+            if _locs_ is None:
+                _locs_ = frame.f_locals
+            del frame
+        elif _locs_ is None:
+            _locs_ = _globs_
+        exec("""exec _code_ in _globs_, _locs_""")
+
+    exec_("""def reraise(tp, value, tb=None):
+    try:
+        raise tp, value, tb
+    finally:
+        tb = None
+""")
+
+
+if sys.version_info[:2] == (3, 2):
+    exec_("""def raise_from(value, from_value):
+    try:
+        if from_value is None:
+            raise value
+        raise value from from_value
+    finally:
+        value = None
+""")
+elif sys.version_info[:2] > (3, 2):
+    exec_("""def raise_from(value, from_value):
+    try:
+        raise value from from_value
+    finally:
+        value = None
+""")
+else:
+    def raise_from(value, from_value):
+        raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+    def print_(*args, **kwargs):
+        """The new-style print function for Python 2.4 and 2.5."""
+        fp = kwargs.pop("file", sys.stdout)
+        if fp is None:
+            return
+
+        def write(data):
+            if not isinstance(data, basestring):
+                data = str(data)
+            # If the file has an encoding, encode unicode with it.
+            if (isinstance(fp, file) and
+                    isinstance(data, unicode) and
+                    fp.encoding is not None):
+                errors = getattr(fp, "errors", None)
+                if errors is None:
+                    errors = "strict"
+                data = data.encode(fp.encoding, errors)
+            fp.write(data)
+        want_unicode = False
+        sep = kwargs.pop("sep", None)
+        if sep is not None:
+            if isinstance(sep, unicode):
+                want_unicode = True
+            elif not isinstance(sep, str):
+                raise TypeError("sep must be None or a string")
+        end = kwargs.pop("end", None)
+        if end is not None:
+            if isinstance(end, unicode):
+                want_unicode = True
+            elif not isinstance(end, str):
+                raise TypeError("end must be None or a string")
+        if kwargs:
+            raise TypeError("invalid keyword arguments to print()")
+        if not want_unicode:
+            for arg in args:
+                if isinstance(arg, unicode):
+                    want_unicode = True
+                    break
+        if want_unicode:
+            newline = unicode("\n")
+            space = unicode(" ")
+        else:
+            newline = "\n"
+            space = " "
+        if sep is None:
+            sep = space
+        if end is None:
+            end = newline
+        for i, arg in enumerate(args):
+            if i:
+                write(sep)
+            write(arg)
+        write(end)
+if sys.version_info[:2] < (3, 3):
+    _print = print_
+
+    def print_(*args, **kwargs):
+        fp = kwargs.get("file", sys.stdout)
+        flush = kwargs.pop("flush", False)
+        _print(*args, **kwargs)
+        if flush and fp is not None:
+            fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+    def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+              updated=functools.WRAPPER_UPDATES):
+        def wrapper(f):
+            f = functools.wraps(wrapped, assigned, updated)(f)
+            f.__wrapped__ = wrapped
+            return f
+        return wrapper
+else:
+    wraps = functools.wraps
+
+
+def with_metaclass(meta, *bases):
+    """Create a base class with a metaclass."""
+    # This requires a bit of explanation: the basic idea is to make a dummy
+    # metaclass for one level of class instantiation that replaces itself with
+    # the actual metaclass.
+    class metaclass(type):
+
+        def __new__(cls, name, this_bases, d):
+            return meta(name, bases, d)
+
+        @classmethod
+        def __prepare__(cls, name, this_bases):
+            return meta.__prepare__(name, bases)
+    return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+    """Class decorator for creating a class with a metaclass."""
+    def wrapper(cls):
+        orig_vars = cls.__dict__.copy()
+        slots = orig_vars.get('__slots__')
+        if slots is not None:
+            if isinstance(slots, str):
+                slots = [slots]
+            for slots_var in slots:
+                orig_vars.pop(slots_var)
+        orig_vars.pop('__dict__', None)
+        orig_vars.pop('__weakref__', None)
+        return metaclass(cls.__name__, cls.__bases__, orig_vars)
+    return wrapper
+
+
+def python_2_unicode_compatible(klass):
+    """
+    A decorator that defines __unicode__ and __str__ methods under Python 2.
+    Under Python 3 it does nothing.
+
+    To support Python 2 and 3 with a single code base, define a __str__ method
+    returning text and apply this decorator to the class.
+    """
+    if PY2:
+        if '__str__' not in klass.__dict__:
+            raise ValueError("@python_2_unicode_compatible cannot be applied "
+                             "to %s because it doesn't define __str__()." %
+                             klass.__name__)
+        klass.__unicode__ = klass.__str__
+        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+    return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = []  # required for PEP 302 and PEP 451
+__package__ = __name__  # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+    __spec__.submodule_search_locations = []  # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+    for i, importer in enumerate(sys.meta_path):
+        # Here's some real nastiness: Another "instance" of the six module might
+        # be floating around. Therefore, we can't use isinstance() to check for
+        # the six meta path importer, since the other six instance will have
+        # inserted an importer with different class.
+        if (type(importer).__name__ == "_SixMetaPathImporter" and
+                importer.name == __name__):
+            del sys.meta_path[i]
+            break
+    del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/resources/lib/libraries/dateutil/test/__init__.py b/resources/lib/libraries/dateutil/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/resources/lib/libraries/dateutil/test/_common.py b/resources/lib/libraries/dateutil/test/_common.py
new file mode 100644
index 00000000..264dfbda
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/_common.py
@@ -0,0 +1,275 @@
+from __future__ import unicode_literals
+import os
+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, singleton=False, 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)
+        if not singleton:
+            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.'))
+
+
+###
+# 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()
diff --git a/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py b/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py
new file mode 100644
index 00000000..c6a4b82a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py
@@ -0,0 +1,27 @@
+from hypothesis import given, assume
+from hypothesis import strategies as st
+
+from dateutil import tz
+from dateutil.parser import isoparse
+
+import pytest
+
+# Strategies
+TIME_ZONE_STRATEGY = st.sampled_from([None, tz.tzutc()] +
+    [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific',
+                                   'Australia/Sydney', 'Europe/London')])
+ASCII_STRATEGY = st.characters(max_codepoint=127)
+
+
+@pytest.mark.isoparser
+@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY)
+def test_timespec_auto(dt, sep):
+    if dt.tzinfo is not None:
+        # Assume offset has no sub-second components
+        assume(dt.utcoffset().total_seconds() % 60 == 0)
+
+    sep = str(sep)          # Python 2.7 requires bytes
+    dtstr = dt.isoformat(sep=sep)
+    dt_rt = isoparse(dtstr)
+
+    assert dt_rt == dt
diff --git a/resources/lib/libraries/dateutil/test/property/test_parser_prop.py b/resources/lib/libraries/dateutil/test/property/test_parser_prop.py
new file mode 100644
index 00000000..fdfd171e
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/property/test_parser_prop.py
@@ -0,0 +1,22 @@
+from hypothesis.strategies import integers
+from hypothesis import given
+
+import pytest
+
+from dateutil.parser import parserinfo
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=100, max_value=9999))
+def test_convertyear(n):
+    assert n == parserinfo().convertyear(n)
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=-50,
+                max_value=49))
+def test_convertyear_no_specified_century(n):
+    p = parserinfo()
+    new_year = p._year + n
+    result = p.convertyear(new_year % 100, century_specified=False)
+    assert result == new_year
diff --git a/resources/lib/libraries/dateutil/test/test_easter.py b/resources/lib/libraries/dateutil/test/test_easter.py
new file mode 100644
index 00000000..eeb094ee
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_easter.py
@@ -0,0 +1,95 @@
+from dateutil.easter import easter
+from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN
+
+from datetime import date
+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)
diff --git a/resources/lib/libraries/dateutil/test/test_import_star.py b/resources/lib/libraries/dateutil/test/test_import_star.py
new file mode 100644
index 00000000..8e66f38a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_import_star.py
@@ -0,0 +1,33 @@
+"""Test for the "import *" functionality.
+
+As imort * can be only done at module level, it has been added in a separate file
+"""
+import unittest
+
+prev_locals = list(locals())
+from dateutil import *
+new_locals = {name:value for name,value in locals().items()
+              if name not in prev_locals}
+new_locals.pop('prev_locals')
+
+class ImportStarTest(unittest.TestCase):
+    """ Test that `from dateutil import *` adds the modules in __all__ locally"""
+
+    def testImportedModules(self):
+        import dateutil.easter
+        import dateutil.parser
+        import dateutil.relativedelta
+        import dateutil.rrule
+        import dateutil.tz
+        import dateutil.utils
+        import dateutil.zoneinfo
+
+        self.assertEquals(dateutil.easter, new_locals.pop("easter"))
+        self.assertEquals(dateutil.parser, new_locals.pop("parser"))
+        self.assertEquals(dateutil.relativedelta, new_locals.pop("relativedelta"))
+        self.assertEquals(dateutil.rrule, new_locals.pop("rrule"))
+        self.assertEquals(dateutil.tz, new_locals.pop("tz"))
+        self.assertEquals(dateutil.utils, new_locals.pop("utils"))
+        self.assertEquals(dateutil.zoneinfo, new_locals.pop("zoneinfo"))
+
+        self.assertFalse(new_locals)
diff --git a/resources/lib/libraries/dateutil/test/test_imports.py b/resources/lib/libraries/dateutil/test/test_imports.py
new file mode 100644
index 00000000..2a19b62a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_imports.py
@@ -0,0 +1,166 @@
+import sys
+import unittest
+
+class ImportVersionTest(unittest.TestCase):
+    """ Test that dateutil.__version__ can be imported"""
+
+    def testImportVersionStr(self):
+        from dateutil import __version__
+
+    def testImportRoot(self):
+        import dateutil
+
+        self.assertTrue(hasattr(dateutil, '__version__'))
+
+
+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
+        from dateutil.tz import UTC
+        from dateutil.tz import datetime_ambiguous
+        from dateutil.tz import datetime_exists
+        from dateutil.tz import resolve_imaginary
+
+        tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+                  "tzstr", "tzical", "gettz", "datetime_ambiguous",
+                  "datetime_exists", "resolve_imaginary", "UTC"]
+
+        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):
+        from dateutil.tzwin import tzwin
+        from dateutil.tzwin import tzwinlocal
+
+        tzwin_all = [tzwin, tzwinlocal]
+
+        for var in tzwin_all:
+            self.assertIsNot(var, None)
+
+
+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)
diff --git a/resources/lib/libraries/dateutil/test/test_internals.py b/resources/lib/libraries/dateutil/test/test_internals.py
new file mode 100644
index 00000000..a64c5148
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_internals.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for implementation details, not necessarily part of the user-facing
+API.
+
+The motivating case for these tests is #483, where we want to smoke-test
+code that may be difficult to reach through the standard API calls.
+"""
+
+import unittest
+import sys
+
+import pytest
+
+from dateutil.parser._parser import _ymd
+from dateutil import tz
+
+IS_PY32 = sys.version_info[0:2] == (3, 2)
+
+
+class TestYMD(unittest.TestCase):
+
+    # @pytest.mark.smoke
+    def test_could_be_day(self):
+        ymd = _ymd('foo bar 124 baz')
+
+        ymd.append(2, 'M')
+        assert ymd.has_month
+        assert not ymd.has_year
+        assert ymd.could_be_day(4)
+        assert not ymd.could_be_day(-6)
+        assert not ymd.could_be_day(32)
+
+        # Assumes leapyear
+        assert ymd.could_be_day(29)
+
+        ymd.append(1999)
+        assert ymd.has_year
+        assert not ymd.could_be_day(29)
+
+        ymd.append(16, 'D')
+        assert ymd.has_day
+        assert not ymd.could_be_day(1)
+
+        ymd = _ymd('foo bar 124 baz')
+        ymd.append(1999)
+        assert ymd.could_be_day(31)
+
+
+###
+# Test that private interfaces in _parser are deprecated properly
+@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
+def test_parser_private_warns():
+    from dateutil.parser import _timelex, _tzparser
+    from dateutil.parser import _parsetz
+
+    with pytest.warns(DeprecationWarning):
+        _tzparser()
+
+    with pytest.warns(DeprecationWarning):
+        _timelex('2014-03-03')
+
+    with pytest.warns(DeprecationWarning):
+        _parsetz('+05:00')
+
+
+@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
+def test_parser_parser_private_not_warns():
+    from dateutil.parser._parser import _timelex, _tzparser
+    from dateutil.parser._parser import _parsetz
+
+    with pytest.warns(None) as recorder:
+        _tzparser()
+        assert len(recorder) == 0
+
+    with pytest.warns(None) as recorder:
+        _timelex('2014-03-03')
+
+        assert len(recorder) == 0
+
+    with pytest.warns(None) as recorder:
+        _parsetz('+05:00')
+        assert len(recorder) == 0
+
+
+@pytest.mark.tzstr
+def test_tzstr_internal_timedeltas():
+    with pytest.warns(tz.DeprecatedTzFormatWarning):
+        tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200")
+
+    with pytest.warns(tz.DeprecatedTzFormatWarning):
+        tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200")
+
+    assert tz1._start_delta != tz2._start_delta
+    assert tz1._end_delta != tz2._end_delta
diff --git a/resources/lib/libraries/dateutil/test/test_isoparser.py b/resources/lib/libraries/dateutil/test/test_isoparser.py
new file mode 100644
index 00000000..28c1bf76
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_isoparser.py
@@ -0,0 +1,482 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime, timedelta, date, time
+import itertools as it
+
+from dateutil.tz import tz
+from dateutil.parser import isoparser, isoparse
+
+import pytest
+import six
+
+UTC = tz.tzutc()
+
+def _generate_tzoffsets(limited):
+    def _mkoffset(hmtuple, fmt):
+        h, m = hmtuple
+        m_td = (-1 if h < 0 else 1) * m
+
+        tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
+        return tzo, fmt.format(h, m)
+
+    out = []
+    if not limited:
+        # The subset that's just hours
+        hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
+        out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])
+
+        # Ones that have hours and minutes
+        hm_out = [] + hm_out_h
+        hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
+    else:
+        hm_out = [(-5, -0)]
+
+    fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
+    out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]
+
+    # Also add in UTC and naive
+    out.append((tz.tzutc(), 'Z'))
+    out.append((None, ''))
+
+    return out
+
+FULL_TZOFFSETS = _generate_tzoffsets(False)
+FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
+TZOFFSETS = _generate_tzoffsets(True)
+
+DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
+@pytest.mark.parametrize('dt', tuple(DATES))
+def test_year_only(dt):
+    dtstr = dt.strftime('%Y')
+
+    assert isoparse(dtstr) == dt
+
+DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
+@pytest.mark.parametrize('dt', tuple(DATES))
+def test_year_month(dt):
+    fmt   = '%Y-%m'
+    dtstr = dt.strftime(fmt)
+
+    assert isoparse(dtstr) == dt
+
+DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
+YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
+@pytest.mark.parametrize('dt', tuple(DATES))
+@pytest.mark.parametrize('fmt', YMD_FMTS)
+def test_year_month_day(dt, fmt):
+    dtstr = dt.strftime(fmt)
+
+    assert isoparse(dtstr) == dt
+
+def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
+                            microsecond_precision=None):
+    tzi, offset_str = tzoffset
+    fmt = date_fmt + 'T' + time_fmt
+    dt = dt.replace(tzinfo=tzi)
+    dtstr = dt.strftime(fmt)
+
+    if microsecond_precision is not None:
+        if not fmt.endswith('%f'):
+            raise ValueError('Time format has no microseconds!')
+
+        if microsecond_precision != 6:
+            dtstr = dtstr[:-(6 - microsecond_precision)]
+        elif microsecond_precision > 6:
+            raise ValueError('Precision must be 1-6')
+
+    dtstr += offset_str
+
+    assert isoparse(dtstr) == dt
+
+DATETIMES = [datetime(1998, 4, 16, 12),
+             datetime(2019, 11, 18, 23),
+             datetime(2014, 12, 16, 4)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_h(dt, date_fmt, tzoffset):
+    _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)
+
+DATETIMES = [datetime(2012, 1, 6, 9, 37)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
+    _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
+             datetime(2003, 8, 8, 14, 9, 14),
+             datetime(2003, 4, 7, 6, 14, 59)]
+HMS_FMTS = ('%H%M%S', '%H:%M:%S')
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', HMS_FMTS)
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
+    _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
+@pytest.mark.parametrize('dt', tuple(DATETIMES))
+@pytest.mark.parametrize('date_fmt', YMD_FMTS)
+@pytest.mark.parametrize('time_fmt', (x + '.%f' for x in HMS_FMTS))
+@pytest.mark.parametrize('tzoffset', TZOFFSETS)
+@pytest.mark.parametrize('precision', list(range(3, 7)))
+def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
+    # Truncate the microseconds to the desired precision for the representation
+    dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))
+
+    _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
+
+@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
+def test_full_tzoffsets(tzoffset):
+    dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
+    date_fmt = '%Y-%m-%d'
+    time_fmt = '%H:%M:%S.%f'
+
+    _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+@pytest.mark.parametrize('dt_str', [
+    '2014-04-11T00',
+    '2014-04-11T24',
+    '2014-04-11T00:00',
+    '2014-04-11T24:00',
+    '2014-04-11T00:00:00',
+    '2014-04-11T24:00:00',
+    '2014-04-11T00:00:00.000',
+    '2014-04-11T24:00:00.000',
+    '2014-04-11T00:00:00.000000',
+    '2014-04-11T24:00:00.000000']
+)
+def test_datetime_midnight(dt_str):
+    assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
+
+@pytest.mark.parametrize('datestr', [
+    '2014-01-01',
+    '20140101',
+])
+@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
+def test_isoparse_sep_none(datestr, sep):
+    isostr = datestr + sep + '14:33:09'
+    assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)
+
+##
+# Uncommon date formats
+TIME_ARGS = ('time_args',
+    ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
+        for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
+                                  TZOFFSETS)))
+
+@pytest.mark.parametrize('isocal,dt_expected',[
+    ((2017, 10), datetime(2017, 3, 6)),
+    ((2020, 1), datetime(2019, 12, 30)),    # ISO year != Cal year
+    ((2004, 53), datetime(2004, 12, 27)),   # Only half the week is in 2014
+])
+def test_isoweek(isocal, dt_expected):
+    # TODO: Figure out how to parametrize this on formats, too
+    for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
+        dtstr = fmt.format(*isocal)
+        assert isoparse(dtstr) == dt_expected
+
+@pytest.mark.parametrize('isocal,dt_expected',[
+    ((2016, 13, 7), datetime(2016, 4, 3)),
+    ((2004, 53, 7), datetime(2005, 1, 2)),      # ISO year != Cal year
+    ((2009, 1, 2), datetime(2008, 12, 30)),     # ISO year < Cal year
+    ((2009, 53, 6), datetime(2010, 1, 2))       # ISO year > Cal year
+])
+def test_isoweek_day(isocal, dt_expected):
+    # TODO: Figure out how to parametrize this on formats, too
+    for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
+        dtstr = fmt.format(*isocal)
+        assert isoparse(dtstr) == dt_expected
+
+@pytest.mark.parametrize('isoord,dt_expected', [
+    ((2004, 1), datetime(2004, 1, 1)),
+    ((2016, 60), datetime(2016, 2, 29)),
+    ((2017, 60), datetime(2017, 3, 1)),
+    ((2016, 366), datetime(2016, 12, 31)),
+    ((2017, 365), datetime(2017, 12, 31))
+])
+def test_iso_ordinal(isoord, dt_expected):
+    for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
+        dtstr = fmt.format(*isoord)
+
+        assert isoparse(dtstr) == dt_expected
+
+
+###
+# Acceptance of bytes
+@pytest.mark.parametrize('isostr,dt', [
+    (b'2014', datetime(2014, 1, 1)),
+    (b'20140204', datetime(2014, 2, 4)),
+    (b'2014-02-04', datetime(2014, 2, 4)),
+    (b'2014-02-04T12', datetime(2014, 2, 4, 12)),
+    (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
+    (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
+    (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
+    (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
+    (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
+                                           tz.tzutc())),
+    (b'2014-02-04T12:30:15.224+05:00',
+        datetime(2014, 2, 4, 12, 30, 15, 224000,
+                 tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
+def test_bytes(isostr, dt):
+    assert isoparse(isostr) == dt
+
+
+###
+# Invalid ISO strings
+@pytest.mark.parametrize('isostr,exception', [
+    ('201', ValueError),                        # ISO string too short
+    ('2012-0425', ValueError),                  # Inconsistent date separators
+    ('201204-25', ValueError),                  # Inconsistent date separators
+    ('20120425T0120:00', ValueError),           # Inconsistent time separators
+    ('20120425T012500-334', ValueError),        # Wrong microsecond separator
+    ('2001-1', ValueError),                     # YYYY-M not valid
+    ('2012-04-9', ValueError),                  # YYYY-MM-D not valid
+    ('201204', ValueError),                     # YYYYMM not valid
+    ('20120411T03:30+', ValueError),            # Time zone too short
+    ('20120411T03:30+1234567', ValueError),     # Time zone too long
+    ('20120411T03:30-25:40', ValueError),       # Time zone invalid
+    ('2012-1a', ValueError),                    # Invalid month
+    ('20120411T03:30+00:60', ValueError),       # Time zone invalid minutes
+    ('20120411T03:30+00:61', ValueError),       # Time zone invalid minutes
+    ('20120411T033030.123456012:00',            # No sign in time zone
+        ValueError),
+    ('2012-W00', ValueError),                   # Invalid ISO week
+    ('2012-W55', ValueError),                   # Invalid ISO week
+    ('2012-W01-0', ValueError),                 # Invalid ISO week day
+    ('2012-W01-8', ValueError),                 # Invalid ISO week day
+    ('2013-000', ValueError),                   # Invalid ordinal day
+    ('2013-366', ValueError),                   # Invalid ordinal day
+    ('2013366', ValueError),                    # Invalid ordinal day
+    ('2014-03-12Т12:30:14', ValueError),        # Cyrillic T
+    ('2014-04-21T24:00:01', ValueError),        # Invalid use of 24 for midnight
+    ('2014_W01-1', ValueError),                 # Invalid separator
+    ('2014W01-1', ValueError),                  # Inconsistent use of dashes
+    ('2014-W011', ValueError),                  # Inconsistent use of dashes
+
+])
+def test_iso_raises(isostr, exception):
+    with pytest.raises(exception):
+        isoparse(isostr)
+
+
+@pytest.mark.parametrize('sep_act,valid_sep', [
+    ('C', 'T'),
+    ('T', 'C')
+])
+def test_iso_raises_sep(sep_act, valid_sep):
+    isostr = '2012-04-25' + sep_act + '01:25:00'
+
+
+@pytest.mark.xfail()
+@pytest.mark.parametrize('isostr,exception', [
+    ('20120425T01:2000', ValueError),           # Inconsistent time separators
+])
+def test_iso_raises_failing(isostr, exception):
+    # These are test cases where the current implementation is too lenient
+    # and need to be fixed
+    with pytest.raises(exception):
+        isoparse(isostr)
+
+
+###
+# Test ISOParser constructor
+@pytest.mark.parametrize('sep', ['  ', '9', '🍛'])
+def test_isoparser_invalid_sep(sep):
+    with pytest.raises(ValueError):
+        isoparser(sep=sep)
+
+
+# This only fails on Python 3
+@pytest.mark.xfail(six.PY3, reason="Fails on Python 3 only")
+def test_isoparser_byte_sep():
+    dt = datetime(2017, 12, 6, 12, 30, 45)
+    dt_str = dt.isoformat(sep=str('T'))
+
+    dt_rt = isoparser(sep=b'T').isoparse(dt_str)
+
+    assert dt == dt_rt
+
+
+###
+# Test parse_tzstr
+@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
+def test_parse_tzstr(tzoffset):
+    dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
+    date_fmt = '%Y-%m-%d'
+    time_fmt = '%H:%M:%S.%f'
+
+    _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
+
+
+@pytest.mark.parametrize('tzstr', [
+    '-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
+])
+@pytest.mark.parametrize('zero_as_utc', [True, False])
+def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
+    tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
+    assert tzi == tz.tzutc()
+    assert (type(tzi) == tz.tzutc) == zero_as_utc
+
+
+@pytest.mark.parametrize('tzstr,exception', [
+    ('00:00', ValueError),     # No sign
+    ('05:00', ValueError),     # No sign
+    ('_00:00', ValueError),    # Invalid sign
+    ('+25:00', ValueError),    # Offset too large
+    ('00:0000', ValueError),   # String too long
+])
+def test_parse_tzstr_fails(tzstr, exception):
+    with pytest.raises(exception):
+        isoparser().parse_tzstr(tzstr)
+
+###
+# Test parse_isodate
+def __make_date_examples():
+    dates_no_day = [
+        date(1999, 12, 1),
+        date(2016, 2, 1)
+    ]
+
+    if six.PY3:
+        # strftime does not support dates before 1900 in Python 2
+        dates_no_day.append(date(1000, 11, 1))
+
+    # Only one supported format for dates with no day
+    o = zip(dates_no_day, it.repeat('%Y-%m'))
+
+    dates_w_day = [
+        date(1969, 12, 31),
+        date(1900, 1, 1),
+        date(2016, 2, 29),
+        date(2017, 11, 14)
+    ]
+
+    dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
+    o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))
+
+    return list(o)
+
+
+@pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
+@pytest.mark.parametrize('as_bytes', [True, False])
+def test_parse_isodate(d, dt_fmt, as_bytes):
+    d_str = d.strftime(dt_fmt)
+    if isinstance(d_str, six.text_type) and as_bytes:
+        d_str = d_str.encode('ascii')
+    elif isinstance(d_str, six.binary_type) and not as_bytes:
+        d_str = d_str.decode('ascii')
+
+    iparser = isoparser()
+    assert iparser.parse_isodate(d_str) == d
+
+
+@pytest.mark.parametrize('isostr,exception', [
+    ('243', ValueError),                        # ISO string too short
+    ('2014-0423', ValueError),                  # Inconsistent date separators
+    ('201404-23', ValueError),                  # Inconsistent date separators
+    ('2014日03月14', ValueError),                # Not ASCII
+    ('2013-02-29', ValueError),                 # Not a leap year
+    ('2014/12/03', ValueError),                 # Wrong separators
+    ('2014-04-19T', ValueError),                # Unknown components
+])
+def test_isodate_raises(isostr, exception):
+    with pytest.raises(exception):
+        isoparser().parse_isodate(isostr)
+
+
+###
+# Test parse_isotime
+def __make_time_examples():
+    outputs = []
+
+    # HH
+    time_h = [time(0), time(8), time(22)]
+    time_h_fmts = ['%H']
+
+    outputs.append(it.product(time_h, time_h_fmts))
+
+    # HHMM / HH:MM
+    time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
+    time_hm_fmts = ['%H%M', '%H:%M']
+
+    outputs.append(it.product(time_hm, time_hm_fmts))
+
+    # HHMMSS / HH:MM:SS
+    time_hms = [time(0, 0, 0), time(0, 15, 30),
+                time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]
+
+    time_hms_fmts = ['%H%M%S', '%H:%M:%S']
+
+    outputs.append(it.product(time_hms, time_hms_fmts))
+
+    # HHMMSS.ffffff / HH:MM:SS.ffffff
+    time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
+                 time(14, 21, 59, 948730),
+                 time(23, 59, 59, 999999)]
+
+    time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']
+
+    outputs.append(it.product(time_hmsu, time_hmsu_fmts))
+
+    outputs = list(map(list, outputs))
+
+    # Time zones
+    ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
+    o = it.product(ex_naive, TZOFFSETS)    # ((time, fmt), (tzinfo, offsetstr))
+    o = ((t.replace(tzinfo=tzi), fmt + off_str)
+         for (t, fmt), (tzi, off_str) in o)
+
+    outputs.append(o)
+
+    return list(it.chain.from_iterable(outputs))
+
+
+@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
+@pytest.mark.parametrize('as_bytes', [True, False])
+def test_isotime(time_val, time_fmt, as_bytes):
+    tstr = time_val.strftime(time_fmt)
+    if isinstance(time_val, six.text_type) and as_bytes:
+        tstr = tstr.encode('ascii')
+    elif isinstance(time_val, six.binary_type) and not as_bytes:
+        tstr = tstr.decode('ascii')
+
+    iparser = isoparser()
+
+    assert iparser.parse_isotime(tstr) == time_val
+
+@pytest.mark.parametrize('isostr,exception', [
+    ('3', ValueError),                          # ISO string too short
+    ('14時30分15秒', ValueError),                # Not ASCII
+    ('14_30_15', ValueError),                   # Invalid separators
+    ('1430:15', ValueError),                    # Inconsistent separator use
+    ('14:30:15.3684000309', ValueError),        # Too much us precision
+    ('25', ValueError),                         # Invalid hours
+    ('25:15', ValueError),                      # Invalid hours
+    ('14:60', ValueError),                      # Invalid minutes
+    ('14:59:61', ValueError),                   # Invalid seconds
+    ('14:30:15.3446830500', ValueError),        # No sign in time zone
+    ('14:30:15+', ValueError),                  # Time zone too short
+    ('14:30:15+1234567', ValueError),           # Time zone invalid
+    ('14:59:59+25:00', ValueError),             # Invalid tz hours
+    ('14:59:59+12:62', ValueError),             # Invalid tz minutes
+    ('14:59:30_344583', ValueError),            # Invalid microsecond separator
+])
+def test_isotime_raises(isostr, exception):
+    iparser = isoparser()
+    with pytest.raises(exception):
+        iparser.parse_isotime(isostr)
+
+
+@pytest.mark.xfail()
+@pytest.mark.parametrize('isostr,exception', [
+    ('14:3015', ValueError),                    # Inconsistent separator use
+    ('201202', ValueError)                      # Invalid ISO format
+])
+def test_isotime_raises_xfail(isostr, exception):
+    iparser = isoparser()
+    with pytest.raises(exception):
+        iparser.parse_isotime(isostr)
diff --git a/resources/lib/libraries/dateutil/test/test_parser.py b/resources/lib/libraries/dateutil/test/test_parser.py
new file mode 100644
index 00000000..f8c20720
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_parser.py
@@ -0,0 +1,1114 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import itertools
+from datetime import datetime, timedelta
+import unittest
+import sys
+
+from dateutil import tz
+from dateutil.tz import tzoffset
+from dateutil.parser import parse, parserinfo
+from dateutil.parser import UnknownTimezoneWarning
+
+from ._common import TZEnvContext
+
+from six import assertRaisesRegex, PY3
+from six.moves import StringIO
+
+import pytest
+
+# Platform info
+IS_WIN = sys.platform.startswith('win')
+
+try:
+    datetime.now().strftime('%-d')
+    PLATFORM_HAS_DASH_D = True
+except ValueError:
+    PLATFORM_HAS_DASH_D = False
+
+
+class TestFormat(unittest.TestCase):
+
+    def test_ybd(self):
+        # If we have a 4-digit year, a non-numeric month (abbreviated or not),
+        # and a day (1 or 2 digits), then there is no ambiguity as to which
+        # token is a year/month/day.  This holds regardless of what order the
+        # terms are in and for each of the separators below.
+
+        seps = ['-', ' ', '/', '.']
+
+        year_tokens = ['%Y']
+        month_tokens = ['%b', '%B']
+        day_tokens = ['%d']
+        if PLATFORM_HAS_DASH_D:
+            day_tokens.append('%-d')
+
+        prods = itertools.product(year_tokens, month_tokens, day_tokens)
+        perms = [y for x in prods for y in itertools.permutations(x)]
+        unambig_fmts = [sep.join(perm) for sep in seps for perm in perms]
+
+        actual = datetime(2003, 9, 25)
+
+        for fmt in unambig_fmts:
+            dstr = actual.strftime(fmt)
+            res = parse(dstr)
+            self.assertEqual(res, actual)
+
+
+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
+        self.uni_str = '2014-05-01 08:00:00'
+        self.str_str = self.uni_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 testParseBytes(self):
+        self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19))
+
+    def testParseBytearray(self):
+        # GH #417
+        self.assertEqual(parse(bytearray(b'2014 January 19')),
+                         datetime(2014, 1, 19))
+
+    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 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 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 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 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 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 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 testHourWithLetterStrip5(self):
+        self.assertEqual(parse("10 h 36.5", default=self.default),
+                         datetime(2003, 9, 25, 10, 36, 30))
+
+    def testMinuteWithLettersSpaces1(self):
+        self.assertEqual(parse("36 m 5", default=self.default),
+                         datetime(2003, 9, 25, 0, 36, 5))
+
+    def testMinuteWithLettersSpaces2(self):
+        self.assertEqual(parse("36 m 5 s", default=self.default),
+                         datetime(2003, 9, 25, 0, 36, 5))
+
+    def testMinuteWithLettersSpaces3(self):
+        self.assertEqual(parse("36 m 05", default=self.default),
+                         datetime(2003, 9, 25, 0, 36, 5))
+
+    def testMinuteWithLettersSpaces4(self):
+        self.assertEqual(parse("36 m 05 s", default=self.default),
+                         datetime(2003, 9, 25, 0, 36, 5))
+
+    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):
+        s1 = "Today is 25 of September of 2003, exactly " \
+            "at 10:49:41 with timezone -03:00."
+        self.assertEqual(parse(s1, fuzzy_with_tokens=True),
+                         (datetime(2003, 9, 25, 10, 49, 41,
+                                   tzinfo=self.brsttz),
+                         ('Today is ', 'of ', ', exactly at ',
+                          ' with timezone ', '.')))
+
+        s2 = "http://biz.yahoo.com/ipo/p/600221.html"
+        self.assertEqual(parse(s2, fuzzy_with_tokens=True),
+                         (datetime(2060, 2, 21, 0, 0, 0),
+                         ('http://biz.yahoo.com/ipo/p/', '.html')))
+
+    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?"
+        with pytest.warns(UnknownTimezoneWarning):
+            res = parse(s1, fuzzy=True)
+        self.assertEqual(res, 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):
+        with pytest.warns(UnknownTimezoneWarning):
+            res = parse("5:50 A.M. on June 13, 1990")
+
+        self.assertEqual(res, 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 testTzinfoDictionaryCouldReturnNone(self):
+        self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos={"BRST": None}),
+                        datetime(2017, 2, 3, 12, 40))
+
+    def testTzinfosCallableCouldReturnNone(self):
+        self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos=lambda *args: None),
+                                    datetime(2017, 2, 3, 12, 40))
+
+    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 testCustomParserShortDaynames(self):
+        # Horacio Hoyos discovered that day names shorter than 3 characters,
+        # for example two letter German day name abbreviations, don't work:
+        # https://github.com/dateutil/dateutil/issues/343
+        from dateutil.parser import parserinfo, parser
+
+        class GermanParserInfo(parserinfo):
+            WEEKDAYS = [("Mo", "Montag"),
+                        ("Di", "Dienstag"),
+                        ("Mi", "Mittwoch"),
+                        ("Do", "Donnerstag"),
+                        ("Fr", "Freitag"),
+                        ("Sa", "Samstag"),
+                        ("So", "Sonntag")]
+
+        myparser = parser(GermanParserInfo())
+        dt = myparser.parse("Sa 21. Jan 2017")
+        self.assertEqual(dt, datetime(2017, 1, 21))
+
+    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))
+
+    def test_mstridx(self):
+        # See GH408
+        dtstr = '2015-15-May'
+        self.assertEqual(parse(dtstr),
+                         datetime(2015, 5, 15))
+
+    def test_idx_check(self):
+        dtstr = '2017-07-17 06:15:'
+        # Pre-PR, the trailing colon will cause an IndexError at 824-825
+        # when checking `i < len_l` and then accessing `l[i+1]`
+        res = parse(dtstr, fuzzy=True)
+        self.assertEqual(res, datetime(2017, 7, 17, 6, 15))
+
+    def test_dBY(self):
+        # See GH360
+        dtstr = '13NOV2017'
+        res = parse(dtstr)
+        self.assertEqual(res, datetime(2017, 11, 13))
+
+    def test_hmBY(self):
+        # See GH#483
+        dtstr = '02:17NOV2017'
+        res = parse(dtstr, default=self.default)
+        self.assertEqual(res, datetime(2017, 11, self.default.day, 2, 17))
+
+    def test_validate_hour(self):
+        # See GH353
+        invalid = "201A-01-01T23:58:39.239769+03:00"
+        with self.assertRaises(ValueError):
+            parse(invalid)
+
+    def test_era_trailing_year(self):
+        dstr = 'AD2001'
+        res = parse(dstr)
+        assert res.year == 2001, res
+
+    def test_pre_12_year_same_month(self):
+        # See GH PR #293
+        dtstr = '0003-03-04'
+        assert parse(dtstr) == datetime(3, 3, 4)
+
+
+class TestParseUnimplementedCases(object):
+    @pytest.mark.xfail
+    def test_somewhat_ambiguous_string(self):
+        # Ref: github issue #487
+        # The parser is choosing the wrong part for hour
+        # causing datetime to raise an exception.
+        dtstr = '1237 PM BRST Mon Oct 30 2017'
+        res = parse(dtstr, tzinfo=self.tzinfos)
+        assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos)
+
+    @pytest.mark.xfail
+    def test_YmdH_M_S(self):
+        # found in nasdaq's ftp data
+        dstr = '1991041310:19:24'
+        expected = datetime(1991, 4, 13, 10, 19, 24)
+        res = parse(dstr)
+        assert res == expected, (res, expected)
+
+    @pytest.mark.xfail
+    def test_first_century(self):
+        dstr = '0031 Nov 03'
+        expected = datetime(31, 11, 3)
+        res = parse(dstr)
+        assert res == expected, res
+
+    @pytest.mark.xfail
+    def test_era_trailing_year_with_dots(self):
+        dstr = 'A.D.2001'
+        res = parse(dstr)
+        assert res.year == 2001, res
+
+    @pytest.mark.xfail
+    def test_ad_nospace(self):
+        expected = datetime(6, 5, 19)
+        for dstr in [' 6AD May 19', ' 06AD May 19',
+                     ' 006AD May 19', ' 0006AD May 19']:
+            res = parse(dstr)
+            assert res == expected, (dstr, res)
+
+    @pytest.mark.xfail
+    def test_four_letter_day(self):
+        dstr = 'Frid Dec 30, 2016'
+        expected = datetime(2016, 12, 30)
+        res = parse(dstr)
+        assert res == expected
+
+    @pytest.mark.xfail
+    def test_non_date_number(self):
+        dstr = '1,700'
+        with pytest.raises(ValueError):
+            parse(dstr)
+
+    @pytest.mark.xfail
+    def test_on_era(self):
+        # This could be classified as an "eras" test, but the relevant part
+        # about this is the ` on `
+        dstr = '2:15 PM on January 2nd 1973 A.D.'
+        expected = datetime(1973, 1, 2, 14, 15)
+        res = parse(dstr)
+        assert res == expected
+
+    @pytest.mark.xfail
+    def test_extraneous_year(self):
+        # This was found in the wild at insidertrading.org
+        dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
+        res = parse(dstr, fuzzy_with_tokens=True)
+        expected = datetime(2012, 11, 7)
+        assert res == expected
+
+    @pytest.mark.xfail
+    def test_extraneous_year_tokens(self):
+        # This was found in the wild at insidertrading.org
+        # Unlike in the case above, identifying the first "2012" as the year
+        # would not be a problem, but infering that the latter 2012 is hhmm
+        # is a problem.
+        dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
+        expected = datetime(2012, 11, 7)
+        (res, tokens) = parse(dstr, fuzzy_with_tokens=True)
+        assert res == expected
+        assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",)
+
+    @pytest.mark.xfail
+    def test_extraneous_year2(self):
+        # This was found in the wild at insidertrading.org
+        dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust "
+                "u/d/t November 2, 1998 f/b/o Jennifer L Berylson")
+        res = parse(dstr, fuzzy_with_tokens=True)
+        expected = datetime(1998, 11, 2)
+        assert res == expected
+
+    @pytest.mark.xfail
+    def test_extraneous_year3(self):
+        # This was found in the wild at insidertrading.org
+        dstr = "SMITH R &  WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994"
+        res = parse(dstr, fuzzy_with_tokens=True)
+        expected = datetime(1994, 12, 1)
+        assert res == expected
+
+    @pytest.mark.xfail
+    def test_unambiguous_YYYYMM(self):
+        # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed
+        # as instance of YYMMDD and parser could fallback to YYYYMM format.
+        dstr = "201712"
+        res = parse(dstr)
+        expected = datetime(2017, 12, 1)
+        assert res == expected
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_parse_unambiguous_nonexistent_local():
+    # When dates are specified "EST" even when they should be "EDT" in the
+    # local time zone, we should still assign the local time zone
+    with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+        dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal())
+        dt = parse('2011-08-01T12:30 EST')
+
+        assert dt.tzname() == 'EDT'
+        assert dt == dt_exp
+
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_tzlocal_in_gmt():
+    # GH #318
+    with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'):
+        # This is an imaginary datetime in tz.tzlocal() but should still
+        # parse using the GMT-as-alias-for-UTC rule
+        dt = parse('2004-05-01T12:00 GMT')
+        dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.tzutc())
+
+        assert dt == dt_exp
+
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_tzlocal_parse_fold():
+    # One manifestion of GH #318
+    with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+        dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal())
+        dt_exp = tz.enfold(dt_exp, fold=1)
+        dt = parse('2011-11-06T01:30 EST')
+
+        # Because this is ambiguous, kuntil `tz.tzlocal() is tz.tzlocal()`
+        # we'll just check the attributes we care about rather than
+        # dt == dt_exp
+        assert dt.tzname() == dt_exp.tzname()
+        assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None)
+        assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+        assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc())
+
+
+def test_parse_tzinfos_fold():
+    NYC = tz.gettz('America/New_York')
+    tzinfos = {'EST': NYC, 'EDT': NYC}
+
+    dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1)
+    dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos)
+
+    assert dt == dt_exp
+    assert dt.tzinfo is dt_exp.tzinfo
+    assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+    assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc())
+
+
+@pytest.mark.parametrize('dtstr,dt', [
+    ('5.6h', datetime(2003, 9, 25, 5, 36)),
+    ('5.6m', datetime(2003, 9, 25, 0, 5, 36)),
+    # '5.6s' never had a rounding problem, test added for completeness
+    ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000))
+])
+def test_rounding_floatlike_strings(dtstr, dt):
+    assert parse(dtstr, default=datetime(2003, 9, 25)) == dt
+
+
+@pytest.mark.parametrize('value', ['1: test', 'Nan'])
+def test_decimal_error(value):
+    # GH 632, GH 662 - decimal.Decimal raises some non-ValueError exception when
+    # constructed with an invalid value
+    with pytest.raises(ValueError):
+        parse(value)
+
+
+def test_BYd_corner_case():
+    # GH#687
+    res = parse('December.0031.30')
+    assert res == datetime(31, 12, 30)
diff --git a/resources/lib/libraries/dateutil/test/test_relativedelta.py b/resources/lib/libraries/dateutil/test/test_relativedelta.py
new file mode 100644
index 00000000..70cb543a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_relativedelta.py
@@ -0,0 +1,678 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import WarningTestMixin, NotAValue
+
+import calendar
+from datetime import datetime, date, timedelta
+import unittest
+
+from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU
+
+
+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 testAbsoluteAddition(self):
+        self.assertEqual(relativedelta() + relativedelta(day=0, hour=0),
+                         relativedelta(day=0, hour=0))
+        self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(),
+                         relativedelta(day=0, hour=0))
+
+    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 testAdditionFloatValue(self):
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)),
+                         datetime(2000, 1, 2))
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)),
+                         datetime(2000, 2, 1))
+        self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)),
+                         datetime(2001, 1, 1))
+
+    def testAdditionFloatFractionals(self):
+        self.assertEqual(datetime(2000, 1, 1, 0) +
+                         relativedelta(days=float(0.5)),
+                         datetime(2000, 1, 1, 12))
+        self.assertEqual(datetime(2000, 1, 1, 0, 0) +
+                         relativedelta(hours=float(0.5)),
+                         datetime(2000, 1, 1, 0, 30))
+        self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) +
+                         relativedelta(minutes=float(0.5)),
+                         datetime(2000, 1, 1, 0, 0, 30))
+        self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
+                         relativedelta(seconds=float(0.5)),
+                         datetime(2000, 1, 1, 0, 0, 0, 500000))
+        self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
+                         relativedelta(microseconds=float(500000.25)),
+                         datetime(2000, 1, 1, 0, 0, 0, 500000))
+
+    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 testAbsoluteValueNegative(self):
+        rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3,
+                                minutes=-5, seconds=-2, microseconds=-12)
+        rd_expected = relativedelta(years=1, months=5, days=2, hours=3,
+                                    minutes=5, seconds=2, microseconds=12)
+        self.assertEqual(abs(rd_base), rd_expected)
+
+    def testAbsoluteValuePositive(self):
+        rd_base = relativedelta(years=1, months=5, days=2, hours=3,
+                                minutes=5, seconds=2, microseconds=12)
+        rd_expected = rd_base
+
+        self.assertEqual(abs(rd_base), rd_expected)
+
+    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 testRelativeDeltaNormalizeFractionalDays2(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 testRelativeDeltaFractionalPositiveOverflow2(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)
+
+    def testHashable(self):
+        try:
+            {relativedelta(minute=1): 'test'}
+        except:
+            self.fail("relativedelta() failed to hash!")
+
+
+class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase):
+    """Test the weeks property getter"""
+
+    def test_one_day(self):
+        rd = relativedelta(days=1)
+        self.assertEqual(rd.days, 1)
+        self.assertEqual(rd.weeks, 0)
+
+    def test_minus_one_day(self):
+        rd = relativedelta(days=-1)
+        self.assertEqual(rd.days, -1)
+        self.assertEqual(rd.weeks, 0)
+
+    def test_height_days(self):
+        rd = relativedelta(days=8)
+        self.assertEqual(rd.days, 8)
+        self.assertEqual(rd.weeks, 1)
+
+    def test_minus_height_days(self):
+        rd = relativedelta(days=-8)
+        self.assertEqual(rd.days, -8)
+        self.assertEqual(rd.weeks, -1)
+
+
+class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase):
+    """Test the weeks setter which makes a "smart" update of the days attribute"""
+
+    def test_one_day_set_one_week(self):
+        rd = relativedelta(days=1)
+        rd.weeks = 1  # add 7 days
+        self.assertEqual(rd.days, 8)
+        self.assertEqual(rd.weeks, 1)
+
+    def test_minus_one_day_set_one_week(self):
+        rd = relativedelta(days=-1)
+        rd.weeks = 1  # add 7 days
+        self.assertEqual(rd.days, 6)
+        self.assertEqual(rd.weeks, 0)
+
+    def test_height_days_set_minus_one_week(self):
+        rd = relativedelta(days=8)
+        rd.weeks = -1  # change from 1 week, 1 day to -1 week, 1 day
+        self.assertEqual(rd.days, -6)
+        self.assertEqual(rd.weeks, 0)
+
+    def test_minus_height_days_set_minus_one_week(self):
+        rd = relativedelta(days=-8)
+        rd.weeks = -1  # does not change anything
+        self.assertEqual(rd.days, -8)
+        self.assertEqual(rd.weeks, -1)
+
+
+# vim:ts=4:sw=4:et
diff --git a/resources/lib/libraries/dateutil/test/test_rrule.py b/resources/lib/libraries/dateutil/test/test_rrule.py
new file mode 100644
index 00000000..cd08ce29
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_rrule.py
@@ -0,0 +1,4842 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import WarningTestMixin
+
+from datetime import datetime, date
+import unittest
+from six import PY3
+
+from dateutil import tz
+from dateutil.rrule import (
+    rrule, rruleset, rrulestr,
+    YEARLY, MONTHLY, WEEKLY, DAILY,
+    HOURLY, MINUTELY, SECONDLY,
+    MO, TU, WE, TH, FR, SA, SU
+)
+
+from freezegun import freeze_time
+
+import pytest
+
+
+@pytest.mark.rrule
+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 testStrAppendRRULEToken(self):
+        # `_rrulestr_reverse_test` does not check if the "RRULE:" prefix
+        # property is appended properly, so give it a dedicated test
+        self.assertEqual(str(rrule(YEARLY,
+                             count=5,
+                             dtstart=datetime(1997, 9, 2, 9, 0))),
+                         "DTSTART:19970902T090000\n"
+                         "RRULE:FREQ=YEARLY;COUNT=5")
+
+        rr_str = (
+          'DTSTART:19970105T083000\nRRULE:FREQ=YEARLY;INTERVAL=2'
+        )
+        self.assertEqual(str(rrulestr(rr_str)), rr_str)
+
+    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),
+                          datetime(1997, 12, 31, 0, 0, 1),
+                          datetime(1997, 12, 31, 0, 0, 2),
+                          datetime(1997, 12, 31, 0, 0, 3)])
+
+    def testSecondlyByMonthAndYearDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0, 0),
+                          datetime(1998, 4, 10, 0, 0, 1),
+                          datetime(1998, 4, 10, 0, 0, 2),
+                          datetime(1998, 4, 10, 0, 0, 3)])
+
+    def testSecondlyByMonthAndYearDayNeg(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 10, 0, 0, 0),
+                          datetime(1998, 4, 10, 0, 0, 1),
+                          datetime(1998, 4, 10, 0, 0, 2),
+                          datetime(1998, 4, 10, 0, 0, 3)])
+
+    def testSecondlyByWeekNo(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 5, 11, 0, 0, 0),
+                          datetime(1998, 5, 11, 0, 0, 1),
+                          datetime(1998, 5, 11, 0, 0, 2)])
+
+    def testSecondlyByWeekNoAndWeekDay(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 29, 0, 0, 0),
+                          datetime(1997, 12, 29, 0, 0, 1),
+                          datetime(1997, 12, 29, 0, 0, 2)])
+
+    def testSecondlyByWeekNoAndWeekDayLarge(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0, 0),
+                          datetime(1997, 12, 28, 0, 0, 1),
+                          datetime(1997, 12, 28, 0, 0, 2)])
+
+    def testSecondlyByWeekNoAndWeekDayLast(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 12, 28, 0, 0, 0),
+                          datetime(1997, 12, 28, 0, 0, 1),
+                          datetime(1997, 12, 28, 0, 0, 2)])
+
+    def testSecondlyByWeekNoAndWeekDay53(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 12, 28, 0, 0, 0),
+                          datetime(1998, 12, 28, 0, 0, 1),
+                          datetime(1998, 12, 28, 0, 0, 2)])
+
+    def testSecondlyByEaster(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 12, 0, 0, 0),
+                          datetime(1998, 4, 12, 0, 0, 1),
+                          datetime(1998, 4, 12, 0, 0, 2)])
+
+    def testSecondlyByEasterPos(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 13, 0, 0, 0),
+                          datetime(1998, 4, 13, 0, 0, 1),
+                          datetime(1998, 4, 13, 0, 0, 2)])
+
+    def testSecondlyByEasterNeg(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1998, 4, 11, 0, 0, 0),
+                          datetime(1998, 4, 11, 0, 0, 1),
+                          datetime(1998, 4, 11, 0, 0, 2)])
+
+    def testSecondlyByHour(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 0, 0),
+                          datetime(1997, 9, 2, 18, 0, 1),
+                          datetime(1997, 9, 2, 18, 0, 2)])
+
+    def testSecondlyByMinute(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 6, 0),
+                          datetime(1997, 9, 2, 9, 6, 1),
+                          datetime(1997, 9, 2, 9, 6, 2)])
+
+    def testSecondlyBySecond(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              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 testSecondlyByHourAndMinute(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 18, 6, 0),
+                          datetime(1997, 9, 2, 18, 6, 1),
+                          datetime(1997, 9, 2, 18, 6, 2)])
+
+    def testSecondlyByHourAndSecond(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              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 testSecondlyByMinuteAndSecond(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              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 testSecondlyByHourAndMinuteAndSecond(self):
+        self.assertEqual(list(rrule(SECONDLY,
+                              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 testSecondlyByHourAndMinuteAndSecondBug(self):
+        # This explores a bug found by Mathieu Bridon.
+        self.assertEqual(list(rrule(SECONDLY,
+                              count=3,
+                              bysecond=(0,),
+                              byminute=(1,),
+                              dtstart=datetime(2010, 3, 22, 12, 1))),
+                         [datetime(2010, 3, 22, 12, 1),
+                          datetime(2010, 3, 22, 13, 1),
+                          datetime(2010, 3, 22, 14, 1)])
+
+    def testLongIntegers(self):
+        if not PY3:  # There is no longs in python3
+            self.assertEqual(list(rrule(MINUTELY,
+                                  count=long(2),
+                                  interval=long(2),
+                                  bymonth=long(2),
+                                  byweekday=long(3),
+                                  byhour=long(6),
+                                  byminute=long(6),
+                                  bysecond=long(6),
+                                  dtstart=datetime(1997, 9, 2, 9, 0))),
+                             [datetime(1998, 2, 5, 6, 6, 6),
+                              datetime(1998, 2, 12, 6, 6, 6)])
+            self.assertEqual(list(rrule(YEARLY,
+                                  count=long(2),
+                                  bymonthday=long(5),
+                                  byweekno=long(2),
+                                  dtstart=datetime(1997, 9, 2, 9, 0))),
+                             [datetime(1998, 1, 5, 9, 0),
+                              datetime(2004, 1, 5, 9, 0)])
+
+    def testHourlyBadRRule(self):
+        """
+        When `byhour` is specified with `freq=HOURLY`, there are certain
+        combinations of `dtstart` and `byhour` which result in an rrule with no
+        valid values.
+
+        See https://github.com/dateutil/dateutil/issues/4
+        """
+
+        self.assertRaises(ValueError, rrule, HOURLY,
+                          **dict(interval=4, byhour=(7, 11, 15, 19),
+                                 dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testMinutelyBadRRule(self):
+        """
+        See :func:`testHourlyBadRRule` for details.
+        """
+
+        self.assertRaises(ValueError, rrule, MINUTELY,
+                          **dict(interval=12, byminute=(10, 11, 25, 39, 50),
+                                 dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testSecondlyBadRRule(self):
+        """
+        See :func:`testHourlyBadRRule` for details.
+        """
+
+        self.assertRaises(ValueError, rrule, SECONDLY,
+                          **dict(interval=10, bysecond=(2, 15, 37, 42, 59),
+                                 dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testMinutelyBadComboRRule(self):
+        """
+        Certain values of :param:`interval` in :class:`rrule`, when combined
+        with certain values of :param:`byhour` create rules which apply to no
+        valid dates. The library should detect this case in the iterator and
+        raise a :exception:`ValueError`.
+        """
+
+        # In Python 2.7 you can use a context manager for this.
+        def make_bad_rrule():
+            list(rrule(MINUTELY, interval=120, byhour=(10, 12, 14, 16),
+                 count=2, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+        self.assertRaises(ValueError, make_bad_rrule)
+
+    def testSecondlyBadComboRRule(self):
+        """
+        See :func:`testMinutelyBadComboRRule' for details.
+        """
+
+        # In Python 2.7 you can use a context manager for this.
+        def make_bad_minute_rrule():
+            list(rrule(SECONDLY, interval=360, byminute=(10, 28, 49),
+                 count=4, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+        def make_bad_hour_rrule():
+            list(rrule(SECONDLY, interval=43200, byhour=(2, 10, 18, 23),
+                 count=4, dtstart=datetime(1997, 9, 2, 9, 0)))
+
+        self.assertRaises(ValueError, make_bad_minute_rrule)
+        self.assertRaises(ValueError, make_bad_hour_rrule)
+
+    def testBadUntilCountRRule(self):
+        """
+        See rfc-5545 3.3.10 - This checks for the deprecation warning, and will
+        eventually check for an error.
+        """
+        with self.assertWarns(DeprecationWarning):
+            rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0),
+                         count=3, until=datetime(1997, 9, 4, 9, 0))
+
+    def testUntilNotMatching(self):
+        self.assertEqual(list(rrule(DAILY,
+                              dtstart=datetime(1997, 9, 2, 9, 0),
+                              until=datetime(1997, 9, 5, 8, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testUntilMatching(self):
+        self.assertEqual(list(rrule(DAILY,
+                              dtstart=datetime(1997, 9, 2, 9, 0),
+                              until=datetime(1997, 9, 4, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testUntilSingle(self):
+        self.assertEqual(list(rrule(DAILY,
+                              dtstart=datetime(1997, 9, 2, 9, 0),
+                              until=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0)])
+
+    def testUntilEmpty(self):
+        self.assertEqual(list(rrule(DAILY,
+                              dtstart=datetime(1997, 9, 2, 9, 0),
+                              until=datetime(1997, 9, 1, 9, 0))),
+                         [])
+
+    def testUntilWithDate(self):
+        self.assertEqual(list(rrule(DAILY,
+                              dtstart=datetime(1997, 9, 2, 9, 0),
+                              until=date(1997, 9, 5))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testWkStIntervalMO(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              interval=2,
+                              byweekday=(TU, SU),
+                              wkst=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testWkStIntervalSU(self):
+        self.assertEqual(list(rrule(WEEKLY,
+                              count=3,
+                              interval=2,
+                              byweekday=(TU, SU),
+                              wkst=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testDTStartIsDate(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              dtstart=date(1997, 9, 2))),
+                         [datetime(1997, 9, 2, 0, 0),
+                          datetime(1997, 9, 3, 0, 0),
+                          datetime(1997, 9, 4, 0, 0)])
+
+    def testDTStartWithMicroseconds(self):
+        self.assertEqual(list(rrule(DAILY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0, 0, 500000))),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testMaxYear(self):
+        self.assertEqual(list(rrule(YEARLY,
+                              count=3,
+                              bymonth=2,
+                              bymonthday=31,
+                              dtstart=datetime(9997, 9, 2, 9, 0, 0))),
+                         [])
+
+    def testGetItem(self):
+        self.assertEqual(rrule(DAILY,
+                               count=3,
+                               dtstart=datetime(1997, 9, 2, 9, 0))[0],
+                         datetime(1997, 9, 2, 9, 0))
+
+    def testGetItemNeg(self):
+        self.assertEqual(rrule(DAILY,
+                               count=3,
+                               dtstart=datetime(1997, 9, 2, 9, 0))[-1],
+                         datetime(1997, 9, 4, 9, 0))
+
+    def testGetItemSlice(self):
+        self.assertEqual(rrule(DAILY,
+                               # count=3,
+                               dtstart=datetime(1997, 9, 2, 9, 0))[1:2],
+                         [datetime(1997, 9, 3, 9, 0)])
+
+    def testGetItemSliceEmpty(self):
+        self.assertEqual(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 testGetItemSliceStep(self):
+        self.assertEqual(rrule(DAILY,
+                               count=3,
+                               dtstart=datetime(1997, 9, 2, 9, 0))[::-2],
+                         [datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 2, 9, 0)])
+
+    def testCount(self):
+        self.assertEqual(rrule(DAILY,
+                               count=3,
+                               dtstart=datetime(1997, 9, 2, 9, 0)).count(),
+                         3)
+
+    def testCountZero(self):
+        self.assertEqual(rrule(YEARLY,
+                               count=0,
+                               dtstart=datetime(1997, 9, 2, 9, 0)).count(),
+                         0)
+
+    def testContains(self):
+        rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+        self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+    def testContainsNot(self):
+        rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+        self.assertEqual(datetime(1997, 9, 3, 9, 0) not in rr, False)
+
+    def testBefore(self):
+        self.assertEqual(rrule(DAILY,  # count=5
+            dtstart=datetime(1997, 9, 2, 9, 0)).before(datetime(1997, 9, 5, 9, 0)),
+                         datetime(1997, 9, 4, 9, 0))
+
+    def testBeforeInc(self):
+        self.assertEqual(rrule(DAILY,
+                               #count=5,
+                               dtstart=datetime(1997, 9, 2, 9, 0))
+                               .before(datetime(1997, 9, 5, 9, 0), inc=True),
+                         datetime(1997, 9, 5, 9, 0))
+
+    def testAfter(self):
+        self.assertEqual(rrule(DAILY,
+                               #count=5,
+                               dtstart=datetime(1997, 9, 2, 9, 0))
+                               .after(datetime(1997, 9, 4, 9, 0)),
+                         datetime(1997, 9, 5, 9, 0))
+
+    def testAfterInc(self):
+        self.assertEqual(rrule(DAILY,
+                               #count=5,
+                               dtstart=datetime(1997, 9, 2, 9, 0))
+                               .after(datetime(1997, 9, 4, 9, 0), inc=True),
+                         datetime(1997, 9, 4, 9, 0))
+
+    def testXAfter(self):
+        self.assertEqual(list(rrule(DAILY,
+                                    dtstart=datetime(1997, 9, 2, 9, 0))
+                                    .xafter(datetime(1997, 9, 8, 9, 0), count=12)),
+                                    [datetime(1997, 9, 9, 9, 0),
+                                     datetime(1997, 9, 10, 9, 0),
+                                     datetime(1997, 9, 11, 9, 0),
+                                     datetime(1997, 9, 12, 9, 0),
+                                     datetime(1997, 9, 13, 9, 0),
+                                     datetime(1997, 9, 14, 9, 0),
+                                     datetime(1997, 9, 15, 9, 0),
+                                     datetime(1997, 9, 16, 9, 0),
+                                     datetime(1997, 9, 17, 9, 0),
+                                     datetime(1997, 9, 18, 9, 0),
+                                     datetime(1997, 9, 19, 9, 0),
+                                     datetime(1997, 9, 20, 9, 0)])
+
+    def testXAfterInc(self):
+        self.assertEqual(list(rrule(DAILY,
+                                    dtstart=datetime(1997, 9, 2, 9, 0))
+                                    .xafter(datetime(1997, 9, 8, 9, 0), count=12, inc=True)),
+                                    [datetime(1997, 9, 8, 9, 0),
+                                     datetime(1997, 9, 9, 9, 0),
+                                     datetime(1997, 9, 10, 9, 0),
+                                     datetime(1997, 9, 11, 9, 0),
+                                     datetime(1997, 9, 12, 9, 0),
+                                     datetime(1997, 9, 13, 9, 0),
+                                     datetime(1997, 9, 14, 9, 0),
+                                     datetime(1997, 9, 15, 9, 0),
+                                     datetime(1997, 9, 16, 9, 0),
+                                     datetime(1997, 9, 17, 9, 0),
+                                     datetime(1997, 9, 18, 9, 0),
+                                     datetime(1997, 9, 19, 9, 0)])
+
+    def testBetween(self):
+        self.assertEqual(rrule(DAILY,
+                               #count=5,
+                               dtstart=datetime(1997, 9, 2, 9, 0))
+                               .between(datetime(1997, 9, 2, 9, 0),
+                                        datetime(1997, 9, 6, 9, 0)),
+                         [datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0)])
+
+    def testBetweenInc(self):
+        self.assertEqual(rrule(DAILY,
+                               #count=5,
+                               dtstart=datetime(1997, 9, 2, 9, 0))
+                               .between(datetime(1997, 9, 2, 9, 0),
+                                        datetime(1997, 9, 6, 9, 0), inc=True),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0)])
+
+    def testCachePre(self):
+        rr = rrule(DAILY, count=15, cache=True,
+                   dtstart=datetime(1997, 9, 2, 9, 0))
+        self.assertEqual(list(rr),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testCachePost(self):
+        rr = rrule(DAILY, count=15, cache=True,
+                   dtstart=datetime(1997, 9, 2, 9, 0))
+        for x in rr: pass
+        self.assertEqual(list(rr),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testCachePostInternal(self):
+        rr = rrule(DAILY, count=15, cache=True,
+                   dtstart=datetime(1997, 9, 2, 9, 0))
+        for x in rr: pass
+        self.assertEqual(rr._cache,
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testCachePreContains(self):
+        rr = rrule(DAILY, count=3, cache=True,
+                   dtstart=datetime(1997, 9, 2, 9, 0))
+        self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+    def testCachePostContains(self):
+        rr = rrule(DAILY, count=3, cache=True,
+                   dtstart=datetime(1997, 9, 2, 9, 0))
+        for x in rr: pass
+        self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True)
+
+    def testStr(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0)])
+
+    def testStrWithTZID(self):
+        NYC = tz.gettz('America/New_York')
+        self.assertEqual(list(rrulestr(
+                              "DTSTART;TZID=America/New_York:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0, tzinfo=NYC),
+                          datetime(1998, 9, 2, 9, 0, tzinfo=NYC),
+                          datetime(1999, 9, 2, 9, 0, tzinfo=NYC)])
+
+    def testStrWithTZIDMapping(self):
+        rrstr = ("DTSTART;TZID=Eastern:19970902T090000\n" +
+                 "RRULE:FREQ=YEARLY;COUNT=3")
+
+        NYC = tz.gettz('America/New_York')
+        rr = rrulestr(rrstr, tzids={'Eastern': NYC})
+        exp = [datetime(1997, 9, 2, 9, 0, tzinfo=NYC),
+               datetime(1998, 9, 2, 9, 0, tzinfo=NYC),
+               datetime(1999, 9, 2, 9, 0, tzinfo=NYC)]
+
+        self.assertEqual(list(rr), exp)
+
+    def testStrWithTZIDCallable(self):
+        rrstr = ('DTSTART;TZID=UTC+04:19970902T090000\n' +
+                 'RRULE:FREQ=YEARLY;COUNT=3')
+
+        TZ = tz.tzstr('UTC+04')
+        def parse_tzstr(tzstr):
+            if tzstr is None:
+                raise ValueError('Invalid tzstr')
+
+            return tz.tzstr(tzstr)
+
+        rr = rrulestr(rrstr, tzids=parse_tzstr)
+
+        exp = [datetime(1997, 9, 2, 9, 0, tzinfo=TZ),
+               datetime(1998, 9, 2, 9, 0, tzinfo=TZ),
+               datetime(1999, 9, 2, 9, 0, tzinfo=TZ),]
+
+        self.assertEqual(list(rr), exp)
+
+    def testStrWithTZIDCallableFailure(self):
+        rrstr = ('DTSTART;TZID=America/New_York:19970902T090000\n' +
+                 'RRULE:FREQ=YEARLY;COUNT=3')
+
+        class TzInfoError(Exception):
+            pass
+
+        def tzinfos(tzstr):
+            if tzstr == 'America/New_York':
+                raise TzInfoError('Invalid!')
+            return None
+
+        with self.assertRaises(TzInfoError):
+            rrulestr(rrstr, tzids=tzinfos)
+
+    def testStrWithConflictingTZID(self):
+        # RFC 5545 Section 3.3.5, FORM #2: DATE WITH UTC TIME
+        # https://tools.ietf.org/html/rfc5545#section-3.3.5
+        # The "TZID" property parameter MUST NOT be applied to DATE-TIME
+        with self.assertRaises(ValueError):
+            rrulestr("DTSTART;TZID=America/New_York:19970902T090000Z\n"+
+                     "RRULE:FREQ=YEARLY;COUNT=3\n")
+
+    def testStrType(self):
+        self.assertEqual(isinstance(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3\n"
+                              ), rrule), True)
+
+    def testStrForceSetType(self):
+        self.assertEqual(isinstance(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3\n"
+                              , forceset=True), rruleset), True)
+
+    def testStrSetType(self):
+        self.assertEqual(isinstance(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n"
+                              "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n"
+                              ), rruleset), True)
+
+    def testStrCase(self):
+        self.assertEqual(list(rrulestr(
+                              "dtstart:19970902T090000\n"
+                              "rrule:freq=yearly;count=3\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0)])
+
+    def testStrSpaces(self):
+        self.assertEqual(list(rrulestr(
+                              " DTSTART:19970902T090000 "
+                              " RRULE:FREQ=YEARLY;COUNT=3 "
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0)])
+
+    def testStrSpacesAndLines(self):
+        self.assertEqual(list(rrulestr(
+                              " DTSTART:19970902T090000 \n"
+                              " \n"
+                              " RRULE:FREQ=YEARLY;COUNT=3 \n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1998, 9, 2, 9, 0),
+                          datetime(1999, 9, 2, 9, 0)])
+
+    def testStrNoDTStart(self):
+        self.assertEqual(list(rrulestr(
+                              "RRULE:FREQ=YEARLY;COUNT=3\n"
+                              , 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 testStrValueOnly(self):
+        self.assertEqual(list(rrulestr(
+                              "FREQ=YEARLY;COUNT=3\n"
+                              , 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 testStrUnfold(self):
+        self.assertEqual(list(rrulestr(
+                              "FREQ=YEA\n RLY;COUNT=3\n", unfold=True,
+                              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 testStrSet(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n"
+                              "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testStrSetDate(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n"
+                              "RDATE:19970904T090000\n"
+                              "RDATE:19970909T090000\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testStrSetExRule(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+                              "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testStrSetExDate(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+                              "EXDATE:19970904T090000\n"
+                              "EXDATE:19970911T090000\n"
+                              "EXDATE:19970918T090000\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testStrSetDateAndExDate(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RDATE:19970902T090000\n"
+                              "RDATE:19970904T090000\n"
+                              "RDATE:19970909T090000\n"
+                              "RDATE:19970911T090000\n"
+                              "RDATE:19970916T090000\n"
+                              "RDATE:19970918T090000\n"
+                              "EXDATE:19970904T090000\n"
+                              "EXDATE:19970911T090000\n"
+                              "EXDATE:19970918T090000\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testStrSetDateAndExRule(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RDATE:19970902T090000\n"
+                              "RDATE:19970904T090000\n"
+                              "RDATE:19970909T090000\n"
+                              "RDATE:19970911T090000\n"
+                              "RDATE:19970916T090000\n"
+                              "RDATE:19970918T090000\n"
+                              "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n"
+                              )),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testStrKeywords(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;"
+                                    "BYMONTH=3;BYWEEKDAY=TH;BYMONTHDAY=3;"
+                                    "BYHOUR=3;BYMINUTE=3;BYSECOND=3\n"
+                              )),
+                         [datetime(2033, 3, 3, 3, 3, 3),
+                          datetime(2039, 3, 3, 3, 3, 3),
+                          datetime(2072, 3, 3, 3, 3, 3)])
+
+    def testStrNWeekDay(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n"
+                              )),
+                         [datetime(1997, 12, 25, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 12, 31, 9, 0)])
+
+    def testStrUntil(self):
+        self.assertEqual(list(rrulestr(
+                              "DTSTART:19970902T090000\n"
+                              "RRULE:FREQ=YEARLY;"
+                              "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n"
+                              )),
+                         [datetime(1997, 12, 25, 9, 0),
+                          datetime(1998, 1, 6, 9, 0),
+                          datetime(1998, 12, 31, 9, 0)])
+
+    def testStrValueDatetime(self):
+        rr = rrulestr("DTSTART;VALUE=DATE-TIME:19970902T090000\n"
+                       "RRULE:FREQ=YEARLY;COUNT=2")
+
+        self.assertEqual(list(rr), [datetime(1997, 9, 2, 9, 0, 0),
+                                    datetime(1998, 9, 2, 9, 0, 0)])
+
+    def testStrValueDate(self):
+        rr = rrulestr("DTSTART;VALUE=DATE:19970902\n"
+                       "RRULE:FREQ=YEARLY;COUNT=2")
+
+        self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0),
+                                    datetime(1998, 9, 2, 0, 0, 0)])
+
+    def testStrInvalidUntil(self):
+        with self.assertRaises(ValueError):
+            list(rrulestr("DTSTART:19970902T090000\n"
+                          "RRULE:FREQ=YEARLY;"
+                          "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n"))
+
+    def testStrUntilMustBeUTC(self):
+        with self.assertRaises(ValueError):
+            list(rrulestr("DTSTART;TZID=America/New_York:19970902T090000\n"
+                          "RRULE:FREQ=YEARLY;"
+                          "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n"))
+
+    def testStrUntilWithTZ(self):
+        NYC = tz.gettz('America/New_York')
+        rr = list(rrulestr("DTSTART;TZID=America/New_York:19970101T000000\n"
+                          "RRULE:FREQ=YEARLY;"
+                          "UNTIL=19990101T000000Z\n"))
+        self.assertEqual(list(rr), [datetime(1997, 1, 1, 0, 0, 0, tzinfo=NYC),
+                                    datetime(1998, 1, 1, 0, 0, 0, tzinfo=NYC)])
+
+    def testStrEmptyByDay(self):
+        with self.assertRaises(ValueError):
+            list(rrulestr("DTSTART:19970902T090000\n"
+                          "FREQ=WEEKLY;"
+                          "BYDAY=;"         # This part is invalid
+                          "WKST=SU"))
+
+    def testStrInvalidByDay(self):
+        with self.assertRaises(ValueError):
+            list(rrulestr("DTSTART:19970902T090000\n"
+                          "FREQ=WEEKLY;"
+                          "BYDAY=-1OK;"         # This part is invalid
+                          "WKST=SU"))
+
+    def testBadBySetPos(self):
+        self.assertRaises(ValueError,
+                          rrule, MONTHLY,
+                                 count=1,
+                                 bysetpos=0,
+                                 dtstart=datetime(1997, 9, 2, 9, 0))
+
+    def testBadBySetPosMany(self):
+        self.assertRaises(ValueError,
+                          rrule, MONTHLY,
+                                 count=1,
+                                 bysetpos=(-1, 0, 1),
+                                 dtstart=datetime(1997, 9, 2, 9, 0))
+
+    # Tests to ensure that str(rrule) works
+    def testToStrYearly(self):
+        rule = rrule(YEARLY, count=3, dtstart=datetime(1997, 9, 2, 9, 0))
+        self._rrulestr_reverse_test(rule)
+
+    def testToStrYearlyInterval(self):
+        rule = rrule(YEARLY, count=3, interval=2,
+                     dtstart=datetime(1997, 9, 2, 9, 0))
+        self._rrulestr_reverse_test(rule)
+
+    def testToStrYearlyByMonth(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                                          count=3,
+                                          bymonth=(1, 3),
+                                          dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                                          count=3,
+                                          bymonthday=(1, 3),
+                                          dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                                          count=3,
+                                          bymonth=(1, 3),
+                                          bymonthday=(5, 7),
+                                          dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                                          count=3,
+                                          byweekday=(TU, TH),
+                                          dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                                          count=3,
+                                          byweekday=(TU(1), TH(-1)),
+                                          dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByNWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndNWeekDayLarge(self):
+        # This is interesting because the TH(-3) ends up before
+        # the TU(3).
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByEaster(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByHour(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMinute(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyBySecond(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrYearlyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(YEARLY,
+                              count=3,
+                              bymonthday=15,
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthly(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyInterval(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              interval=18,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonth(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 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 testToStrMonthlyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByNWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndNWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(3), TH(-3)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByEaster(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByHour(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMinute(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyBySecond(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMonthlyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(MONTHLY,
+                              count=3,
+                              bymonthday=(13, 17),
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeekly(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyInterval(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              interval=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonth(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndWeekDay(self):
+        # This test is interesting, because it crosses the year
+        # boundary in a weekly period to find day '1' as a
+        # valid recurrence.
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByEaster(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByHour(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMinute(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyBySecond(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrWeeklyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              byhour=(6, 18),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDaily(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyInterval(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              interval=92,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonth(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=4,
+                              bymonth=(1, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekNoAndWeekDay(self):
+        # That's a nice one. The first days of week number one
+        # may be in the last year.
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekNoAndWeekDayLarge(self):
+        # Another nice test. The last days of week number 52/53
+        # may be in the next year.
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByEaster(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByHour(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMinute(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyBySecond(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrDailyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(DAILY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(15, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourly(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyInterval(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              interval=769,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonth(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekNoAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekNoAndWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByEaster(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByHour(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMinute(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyBySecond(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrHourlyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(HOURLY,
+                              count=3,
+                              byminute=(15, 45),
+                              bysecond=(15, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutely(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyInterval(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              interval=1501,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonth(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekNoAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekNoAndWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByEaster(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByHour(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMinute(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyBySecond(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrMinutelyBySetPos(self):
+        self._rrulestr_reverse_test(rrule(MINUTELY,
+                              count=3,
+                              bysecond=(15, 30, 45),
+                              bysetpos=(3, -3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondly(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyInterval(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              interval=2,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyIntervalLarge(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              interval=90061,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonth(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndMonthDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(5, 7),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndNWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              byweekday=(TU(1), TH(-1)),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndMonthDayAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bymonth=(1, 3),
+                              bymonthday=(1, 3),
+                              byweekday=(TU, TH),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByYearDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=4,
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=4,
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndYearDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(1, 100, 200, 365),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMonthAndYearDayNeg(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=4,
+                              bymonth=(4, 7),
+                              byyearday=(-365, -266, -166, -1),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekNo(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekno=20,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekNoAndWeekDay(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekno=1,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekNoAndWeekDayLarge(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekno=52,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekNoAndWeekDayLast(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekno=-1,
+                              byweekday=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByWeekNoAndWeekDay53(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byweekno=53,
+                              byweekday=MO,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByEaster(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byeaster=0,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByEasterPos(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byeaster=1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByEasterNeg(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byeaster=-1,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByHour(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMinute(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyBySecond(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByHourAndMinute(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByHourAndSecond(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByHourAndMinuteAndSecond(self):
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              byhour=(6, 18),
+                              byminute=(6, 18),
+                              bysecond=(6, 18),
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrSecondlyByHourAndMinuteAndSecondBug(self):
+        # This explores a bug found by Mathieu Bridon.
+        self._rrulestr_reverse_test(rrule(SECONDLY,
+                              count=3,
+                              bysecond=(0,),
+                              byminute=(1,),
+                              dtstart=datetime(2010, 3, 22, 12, 1)))
+
+    def testToStrWithWkSt(self):
+        self._rrulestr_reverse_test(rrule(WEEKLY,
+                              count=3,
+                              wkst=SU,
+                              dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testToStrLongIntegers(self):
+        if not PY3:  # There is no longs in python3
+            self._rrulestr_reverse_test(rrule(MINUTELY,
+                                  count=long(2),
+                                  interval=long(2),
+                                  bymonth=long(2),
+                                  byweekday=long(3),
+                                  byhour=long(6),
+                                  byminute=long(6),
+                                  bysecond=long(6),
+                                  dtstart=datetime(1997, 9, 2, 9, 0)))
+
+            self._rrulestr_reverse_test(rrule(YEARLY,
+                                  count=long(2),
+                                  bymonthday=long(5),
+                                  byweekno=long(2),
+                                  dtstart=datetime(1997, 9, 2, 9, 0)))
+
+    def testReplaceIfSet(self):
+        rr = rrule(YEARLY,
+                   count=1,
+                   bymonthday=5,
+                   dtstart=datetime(1997, 1, 1))
+        newrr = rr.replace(bymonthday=6)
+        self.assertEqual(list(rr), [datetime(1997, 1, 5)])
+        self.assertEqual(list(newrr),
+                             [datetime(1997, 1, 6)])
+
+    def testReplaceIfNotSet(self):
+        rr = rrule(YEARLY,
+           count=1,
+           dtstart=datetime(1997, 1, 1))
+        newrr = rr.replace(bymonthday=6)
+        self.assertEqual(list(rr), [datetime(1997, 1, 1)])
+        self.assertEqual(list(newrr),
+                             [datetime(1997, 1, 6)])
+
+
+@pytest.mark.rrule
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart():
+    dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)
+    UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC)
+
+    rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL)
+    rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL)
+    assert list(rule_without_dtstart) == list(rule_with_dtstart)
+
+
+@pytest.mark.rrule
+@pytest.mark.rrulestr
+@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637")
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart_rrulestr():
+    rrule_without_dtstart = rrule(freq=HOURLY,
+                                  until=datetime(2018, 3, 6, 8, 0,
+                                                 tzinfo=tz.UTC))
+    rrule_r = rrulestr(str(rrule_without_dtstart))
+
+    assert list(rrule_r) == list(rrule_without_dtstart)
+
+
+@pytest.mark.rruleset
+class RRuleSetTest(unittest.TestCase):
+    def testSet(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetDate(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=1, byweekday=TU,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.rdate(datetime(1997, 9, 4, 9))
+        rrset.rdate(datetime(1997, 9, 9, 9))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetExRule(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testSetExDate(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.exdate(datetime(1997, 9, 4, 9))
+        rrset.exdate(datetime(1997, 9, 11, 9))
+        rrset.exdate(datetime(1997, 9, 18, 9))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testSetExDateRevOrder(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(MONTHLY, count=5, bymonthday=10,
+                          dtstart=datetime(2004, 1, 1, 9, 0)))
+        rrset.exdate(datetime(2004, 4, 10, 9, 0))
+        rrset.exdate(datetime(2004, 2, 10, 9, 0))
+        self.assertEqual(list(rrset),
+                         [datetime(2004, 1, 10, 9, 0),
+                          datetime(2004, 3, 10, 9, 0),
+                          datetime(2004, 5, 10, 9, 0)])
+
+    def testSetDateAndExDate(self):
+        rrset = rruleset()
+        rrset.rdate(datetime(1997, 9, 2, 9))
+        rrset.rdate(datetime(1997, 9, 4, 9))
+        rrset.rdate(datetime(1997, 9, 9, 9))
+        rrset.rdate(datetime(1997, 9, 11, 9))
+        rrset.rdate(datetime(1997, 9, 16, 9))
+        rrset.rdate(datetime(1997, 9, 18, 9))
+        rrset.exdate(datetime(1997, 9, 4, 9))
+        rrset.exdate(datetime(1997, 9, 11, 9))
+        rrset.exdate(datetime(1997, 9, 18, 9))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testSetDateAndExRule(self):
+        rrset = rruleset()
+        rrset.rdate(datetime(1997, 9, 2, 9))
+        rrset.rdate(datetime(1997, 9, 4, 9))
+        rrset.rdate(datetime(1997, 9, 9, 9))
+        rrset.rdate(datetime(1997, 9, 11, 9))
+        rrset.rdate(datetime(1997, 9, 16, 9))
+        rrset.rdate(datetime(1997, 9, 18, 9))
+        rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+                           dtstart=datetime(1997, 9, 2, 9, 0)))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testSetCount(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH),
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.exrule(rrule(YEARLY, count=3, byweekday=TH,
+                           dtstart=datetime(1997, 9, 2, 9, 0)))
+        self.assertEqual(rrset.count(), 3)
+
+    def testSetCachePre(self):
+        rrset = rruleset()
+        rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetCachePost(self):
+        rrset = rruleset(cache=True)
+        rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        for x in rrset: pass
+        self.assertEqual(list(rrset),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetCachePostInternal(self):
+        rrset = rruleset(cache=True)
+        rrset.rrule(rrule(YEARLY, count=2, byweekday=TU,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        rrset.rrule(rrule(YEARLY, count=1, byweekday=TH,
+                          dtstart=datetime(1997, 9, 2, 9, 0)))
+        for x in rrset: pass
+        self.assertEqual(list(rrset._cache),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetRRuleCount(self):
+        # Test that the count is updated when an rrule is added
+        rrset = rruleset(cache=False)
+        for cache in (True, False):
+            rrset = rruleset(cache=cache)
+            rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+                              dtstart=datetime(1983, 4, 1)))
+            rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+                              dtstart=datetime(1991, 6, 3)))
+
+            # Check the length twice - first one sets a cache, second reads it
+            self.assertEqual(rrset.count(), 6)
+            self.assertEqual(rrset.count(), 6)
+
+            # This should invalidate the cache and force an update
+            rrset.rrule(rrule(MONTHLY, count=3, dtstart=datetime(1994, 1, 3)))
+
+            self.assertEqual(rrset.count(), 9)
+            self.assertEqual(rrset.count(), 9)
+
+    def testSetRDateCount(self):
+        # Test that the count is updated when an rdate is added
+        rrset = rruleset(cache=False)
+        for cache in (True, False):
+            rrset = rruleset(cache=cache)
+            rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+                              dtstart=datetime(1983, 4, 1)))
+            rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+                              dtstart=datetime(1991, 6, 3)))
+
+            # Check the length twice - first one sets a cache, second reads it
+            self.assertEqual(rrset.count(), 6)
+            self.assertEqual(rrset.count(), 6)
+
+            # This should invalidate the cache and force an update
+            rrset.rdate(datetime(1993, 2, 14))
+
+            self.assertEqual(rrset.count(), 7)
+            self.assertEqual(rrset.count(), 7)
+
+    def testSetExRuleCount(self):
+        # Test that the count is updated when an exrule is added
+        rrset = rruleset(cache=False)
+        for cache in (True, False):
+            rrset = rruleset(cache=cache)
+            rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+                              dtstart=datetime(1983, 4, 1)))
+            rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+                              dtstart=datetime(1991, 6, 3)))
+
+            # Check the length twice - first one sets a cache, second reads it
+            self.assertEqual(rrset.count(), 6)
+            self.assertEqual(rrset.count(), 6)
+
+            # This should invalidate the cache and force an update
+            rrset.exrule(rrule(WEEKLY, count=2, interval=2,
+                               dtstart=datetime(1991, 6, 14)))
+
+            self.assertEqual(rrset.count(), 4)
+            self.assertEqual(rrset.count(), 4)
+
+    def testSetExDateCount(self):
+        # Test that the count is updated when an rdate is added
+        for cache in (True, False):
+            rrset = rruleset(cache=cache)
+            rrset.rrule(rrule(YEARLY, count=2, byweekday=TH,
+                              dtstart=datetime(1983, 4, 1)))
+            rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR,
+                              dtstart=datetime(1991, 6, 3)))
+
+            # Check the length twice - first one sets a cache, second reads it
+            self.assertEqual(rrset.count(), 6)
+            self.assertEqual(rrset.count(), 6)
+
+            # This should invalidate the cache and force an update
+            rrset.exdate(datetime(1991, 6, 28))
+
+            self.assertEqual(rrset.count(), 5)
+            self.assertEqual(rrset.count(), 5)
+
+
+class WeekdayTest(unittest.TestCase):
+    def testInvalidNthWeekday(self):
+        with self.assertRaises(ValueError):
+            FR(0)
+
+    def testWeekdayCallable(self):
+        # Calling a weekday instance generates a new weekday instance with the
+        # value of n changed.
+        from dateutil.rrule import weekday
+        self.assertEqual(MO(1), weekday(0, 1))
+
+        # Calling a weekday instance with the identical n returns the original
+        # object
+        FR_3 = weekday(4, 3)
+        self.assertIs(FR_3(3), FR_3)
+
+    def testWeekdayEquality(self):
+        # Two weekday objects are not equal if they have different values for n
+        self.assertNotEqual(TH, TH(-1))
+        self.assertNotEqual(SA(3), SA(2))
+
+    def testWeekdayEqualitySubclass(self):
+        # Two weekday objects equal if their "weekday" and "n" attributes are
+        # available and the same
+        class BasicWeekday(object):
+            def __init__(self, weekday):
+                self.weekday = weekday
+
+        class BasicNWeekday(BasicWeekday):
+            def __init__(self, weekday, n=None):
+                super(BasicNWeekday, self).__init__(weekday)
+                self.n = n
+
+        MO_Basic = BasicWeekday(0)
+
+        self.assertNotEqual(MO, MO_Basic)
+        self.assertNotEqual(MO(1), MO_Basic)
+
+        TU_BasicN = BasicNWeekday(1)
+
+        self.assertEqual(TU, TU_BasicN)
+        self.assertNotEqual(TU(3), TU_BasicN)
+
+        WE_Basic3 = BasicNWeekday(2, 3)
+        self.assertEqual(WE(3), WE_Basic3)
+        self.assertNotEqual(WE(2), WE_Basic3)
+
+    def testWeekdayReprNoN(self):
+        no_n_reprs = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU')
+        no_n_wdays = (MO, TU, WE, TH, FR, SA, SU)
+
+        for repstr, wday in zip(no_n_reprs, no_n_wdays):
+            self.assertEqual(repr(wday), repstr)
+
+    def testWeekdayReprWithN(self):
+        with_n_reprs = ('WE(+1)', 'TH(-2)', 'SU(+3)')
+        with_n_wdays = (WE(1), TH(-2), SU(+3))
+
+        for repstr, wday in zip(with_n_reprs, with_n_wdays):
+            self.assertEqual(repr(wday), repstr)
diff --git a/resources/lib/libraries/dateutil/test/test_tz.py b/resources/lib/libraries/dateutil/test/test_tz.py
new file mode 100644
index 00000000..54dfb1bd
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_tz.py
@@ -0,0 +1,2603 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from ._common import PicklableMixin
+from ._common import TZEnvContext, TZWinContext
+from ._common import WarningTestMixin
+from ._common import ComparesEqual
+
+from datetime import datetime, timedelta
+from datetime import time as dt_time
+from datetime import tzinfo
+from six import BytesIO, StringIO
+import unittest
+
+import sys
+import base64
+import copy
+
+from functools import partial
+
+IS_WIN = sys.platform.startswith('win')
+
+import pytest
+
+# dateutil imports
+from dateutil.relativedelta import relativedelta, SU, TH
+from dateutil.parser import parse
+from dateutil import tz as tz
+from dateutil import zoneinfo
+
+try:
+    from dateutil import tzwin
+except ImportError as e:
+    if IS_WIN:
+        raise e
+    else:
+        pass
+
+MISSING_TARBALL = ("This test fails if you don't have the dateutil "
+                   "timezone file installed. Please read the README")
+
+TZFILE_EST5EDT = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
+ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
+S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
+YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
+yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
+wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
+8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
+YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
+BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR
+iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x
+znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H
+cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w
+Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH
+JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM
+jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w
+4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg
+b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8
+o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
+AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
+AEVQVAAAAAABAAAAAQ==
+"""
+
+EUROPE_HELSINKI = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV
+I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM
+VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8
+kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q
+Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK
+46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV
+RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u
+kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ
+czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC
+AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD
+BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME
+AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ==
+"""
+
+NEW_YORK = b"""
+VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
+ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
+S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
+YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
+yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
+wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
+8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
+YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
+BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR
+iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x
+zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H
+gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG
+Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH
+LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV
+yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr
+d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3
+b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8
+fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
+AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
+AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
+AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G
+AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A
+AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA
+ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE=
+"""
+
+TZICAL_EST5EDT = """
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:19870101T000000Z
+TZURL:http://zones.stds_r_us.net/tz/US-Eastern
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"""
+
+TZICAL_PST8PDT = """
+BEGIN:VTIMEZONE
+TZID:US-Pacific
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+"""
+
+EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
+EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))
+
+
+###
+# Helper functions
+def get_timezone_tuple(dt):
+    """Retrieve a (tzname, utcoffset, dst) tuple for a given DST"""
+    return dt.tzname(), dt.utcoffset(), dt.dst()
+
+
+###
+# Mix-ins
+class context_passthrough(object):
+    def __init__(*args, **kwargs):
+        pass
+
+    def __enter__(*args, **kwargs):
+        pass
+
+    def __exit__(*args, **kwargs):
+        pass
+
+
+class TzFoldMixin(object):
+    """ Mix-in class for testing ambiguous times """
+    def gettz(self, tzname):
+        raise NotImplementedError
+
+    def _get_tzname(self, tzname):
+        return tzname
+
+    def _gettz_context(self, tzname):
+        return context_passthrough()
+
+    def testFoldPositiveUTCOffset(self):
+        # Test that we can resolve ambiguous times
+        tzname = self._get_tzname('Australia/Sydney')
+
+        with self._gettz_context(tzname):
+            SYD = self.gettz(tzname)
+
+            t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.tzutc())  # AEST
+            t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc())  # AEDT
+
+            t0_syd0 = t0_u.astimezone(SYD)
+            t1_syd1 = t1_u.astimezone(SYD)
+
+            self.assertEqual(t0_syd0.replace(tzinfo=None),
+                             datetime(2012, 4, 1, 2, 30))
+
+            self.assertEqual(t1_syd1.replace(tzinfo=None),
+                             datetime(2012, 4, 1, 2, 30))
+
+            self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11))
+            self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10))
+
+    def testGapPositiveUTCOffset(self):
+        # Test that we don't have a problem around gaps.
+        tzname = self._get_tzname('Australia/Sydney')
+
+        with self._gettz_context(tzname):
+            SYD = self.gettz(tzname)
+
+            t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.tzutc())  # AEST
+            t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.tzutc())  # AEDT
+
+            t0 = t0_u.astimezone(SYD)
+            t1 = t1_u.astimezone(SYD)
+
+            self.assertEqual(t0.replace(tzinfo=None),
+                             datetime(2012, 10, 7, 1, 30))
+
+            self.assertEqual(t1.replace(tzinfo=None),
+                             datetime(2012, 10, 7, 3, 30))
+
+            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
+
+    def testFoldNegativeUTCOffset(self):
+            # Test that we can resolve ambiguous times
+            tzname = self._get_tzname('America/Toronto')
+
+            with self._gettz_context(tzname):
+                TOR = self.gettz(tzname)
+
+                t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.tzutc())
+                t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.tzutc())
+
+                t0_tor = t0_u.astimezone(TOR)
+                t1_tor = t1_u.astimezone(TOR)
+
+                self.assertEqual(t0_tor.replace(tzinfo=None),
+                                 datetime(2011, 11, 6, 1, 30))
+
+                self.assertEqual(t1_tor.replace(tzinfo=None),
+                                 datetime(2011, 11, 6, 1, 30))
+
+                self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
+                self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
+                self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
+
+    def testGapNegativeUTCOffset(self):
+        # Test that we don't have a problem around gaps.
+        tzname = self._get_tzname('America/Toronto')
+
+        with self._gettz_context(tzname):
+            TOR = self.gettz(tzname)
+
+            t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.tzutc())
+            t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.tzutc())
+
+            t0 = t0_u.astimezone(TOR)
+            t1 = t1_u.astimezone(TOR)
+
+            self.assertEqual(t0.replace(tzinfo=None),
+                             datetime(2011, 3, 13, 1, 30))
+
+            self.assertEqual(t1.replace(tzinfo=None),
+                             datetime(2011, 3, 13, 3, 30))
+
+            self.assertNotEqual(t0, t1)
+            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
+
+    def testFoldLondon(self):
+        tzname = self._get_tzname('Europe/London')
+
+        with self._gettz_context(tzname):
+            LON = self.gettz(tzname)
+            UTC = tz.tzutc()
+
+            t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC)   # BST
+            t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC)   # GMT
+
+            t0 = t0_u.astimezone(LON)
+            t1 = t1_u.astimezone(LON)
+
+            self.assertEqual(t0.replace(tzinfo=None),
+                             datetime(2013, 10, 27, 1, 30))
+
+            self.assertEqual(t1.replace(tzinfo=None),
+                             datetime(2013, 10, 27, 1, 30))
+
+            self.assertEqual(t0.utcoffset(), timedelta(hours=1))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=0))
+
+    def testFoldIndependence(self):
+        tzname = self._get_tzname('America/New_York')
+
+        with self._gettz_context(tzname):
+            NYC = self.gettz(tzname)
+            UTC = tz.tzutc()
+            hour = timedelta(hours=1)
+
+            # Firmly 2015-11-01 0:30 EDT-4
+            pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC)
+
+            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
+            in_dst = pre_dst + hour
+            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
+
+            # Doing the arithmetic in UTC creates a date that is unambiguously
+            # 2015-11-01 1:30 EDT-5
+            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
+
+            # Make sure the dates are actually ambiguous
+            self.assertEqual(in_dst, in_dst_via_utc)
+
+            # Make sure we got the right folding behavior
+            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
+
+            # Now check to make sure in_dst's tzname hasn't changed
+            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
+
+    def testInZoneFoldEquality(self):
+        # Two datetimes in the same zone are considered to be equal if their
+        # wall times are equal, even if they have different absolute times.
+
+        tzname = self._get_tzname('America/New_York')
+
+        with self._gettz_context(tzname):
+            NYC = self.gettz(tzname)
+            UTC = tz.tzutc()
+
+            dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC)
+            dt1 = tz.enfold(dt0, fold=1)
+
+            # Make sure these actually represent different times
+            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
+
+            # Test that they compare equal
+            self.assertEqual(dt0, dt1)
+
+    def _test_ambiguous_time(self, dt, tzid, ambiguous):
+        # This is a test to check that the individual is_ambiguous values
+        # on the _tzinfo subclasses work.
+        tzname = self._get_tzname(tzid)
+
+        with self._gettz_context(tzname):
+            tzi = self.gettz(tzname)
+
+            self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous)
+
+    def testAmbiguousNegativeUTCOffset(self):
+        self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30),
+                                  'America/New_York', True)
+
+    def testAmbiguousPositiveUTCOffset(self):
+        self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30),
+                                  'Australia/Sydney', True)
+
+    def testUnambiguousNegativeUTCOffset(self):
+        self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30),
+                                  'America/New_York', False)
+
+    def testUnambiguousPositiveUTCOffset(self):
+        self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30),
+                                  'Australia/Sydney', False)
+
+    def testUnambiguousGapNegativeUTCOffset(self):
+        # Imaginary time
+        self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30),
+                                  'America/New_York', False)
+
+    def testUnambiguousGapPositiveUTCOffset(self):
+        # Imaginary time
+        self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30),
+                                  'Australia/Sydney', False)
+
+    def _test_imaginary_time(self, dt, tzid, exists):
+        tzname = self._get_tzname(tzid)
+        with self._gettz_context(tzname):
+            tzi = self.gettz(tzname)
+
+            self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists)
+
+    def testImaginaryNegativeUTCOffset(self):
+        self._test_imaginary_time(datetime(2011, 3, 13, 2, 30),
+                                  'America/New_York', False)
+
+    def testNotImaginaryNegativeUTCOffset(self):
+        self._test_imaginary_time(datetime(2011, 3, 13, 1, 30),
+                                  'America/New_York', True)
+
+    def testImaginaryPositiveUTCOffset(self):
+        self._test_imaginary_time(datetime(2012, 10, 7, 2, 30),
+                                  'Australia/Sydney', False)
+
+    def testNotImaginaryPositiveUTCOffset(self):
+        self._test_imaginary_time(datetime(2012, 10, 7, 1, 30),
+                                  'Australia/Sydney', True)
+
+    def testNotImaginaryFoldNegativeUTCOffset(self):
+        self._test_imaginary_time(datetime(2015, 11, 1, 1, 30),
+                                  'America/New_York', True)
+
+    def testNotImaginaryFoldPositiveUTCOffset(self):
+        self._test_imaginary_time(datetime(2012, 4, 1, 3, 30),
+                                  'Australia/Sydney', True)
+
+    @unittest.skip("Known failure in Python 3.6.")
+    def testEqualAmbiguousComparison(self):
+        tzname = self._get_tzname('Australia/Sydney')
+
+        with self._gettz_context(tzname):
+            SYD0 = self.gettz(tzname)
+            SYD1 = self.gettz(tzname)
+
+            t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.tzutc())  # AEST
+
+            t0_syd0 = t0_u.astimezone(SYD0)
+            t0_syd1 = t0_u.astimezone(SYD1)
+
+            # This is considered an "inter-zone comparison" because it's an
+            # ambiguous datetime.
+            self.assertEqual(t0_syd0, t0_syd1)
+
+
+class TzWinFoldMixin(object):
+    def get_args(self, tzname):
+        return (tzname, )
+
+    class context(object):
+        def __init__(*args, **kwargs):
+            pass
+
+        def __enter__(*args, **kwargs):
+            pass
+
+        def __exit__(*args, **kwargs):
+            pass
+
+    def get_utc_transitions(self, tzi, year, gap):
+        dston, dstoff = tzi.transitions(year)
+        if gap:
+            t_n = dston - timedelta(minutes=30)
+
+            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc())
+            t1_u = t0_u + timedelta(hours=1)
+        else:
+            # Get 1 hour before the first ambiguous date
+            t_n = dstoff - timedelta(minutes=30)
+
+            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc())
+            t_n += timedelta(hours=1)                   # Naive ambiguous date
+            t0_u = t0_u + timedelta(hours=1)            # First ambiguous date
+            t1_u = t0_u + timedelta(hours=1)            # Second ambiguous date
+
+        return t_n, t0_u, t1_u
+
+    def testFoldPositiveUTCOffset(self):
+        # Test that we can resolve ambiguous times
+        tzname = 'AUS Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            # Calling fromutc() alters the tzfile object
+            SYD = self.tzclass(*args)
+
+            # Get the transition time in UTC from the object, because
+            # Windows doesn't store historical info
+            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False)
+
+            # Using fresh tzfiles
+            t0_syd = t0_u.astimezone(SYD)
+            t1_syd = t1_u.astimezone(SYD)
+
+            self.assertEqual(t0_syd.replace(tzinfo=None), t_n)
+
+            self.assertEqual(t1_syd.replace(tzinfo=None), t_n)
+
+            self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11))
+            self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10))
+            self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname())
+
+    def testGapPositiveUTCOffset(self):
+        # Test that we don't have a problem around gaps.
+        tzname = 'AUS Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            SYD = self.tzclass(*args)
+
+            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True)
+
+            t0 = t0_u.astimezone(SYD)
+            t1 = t1_u.astimezone(SYD)
+
+            self.assertEqual(t0.replace(tzinfo=None), t_n)
+
+            self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2))
+
+            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
+
+    def testFoldNegativeUTCOffset(self):
+        # Test that we can resolve ambiguous times
+        tzname = 'Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            TOR = self.tzclass(*args)
+
+            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False)
+
+            t0_tor = t0_u.astimezone(TOR)
+            t1_tor = t1_u.astimezone(TOR)
+
+            self.assertEqual(t0_tor.replace(tzinfo=None), t_n)
+            self.assertEqual(t1_tor.replace(tzinfo=None), t_n)
+
+            self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
+            self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
+            self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
+
+    def testGapNegativeUTCOffset(self):
+        # Test that we don't have a problem around gaps.
+        tzname = 'Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            TOR = self.tzclass(*args)
+
+            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True)
+
+            t0 = t0_u.astimezone(TOR)
+            t1 = t1_u.astimezone(TOR)
+
+            self.assertEqual(t0.replace(tzinfo=None),
+                             t_n)
+
+            self.assertEqual(t1.replace(tzinfo=None),
+                             t_n + timedelta(hours=2))
+
+            self.assertNotEqual(t0.tzname(), t1.tzname())
+            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
+
+    def testFoldIndependence(self):
+        tzname = 'Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            NYC = self.tzclass(*args)
+            UTC = tz.tzutc()
+            hour = timedelta(hours=1)
+
+            # Firmly 2015-11-01 0:30 EDT-4
+            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False)
+
+            pre_dst = (t_n - hour).replace(tzinfo=NYC)
+
+            # Currently, there's no way around the fact that this resolves to an
+            # ambiguous date, which defaults to EST. I'm not hard-coding in the
+            # answer, though, because the preferred behavior would be that this
+            # results in a time on the EDT side.
+
+            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
+            in_dst = pre_dst + hour
+            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
+
+            # Doing the arithmetic in UTC creates a date that is unambiguously
+            # 2015-11-01 1:30 EDT-5
+            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
+
+            # Make sure we got the right folding behavior
+            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
+
+            # Now check to make sure in_dst's tzname hasn't changed
+            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
+
+    def testInZoneFoldEquality(self):
+        # Two datetimes in the same zone are considered to be equal if their
+        # wall times are equal, even if they have different absolute times.
+        tzname = 'Eastern Standard Time'
+        args = self.get_args(tzname)
+
+        with self.context(tzname):
+            NYC = self.tzclass(*args)
+            UTC = tz.tzutc()
+
+            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False)
+
+            dt0 = t_n.replace(tzinfo=NYC)
+            dt1 = tz.enfold(dt0, fold=1)
+
+            # Make sure these actually represent different times
+            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
+
+            # Test that they compare equal
+            self.assertEqual(dt0, dt1)
+
+###
+# Test Cases
+class TzUTCTest(unittest.TestCase):
+    def testSingleton(self):
+        UTC_0 = tz.tzutc()
+        UTC_1 = tz.tzutc()
+
+        self.assertIs(UTC_0, UTC_1)
+
+    def testOffset(self):
+        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+
+        self.assertEqual(ct.utcoffset(), timedelta(seconds=0))
+
+    def testDst(self):
+        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+
+        self.assertEqual(ct.dst(), timedelta(seconds=0))
+
+    def testTzName(self):
+        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
+        self.assertEqual(ct.tzname(), 'UTC')
+
+    def testEquality(self):
+        UTC0 = tz.tzutc()
+        UTC1 = tz.tzutc()
+
+        self.assertEqual(UTC0, UTC1)
+
+    def testInequality(self):
+        UTC = tz.tzutc()
+        UTCp4 = tz.tzoffset('UTC+4', 14400)
+
+        self.assertNotEqual(UTC, UTCp4)
+
+    def testInequalityInteger(self):
+        self.assertFalse(tz.tzutc() == 7)
+        self.assertNotEqual(tz.tzutc(), 7)
+
+    def testInequalityUnsupported(self):
+        self.assertEqual(tz.tzutc(), ComparesEqual)
+
+    def testRepr(self):
+        UTC = tz.tzutc()
+        self.assertEqual(repr(UTC), 'tzutc()')
+
+    def testTimeOnlyUTC(self):
+        # https://github.com/dateutil/dateutil/issues/132
+        # tzutc doesn't care
+        tz_utc = tz.tzutc()
+        self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(),
+                         timedelta(0))
+
+    def testAmbiguity(self):
+        # Pick an arbitrary datetime, this should always return False.
+        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc())
+
+        self.assertFalse(tz.datetime_ambiguous(dt))
+
+
+@pytest.mark.tzoffset
+class TzOffsetTest(unittest.TestCase):
+    def testTimedeltaOffset(self):
+        est = tz.tzoffset('EST', timedelta(hours=-5))
+        est_s = tz.tzoffset('EST', -18000)
+
+        self.assertEqual(est, est_s)
+
+    def testTzNameNone(self):
+        gmt5 = tz.tzoffset(None, -18000)       # -5:00
+        self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(),
+                      None)
+
+    def testTimeOnlyOffset(self):
+        # tzoffset doesn't care
+        tz_offset = tz.tzoffset('+3', 3600)
+        self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(),
+                         timedelta(seconds=3600))
+
+    def testTzOffsetRepr(self):
+        tname = 'EST'
+        tzo = tz.tzoffset(tname, -5 * 3600)
+        self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)")
+
+    def testEquality(self):
+        utc = tz.tzoffset('UTC', 0)
+        gmt = tz.tzoffset('GMT', 0)
+
+        self.assertEqual(utc, gmt)
+
+    def testUTCEquality(self):
+        utc = tz.tzutc()
+        o_utc = tz.tzoffset('UTC', 0)
+
+        self.assertEqual(utc, o_utc)
+        self.assertEqual(o_utc, utc)
+
+    def testInequalityInvalid(self):
+        tzo = tz.tzoffset('-3', -3 * 3600)
+        self.assertFalse(tzo == -3)
+        self.assertNotEqual(tzo, -3)
+
+    def testInequalityUnsupported(self):
+        tzo = tz.tzoffset('-5', -5 * 3600)
+
+        self.assertTrue(tzo == ComparesEqual)
+        self.assertFalse(tzo != ComparesEqual)
+        self.assertEqual(tzo, ComparesEqual)
+
+    def testAmbiguity(self):
+        # Pick an arbitrary datetime, this should always return False.
+        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600))
+
+        self.assertFalse(tz.datetime_ambiguous(dt))
+
+    def testTzOffsetInstance(self):
+        tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5))
+        tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5))
+
+        assert tz1 is not tz2
+
+    def testTzOffsetSingletonDifferent(self):
+        tz1 = tz.tzoffset('EST', timedelta(hours=-5))
+        tz2 = tz.tzoffset('EST', -18000)
+
+        assert tz1 is tz2
+
+@pytest.mark.tzoffset
+@pytest.mark.parametrize('args', [
+    ('UTC', 0),
+    ('EST', -18000),
+    ('EST', timedelta(hours=-5)),
+    (None, timedelta(hours=3)),
+])
+def test_tzoffset_singleton(args):
+    tz1 = tz.tzoffset(*args)
+    tz2 = tz.tzoffset(*args)
+
+    assert tz1 is tz2
+
+@pytest.mark.tzlocal
+class TzLocalTest(unittest.TestCase):
+    def testEquality(self):
+        tz1 = tz.tzlocal()
+        tz2 = tz.tzlocal()
+
+        # Explicitly calling == and != here to ensure the operators work
+        self.assertTrue(tz1 == tz2)
+        self.assertFalse(tz1 != tz2)
+
+    def testInequalityFixedOffset(self):
+        tzl = tz.tzlocal()
+        tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds())
+        tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds())
+
+        self.assertFalse(tzl == tzos)
+        self.assertFalse(tzl == tzod)
+        self.assertTrue(tzl != tzos)
+        self.assertTrue(tzl != tzod)
+
+    def testInequalityInvalid(self):
+        tzl = tz.tzlocal()
+
+        self.assertTrue(tzl != 1)
+        self.assertFalse(tzl == 1)
+
+        # TODO: Use some sort of universal local mocking so that it's clear
+        # that we're expecting tzlocal to *not* be Pacific/Kiritimati
+        LINT = tz.gettz('Pacific/Kiritimati')
+        self.assertTrue(tzl != LINT)
+        self.assertFalse(tzl == LINT)
+
+    def testInequalityUnsupported(self):
+        tzl = tz.tzlocal()
+
+        self.assertTrue(tzl == ComparesEqual)
+        self.assertFalse(tzl != ComparesEqual)
+
+    def testRepr(self):
+        tzl = tz.tzlocal()
+
+        self.assertEqual(repr(tzl), 'tzlocal()')
+
+
+@pytest.mark.parametrize('args,kwargs', [
+    (('EST', -18000), {}),
+    (('EST', timedelta(hours=-5)), {}),
+    (('EST',), {'offset': -18000}),
+    (('EST',), {'offset': timedelta(hours=-5)}),
+    (tuple(), {'name': 'EST', 'offset': -18000})
+])
+def test_tzoffset_is(args, kwargs):
+    tz_ref = tz.tzoffset('EST', -18000)
+    assert tz.tzoffset(*args, **kwargs) is tz_ref
+
+
+def test_tzoffset_is_not():
+    assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000)
+
+
+@pytest.mark.tzlocal
+@unittest.skipIf(IS_WIN, "requires Unix")
+@unittest.skipUnless(TZEnvContext.tz_change_allowed(),
+                     TZEnvContext.tz_change_disallowed_message())
+class TzLocalNixTest(unittest.TestCase, TzFoldMixin):
+    # This is a set of tests for `tzlocal()` on *nix systems
+
+    # POSIX string indicating change to summer time on the 2nd Sunday in March
+    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
+    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
+
+    # POSIX string for AEST/AEDT (valid >= 2008)
+    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
+
+    # POSIX string for BST/GMT
+    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
+    # POSIX string for UTC
+    UTC = 'UTC'
+
+    def gettz(self, tzname):
+        # Actual time zone changes are handled by the _gettz_context function
+        return tz.tzlocal()
+
+    def _gettz_context(self, tzname):
+        tzname_map = {'Australia/Sydney': self.TZ_AEST,
+                      'America/Toronto': self.TZ_EST,
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
+
+        return TZEnvContext(tzname_map.get(tzname, tzname))
+
+    def _testTzFunc(self, tzval, func, std_val, dst_val):
+        """
+        This generates tests about how the behavior of a function ``func``
+        changes between STD and DST (e.g. utcoffset, tzname, dst).
+
+        It assume that DST starts the 2nd Sunday in March and ends the 1st
+        Sunday in November
+        """
+        with TZEnvContext(tzval):
+            dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal())  # STD
+            dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal())  # DST
+
+            self.assertEqual(func(dt1), std_val)
+            self.assertEqual(func(dt2), dst_val)
+
+    def _testTzName(self, tzval, std_name, dst_name):
+        func = datetime.tzname
+
+        self._testTzFunc(tzval, func, std_name, dst_name)
+
+    def testTzNameDST(self):
+        # Test tzname in a zone with DST
+        self._testTzName(self.TZ_EST, 'EST', 'EDT')
+
+    def testTzNameUTC(self):
+        # Test tzname in a zone without DST
+        self._testTzName(self.UTC, 'UTC', 'UTC')
+
+    def _testOffset(self, tzval, std_off, dst_off):
+        func = datetime.utcoffset
+
+        self._testTzFunc(tzval, func, std_off, dst_off)
+
+    def testOffsetDST(self):
+        self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4))
+
+    def testOffsetUTC(self):
+        self._testOffset(self.UTC, timedelta(0), timedelta(0))
+
+    def _testDST(self, tzval, dst_dst):
+        func = datetime.dst
+        std_dst = timedelta(0)
+
+        self._testTzFunc(tzval, func, std_dst, dst_dst)
+
+    def testDSTDST(self):
+        self._testDST(self.TZ_EST, timedelta(hours=1))
+
+    def testDSTUTC(self):
+        self._testDST(self.UTC, timedelta(0))
+
+    def testTimeOnlyOffsetLocalUTC(self):
+        with TZEnvContext(self.UTC):
+            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
+                             timedelta(0))
+
+    def testTimeOnlyOffsetLocalDST(self):
+        with TZEnvContext(self.TZ_EST):
+            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
+                          None)
+
+    def testTimeOnlyDSTLocalUTC(self):
+        with TZEnvContext(self.UTC):
+            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
+                             timedelta(0))
+
+    def testTimeOnlyDSTLocalDST(self):
+        with TZEnvContext(self.TZ_EST):
+            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
+                          None)
+
+    def testUTCEquality(self):
+        with TZEnvContext(self.UTC):
+            assert tz.tzlocal() == tz.tzutc()
+
+
+# TODO: Maybe a better hack than this?
+def mark_tzlocal_nix(f):
+    marks = [
+        pytest.mark.tzlocal,
+        pytest.mark.skipif(IS_WIN, reason='requires Unix'),
+        pytest.mark.skipif(not TZEnvContext.tz_change_allowed,
+                           reason=TZEnvContext.tz_change_disallowed_message())
+    ]
+
+    for mark in reversed(marks):
+        f = mark(f)
+
+    return f
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0'])
+def test_tzlocal_utc_equal(tzvar):
+    with TZEnvContext(tzvar):
+        assert tz.tzlocal() == tz.UTC
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar', [
+    'Europe/London', 'America/New_York',
+    'GMT0BST', 'EST5EDT'])
+def test_tzlocal_utc_unequal(tzvar):
+    with TZEnvContext(tzvar):
+        assert tz.tzlocal() != tz.UTC
+
+
+@mark_tzlocal_nix
+def test_tzlocal_local_time_trim_colon():
+    with TZEnvContext(':/etc/localtime'):
+        assert tz.gettz() is not None
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar, tzoff', [
+    ('EST5', tz.tzoffset('EST', -18000)),
+    ('GMT', tz.tzoffset('GMT', 0)),
+    ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))),
+    ('JST-9', tz.tzoffset('JST', timedelta(hours=9))),
+])
+def test_tzlocal_offset_equal(tzvar, tzoff):
+    with TZEnvContext(tzvar):
+        # Including both to test both __eq__ and __ne__
+        assert tz.tzlocal() == tzoff
+        assert not (tz.tzlocal() != tzoff)
+
+
+@mark_tzlocal_nix
+@pytest.mark.parametrize('tzvar, tzoff', [
+    ('EST5EDT', tz.tzoffset('EST', -18000)),
+    ('GMT0BST', tz.tzoffset('GMT', 0)),
+    ('EST5', tz.tzoffset('EST', -14400)),
+    ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))),
+    ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))),
+])
+def test_tzlocal_offset_unequal(tzvar, tzoff):
+    with TZEnvContext(tzvar):
+        # Including both to test both __eq__ and __ne__
+        assert tz.tzlocal() != tzoff
+        assert not (tz.tzlocal() == tzoff)
+
+
+@pytest.mark.gettz
+class GettzTest(unittest.TestCase, TzFoldMixin):
+    gettz = staticmethod(tz.gettz)
+
+    def testGettz(self):
+        # bug 892569
+        str(self.gettz('UTC'))
+
+    def testGetTzEquality(self):
+        self.assertEqual(self.gettz('UTC'), self.gettz('UTC'))
+
+    def testTimeOnlyGettz(self):
+        # gettz returns None
+        tz_get = self.gettz('Europe/Minsk')
+        self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None)
+
+    def testTimeOnlyGettzDST(self):
+        # gettz returns None
+        tz_get = self.gettz('Europe/Minsk')
+        self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None)
+
+    def testTimeOnlyGettzTzName(self):
+        tz_get = self.gettz('Europe/Minsk')
+        self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None)
+
+    def testTimeOnlyFormatZ(self):
+        tz_get = self.gettz('Europe/Minsk')
+        t = dt_time(13, 20, tzinfo=tz_get)
+
+        self.assertEqual(t.strftime('%H%M%Z'), '1320')
+
+    def testPortugalDST(self):
+        # In 1996, Portugal changed from CET to WET
+        PORTUGAL = self.gettz('Portugal')
+
+        t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL)
+
+        self.assertEqual(t_cet.tzname(), 'CET')
+        self.assertEqual(t_cet.utcoffset(), timedelta(hours=1))
+        self.assertEqual(t_cet.dst(), timedelta(0))
+
+        t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL)
+
+        self.assertEqual(t_west.tzname(), 'WEST')
+        self.assertEqual(t_west.utcoffset(), timedelta(hours=1))
+        self.assertEqual(t_west.dst(), timedelta(hours=1))
+
+    def testGettzCacheTzFile(self):
+        NYC1 = tz.gettz('America/New_York')
+        NYC2 = tz.gettz('America/New_York')
+
+        assert NYC1 is NYC2
+
+    def testGettzCacheTzLocal(self):
+        local1 = tz.gettz()
+        local2 = tz.gettz()
+
+        assert local1 is not local2
+
+@pytest.mark.gettz
+@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
+def test_gettz_cache_clear():
+    NYC1 = tz.gettz('America/New_York')
+    tz.gettz.cache_clear()
+
+    NYC2 = tz.gettz('America/New_York')
+
+    assert NYC1 is not NYC2
+
+
+class ZoneInfoGettzTest(GettzTest, WarningTestMixin):
+    def gettz(self, name):
+        zoneinfo_file = zoneinfo.get_zonefile_instance()
+        return zoneinfo_file.get(name)
+
+    def testZoneInfoFileStart1(self):
+        tz = self.gettz("EST5EDT")
+        self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST",
+                         MISSING_TARBALL)
+        self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT")
+
+    def testZoneInfoFileEnd1(self):
+        tzc = self.gettz("EST5EDT")
+        self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
+                         "EDT", MISSING_TARBALL)
+
+        end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1)
+        self.assertEqual(end_est.tzname(), "EST")
+
+    def testZoneInfoOffsetSignal(self):
+        utc = self.gettz("UTC")
+        nyc = self.gettz("America/New_York")
+        self.assertNotEqual(utc, None, MISSING_TARBALL)
+        self.assertNotEqual(nyc, None)
+        t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc)
+        t1 = t0.astimezone(utc)
+        t2 = t1.astimezone(nyc)
+        self.assertEqual(t0, t2)
+        self.assertEqual(nyc.dst(t0), timedelta(hours=1))
+
+    def testZoneInfoCopy(self):
+        # copy.copy() called on a ZoneInfo file was returning the same instance
+        CHI = self.gettz('America/Chicago')
+        CHI_COPY = copy.copy(CHI)
+
+        self.assertIsNot(CHI, CHI_COPY)
+        self.assertEqual(CHI, CHI_COPY)
+
+    def testZoneInfoDeepCopy(self):
+        CHI = self.gettz('America/Chicago')
+        CHI_COPY = copy.deepcopy(CHI)
+
+        self.assertIsNot(CHI, CHI_COPY)
+        self.assertEqual(CHI, CHI_COPY)
+
+    def testZoneInfoInstanceCaching(self):
+        zif_0 = zoneinfo.get_zonefile_instance()
+        zif_1 = zoneinfo.get_zonefile_instance()
+
+        self.assertIs(zif_0, zif_1)
+
+    def testZoneInfoNewInstance(self):
+        zif_0 = zoneinfo.get_zonefile_instance()
+        zif_1 = zoneinfo.get_zonefile_instance(new_instance=True)
+        zif_2 = zoneinfo.get_zonefile_instance()
+
+        self.assertIsNot(zif_0, zif_1)
+        self.assertIs(zif_1, zif_2)
+
+    def testZoneInfoDeprecated(self):
+        with self.assertWarns(DeprecationWarning):
+            zoneinfo.gettz('US/Eastern')
+
+    def testZoneInfoMetadataDeprecated(self):
+        with self.assertWarns(DeprecationWarning):
+            zoneinfo.gettz_db_metadata()
+
+
+class TZRangeTest(unittest.TestCase, TzFoldMixin):
+    TZ_EST = tz.tzrange('EST', timedelta(hours=-5),
+                        'EDT', timedelta(hours=-4),
+                        start=relativedelta(month=3, day=1, hour=2,
+                                            weekday=SU(+2)),
+                        end=relativedelta(month=11, day=1, hour=1,
+                                          weekday=SU(+1)))
+
+    TZ_AEST = tz.tzrange('AEST', timedelta(hours=10),
+                         'AEDT', timedelta(hours=11),
+                         start=relativedelta(month=10, day=1, hour=2,
+                                             weekday=SU(+1)),
+                         end=relativedelta(month=4, day=1, hour=2,
+                                           weekday=SU(+1)))
+
+    TZ_LON = tz.tzrange('GMT', timedelta(hours=0),
+                        'BST', timedelta(hours=1),
+                        start=relativedelta(month=3, day=31, weekday=SU(-1),
+                                            hours=2),
+                        end=relativedelta(month=10, day=31, weekday=SU(-1),
+                                          hours=1))
+    # POSIX string for UTC
+    UTC = 'UTC'
+
+    def gettz(self, tzname):
+        tzname_map = {'Australia/Sydney': self.TZ_AEST,
+                      'America/Toronto': self.TZ_EST,
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
+
+        return tzname_map[tzname]
+
+    def testRangeCmp1(self):
+        self.assertEqual(tz.tzstr("EST5EDT"),
+                         tz.tzrange("EST", -18000, "EDT", -14400,
+                                 relativedelta(hours=+2,
+                                               month=4, day=1,
+                                               weekday=SU(+1)),
+                                 relativedelta(hours=+1,
+                                               month=10, day=31,
+                                               weekday=SU(-1))))
+
+    def testRangeCmp2(self):
+        self.assertEqual(tz.tzstr("EST5EDT"),
+                         tz.tzrange("EST", -18000, "EDT"))
+
+    def testRangeOffsets(self):
+        TZR = tz.tzrange('EST', -18000, 'EDT', -14400,
+                         start=relativedelta(hours=2, month=4, day=1,
+                                             weekday=SU(+2)),
+                         end=relativedelta(hours=1, month=10, day=31,
+                                           weekday=SU(-1)))
+
+        dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR)  # STD
+        dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR)  # DST
+
+        dst_zero = timedelta(0)
+        dst_hour = timedelta(hours=1)
+
+        std_offset = timedelta(hours=-5)
+        dst_offset = timedelta(hours=-4)
+
+        # Check dst()
+        self.assertEqual(dt_std.dst(), dst_zero)
+        self.assertEqual(dt_dst.dst(), dst_hour)
+
+        # Check utcoffset()
+        self.assertEqual(dt_std.utcoffset(), std_offset)
+        self.assertEqual(dt_dst.utcoffset(), dst_offset)
+
+        # Check tzname
+        self.assertEqual(dt_std.tzname(), 'EST')
+        self.assertEqual(dt_dst.tzname(), 'EDT')
+
+    def testTimeOnlyRangeFixed(self):
+        # This is a fixed-offset zone, so tzrange allows this
+        tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3))
+        self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(),
+                         timedelta(hours=-3))
+
+    def testTimeOnlyRange(self):
+        # tzrange returns None because this zone has DST
+        tz_range = tz.tzrange('EST', timedelta(hours=-5),
+                              'EDT', timedelta(hours=-4))
+        self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None)
+
+    def testBrokenIsDstHandling(self):
+        # tzrange._isdst() was using a date() rather than a datetime().
+        # Issue reported by Lennart Regebro.
+        dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc())
+        self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")),
+                          datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
+
+    def testRangeTimeDelta(self):
+        # Test that tzrange can be specified with a timedelta instead of an int.
+        EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5),
+                                'EDT', timedelta(hours=-4))
+
+        EST5EDT_sec = tz.tzrange('EST', -18000,
+                                 'EDT', -14400)
+
+        self.assertEqual(EST5EDT_td, EST5EDT_sec)
+
+    def testRangeEquality(self):
+        TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400)
+
+        # Standard abbreviation different
+        TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400)
+        self.assertNotEqual(TZR1, TZR2)
+
+        # DST abbreviation different
+        TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400)
+        self.assertNotEqual(TZR1, TZR3)
+
+        # STD offset different
+        TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400)
+        self.assertNotEqual(TZR1, TZR4)
+
+        # DST offset different
+        TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000)
+        self.assertNotEqual(TZR1, TZR5)
+
+        # Start delta different
+        TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400,
+                          start=relativedelta(hours=+1, month=3,
+                                              day=1, weekday=SU(+2)))
+        self.assertNotEqual(TZR1, TZR6)
+
+        # End delta different
+        TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400,
+            end=relativedelta(hours=+1, month=11,
+                              day=1, weekday=SU(+2)))
+        self.assertNotEqual(TZR1, TZR7)
+
+    def testRangeInequalityUnsupported(self):
+        TZR = tz.tzrange('EST', -18000, 'EDT', -14400)
+
+        self.assertFalse(TZR == 4)
+        self.assertTrue(TZR == ComparesEqual)
+        self.assertFalse(TZR != ComparesEqual)
+
+
+@pytest.mark.tzstr
+class TZStrTest(unittest.TestCase, TzFoldMixin):
+    # POSIX string indicating change to summer time on the 2nd Sunday in March
+    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
+    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
+
+    # POSIX string for AEST/AEDT (valid >= 2008)
+    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
+
+    # POSIX string for GMT/BST
+    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
+    def gettz(self, tzname):
+        # Actual time zone changes are handled by the _gettz_context function
+        tzname_map = {'Australia/Sydney': self.TZ_AEST,
+                      'America/Toronto': self.TZ_EST,
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
+
+        return tz.tzstr(tzname_map[tzname])
+
+    def testStrStr(self):
+        # Test that tz.tzstr() won't throw an error if given a str instead
+        # of a unicode literal.
+        self.assertEqual(datetime(2003, 4, 6, 1, 59,
+                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST")
+        self.assertEqual(datetime(2003, 4, 6, 2, 00,
+                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT")
+
+    def testStrInequality(self):
+        TZS1 = tz.tzstr('EST5EDT4')
+
+        # Standard abbreviation different
+        TZS2 = tz.tzstr('ET5EDT4')
+        self.assertNotEqual(TZS1, TZS2)
+
+        # DST abbreviation different
+        TZS3 = tz.tzstr('EST5EMT')
+        self.assertNotEqual(TZS1, TZS3)
+
+        # STD offset different
+        TZS4 = tz.tzstr('EST4EDT4')
+        self.assertNotEqual(TZS1, TZS4)
+
+        # DST offset different
+        TZS5 = tz.tzstr('EST5EDT3')
+        self.assertNotEqual(TZS1, TZS5)
+
+    def testStrInequalityStartEnd(self):
+        TZS1 = tz.tzstr('EST5EDT4')
+
+        # Start delta different
+        TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00')
+        self.assertNotEqual(TZS1, TZS2)
+
+        # End delta different
+        TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00')
+        self.assertNotEqual(TZS1, TZS3)
+
+    def testPosixOffset(self):
+        TZ1 = tz.tzstr('UTC-3')
+        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(),
+                         timedelta(hours=-3))
+
+        TZ2 = tz.tzstr('UTC-3', posix_offset=True)
+        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(),
+                         timedelta(hours=+3))
+
+    def testStrInequalityUnsupported(self):
+        TZS = tz.tzstr('EST5EDT')
+
+        self.assertFalse(TZS == 4)
+        self.assertTrue(TZS == ComparesEqual)
+        self.assertFalse(TZS != ComparesEqual)
+
+    def testTzStrRepr(self):
+        TZS1 = tz.tzstr('EST5EDT4')
+        TZS2 = tz.tzstr('EST')
+
+        self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")")
+        self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")")
+
+    def testTzStrFailure(self):
+        with self.assertRaises(ValueError):
+            tz.tzstr('InvalidString;439999')
+
+    def testTzStrSingleton(self):
+        tz1 = tz.tzstr('EST5EDT')
+        tz2 = tz.tzstr('CST4CST')
+        tz3 = tz.tzstr('EST5EDT')
+
+        self.assertIsNot(tz1, tz2)
+        self.assertIs(tz1, tz3)
+
+    def testTzStrSingletonPosix(self):
+        tz_t1 = tz.tzstr('GMT+3', posix_offset=True)
+        tz_f1 = tz.tzstr('GMT+3', posix_offset=False)
+
+        tz_t2 = tz.tzstr('GMT+3', posix_offset=True)
+        tz_f2 = tz.tzstr('GMT+3', posix_offset=False)
+
+        self.assertIs(tz_t1, tz_t2)
+        self.assertIsNot(tz_t1, tz_f1)
+
+        self.assertIs(tz_f1, tz_f2)
+
+    def testTzStrInstance(self):
+        tz1 = tz.tzstr('EST5EDT')
+        tz2 = tz.tzstr.instance('EST5EDT')
+        tz3 = tz.tzstr.instance('EST5EDT')
+
+        assert tz1 is not tz2
+        assert tz2 is not tz3
+
+        # Ensure that these still are all the same zone
+        assert tz1 == tz2 == tz3
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str,expected', [
+    # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+    ('', tz.tzrange(None)),     # TODO: Should change this so tz.tzrange('') works
+    ('EST+5EDT,M3.2.0/2,M11.1.0/12',
+     tz.tzrange('EST', -18000, 'EDT', -14400,
+        start=relativedelta(month=3, day=1, weekday=SU(2), hours=2),
+        end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))),
+    ('WART4WARST,J1/0,J365/25',  # This is DST all year, Western Argentina Summer Time
+     tz.tzrange('WART', timedelta(hours=-4), 'WARST',
+        start=relativedelta(month=1, day=1, hours=0),
+        end=relativedelta(month=12, day=31, days=1))),
+    ('IST-2IDT,M3.4.4/26,M10.5.0',      # Israel Standard / Daylight Time
+     tz.tzrange('IST', timedelta(hours=2), 'IDT',
+        start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2),
+        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))),
+    ('WGT3WGST,M3.5.0/2,M10.5.0/1',
+     tz.tzrange('WGT', timedelta(hours=-3), 'WGST',
+        start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2),
+        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))),
+
+    # Different offset specifications
+    ('WGT0300WGST',
+     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
+    ('WGT03:00WGST',
+     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
+    ('AEST-1100AEDT',
+     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
+    ('AEST-11:00AEDT',
+     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
+
+    # Different time formats
+    ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+    ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+    ('EST5EDT,M3.2.0/0400,M11.1.0/0300',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
+        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
+])
+def test_valid_GNU_tzstr(tz_str, expected):
+    tzi = tz.tzstr(tz_str)
+
+    assert tzi == expected
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str, expected', [
+    ('EST5EDT,5,4,0,7200,11,3,0,7200',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2),
+        end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))),
+    ('EST5EDT,5,-4,0,7200,11,3,0,7200',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)),
+        end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6),
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3),
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))),
+    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600',
+     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
+        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
+        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
+])
+def test_valid_dateutil_format(tz_str, expected):
+    # This tests the dateutil-specific format that is used widely in the tests
+    # and examples. It is unclear where this format originated from.
+    with pytest.warns(tz.DeprecatedTzFormatWarning):
+        tzi = tz.tzstr.instance(tz_str)
+
+    assert tzi == expected
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', [
+    'hdfiughdfuig,dfughdfuigpu87ñ::',
+    ',dfughdfuigpu87ñ::',
+    '-1:WART4WARST,J1,J365/25',
+    'WART4WARST,J1,J365/-25',
+    'IST-2IDT,M3.4.-1/26,M10.5.0',
+    'IST-2IDT,M3,2000,1/26,M10,5,0'
+])
+def test_invalid_GNU_tzstr(tz_str):
+    with pytest.raises(ValueError):
+        tz.tzstr(tz_str)
+
+
+# Different representations of the same default rule set
+DEFAULT_TZSTR_RULES_EQUIV_2003 = [
+    'EST5EDT',
+    'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00',
+    'EST5EDT4,95/02:00:00,298/02:00',
+    'EST5EDT4,J96/02:00:00,J299/02:00',
+    'EST5EDT4,J96/02:00:00,J299/02'
+]
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
+def test_tzstr_default_start(tz_str):
+    tzi = tz.tzstr(tz_str)
+    dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi)
+    dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi)
+
+    assert get_timezone_tuple(dt_std) == EST_TUPLE
+    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
+def test_tzstr_default_end(tz_str):
+    tzi = tz.tzstr(tz_str)
+    dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi)
+    dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi)
+    dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1)
+    dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi)
+
+    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
+    assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE
+    assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE
+    assert get_timezone_tuple(dt_std) == EST_TUPLE
+
+
+@pytest.mark.tzstr
+@pytest.mark.parametrize('tzstr_1', ['EST5EDT',
+                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
+@pytest.mark.parametrize('tzstr_2', ['EST5EDT',
+                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
+def test_tzstr_default_cmp(tzstr_1, tzstr_2):
+    tz1 = tz.tzstr(tzstr_1)
+    tz2 = tz.tzstr(tzstr_2)
+
+    assert tz1 == tz2
+
+class TZICalTest(unittest.TestCase, TzFoldMixin):
+    def _gettz_str_tuple(self, tzname):
+        TZ_EST = (
+            'BEGIN:VTIMEZONE',
+            'TZID:US-Eastern',
+            'BEGIN:STANDARD',
+            'DTSTART:19971029T020000',
+            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
+            'TZOFFSETFROM:-0400',
+            'TZOFFSETTO:-0500',
+            'TZNAME:EST',
+            'END:STANDARD',
+            'BEGIN:DAYLIGHT',
+            'DTSTART:19980301T020000',
+            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
+            'TZOFFSETFROM:-0500',
+            'TZOFFSETTO:-0400',
+            'TZNAME:EDT',
+            'END:DAYLIGHT',
+            'END:VTIMEZONE'
+        )
+
+        TZ_PST = (
+            'BEGIN:VTIMEZONE',
+            'TZID:US-Pacific',
+            'BEGIN:STANDARD',
+            'DTSTART:19971029T020000',
+            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
+            'TZOFFSETFROM:-0700',
+            'TZOFFSETTO:-0800',
+            'TZNAME:PST',
+            'END:STANDARD',
+            'BEGIN:DAYLIGHT',
+            'DTSTART:19980301T020000',
+            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
+            'TZOFFSETFROM:-0800',
+            'TZOFFSETTO:-0700',
+            'TZNAME:PDT',
+            'END:DAYLIGHT',
+            'END:VTIMEZONE'
+        )
+
+        TZ_AEST = (
+            'BEGIN:VTIMEZONE',
+            'TZID:Australia-Sydney',
+            'BEGIN:STANDARD',
+            'DTSTART:19980301T030000',
+            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04',
+            'TZOFFSETFROM:+1100',
+            'TZOFFSETTO:+1000',
+            'TZNAME:AEST',
+            'END:STANDARD',
+            'BEGIN:DAYLIGHT',
+            'DTSTART:19971029T020000',
+            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10',
+            'TZOFFSETFROM:+1000',
+            'TZOFFSETTO:+1100',
+            'TZNAME:AEDT',
+            'END:DAYLIGHT',
+            'END:VTIMEZONE'
+        )
+
+        TZ_LON = (
+            'BEGIN:VTIMEZONE',
+            'TZID:Europe-London',
+            'BEGIN:STANDARD',
+            'DTSTART:19810301T030000',
+            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02',
+            'TZOFFSETFROM:+0100',
+            'TZOFFSETTO:+0000',
+            'TZNAME:GMT',
+            'END:STANDARD',
+            'BEGIN:DAYLIGHT',
+            'DTSTART:19961001T030000',
+            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01',
+            'TZOFFSETFROM:+0000',
+            'TZOFFSETTO:+0100',
+            'TZNAME:BST',
+            'END:DAYLIGHT',
+            'END:VTIMEZONE'
+        )
+
+        tzname_map = {'Australia/Sydney': TZ_AEST,
+                      'America/Toronto': TZ_EST,
+                      'America/New_York': TZ_EST,
+                      'America/Los_Angeles': TZ_PST,
+                      'Europe/London': TZ_LON}
+
+        return tzname_map[tzname]
+
+    def _gettz_str(self, tzname):
+        return '\n'.join(self._gettz_str_tuple(tzname))
+
+    def _tzstr_dtstart_with_params(self, tzname, param_str):
+        # Adds parameters to the DTSTART values of a given tzstr
+        tz_str_tuple = self._gettz_str_tuple(tzname)
+
+        out_tz = []
+        for line in tz_str_tuple:
+            if line.startswith('DTSTART'):
+                name, value = line.split(':', 1)
+                line = name + ';' + param_str + ':' + value
+
+            out_tz.append(line)
+
+        return '\n'.join(out_tz)
+
+    def gettz(self, tzname):
+        tz_str = self._gettz_str(tzname)
+
+        tzc = tz.tzical(StringIO(tz_str)).get()
+
+        return tzc
+
+    def testRepr(self):
+        instr = StringIO(TZICAL_PST8PDT)
+        instr.name = 'StringIO(PST8PDT)'
+        tzc = tz.tzical(instr)
+
+        self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")")
+
+    # Test performance
+    def _test_us_zone(self, tzc, func, values, start):
+        if start:
+            dt1 = datetime(2003, 3, 9, 1, 59)
+            dt2 = datetime(2003, 3, 9, 2, 00)
+            fold = [0, 0]
+        else:
+            dt1 = datetime(2003, 11, 2, 0, 59)
+            dt2 = datetime(2003, 11, 2, 1, 00)
+            fold = [0, 1]
+
+        dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f)
+               for dt, f in zip((dt1, dt2), fold))
+
+        for value, dt in zip(values, dts):
+            self.assertEqual(func(dt), value)
+
+    def _test_multi_zones(self, tzstrs, tzids, func, values, start):
+        tzic = tz.tzical(StringIO('\n'.join(tzstrs)))
+        for tzid, vals in zip(tzids, values):
+            tzc = tzic.get(tzid)
+
+            self._test_us_zone(tzc, func, vals, start)
+
+    def _prepare_EST(self):
+        tz_str = self._gettz_str('America/New_York')
+        return tz.tzical(StringIO(tz_str)).get()
+
+    def _testEST(self, start, test_type, tzc=None):
+        if tzc is None:
+            tzc = self._prepare_EST()
+
+        argdict = {
+            'name':   (datetime.tzname, ('EST', 'EDT')),
+            'offset': (datetime.utcoffset, (timedelta(hours=-5),
+                                            timedelta(hours=-4))),
+            'dst':    (datetime.dst, (timedelta(hours=0),
+                                      timedelta(hours=1)))
+        }
+
+        func, values = argdict[test_type]
+
+        if not start:
+            values = reversed(values)
+
+        self._test_us_zone(tzc, func, values, start=start)
+
+    def testESTStartName(self):
+        self._testEST(start=True, test_type='name')
+
+    def testESTEndName(self):
+        self._testEST(start=False, test_type='name')
+
+    def testESTStartOffset(self):
+        self._testEST(start=True, test_type='offset')
+
+    def testESTEndOffset(self):
+        self._testEST(start=False, test_type='offset')
+
+    def testESTStartDST(self):
+        self._testEST(start=True, test_type='dst')
+
+    def testESTEndDST(self):
+        self._testEST(start=False, test_type='dst')
+
+    def testESTValueDatetime(self):
+        # Violating one-test-per-test rule because we're not set up to do
+        # parameterized tests and the manual proliferation is getting a bit
+        # out of hand.
+        tz_str = self._tzstr_dtstart_with_params('America/New_York',
+                                                 'VALUE=DATE-TIME')
+
+        tzc = tz.tzical(StringIO(tz_str)).get()
+
+        for start in (True, False):
+            for test_type in ('name', 'offset', 'dst'):
+                self._testEST(start=start, test_type=test_type, tzc=tzc)
+
+    def _testMultizone(self, start, test_type):
+        tzstrs = (self._gettz_str('America/New_York'),
+                  self._gettz_str('America/Los_Angeles'))
+        tzids = ('US-Eastern', 'US-Pacific')
+
+        argdict = {
+            'name':   (datetime.tzname, (('EST', 'EDT'),
+                                         ('PST', 'PDT'))),
+            'offset': (datetime.utcoffset, ((timedelta(hours=-5),
+                                             timedelta(hours=-4)),
+                                            (timedelta(hours=-8),
+                                             timedelta(hours=-7)))),
+            'dst':    (datetime.dst, ((timedelta(hours=0),
+                                       timedelta(hours=1)),
+                                      (timedelta(hours=0),
+                                       timedelta(hours=1))))
+        }
+
+        func, values = argdict[test_type]
+
+        if not start:
+            values = map(reversed, values)
+
+        self._test_multi_zones(tzstrs, tzids, func, values, start)
+
+    def testMultiZoneStartName(self):
+        self._testMultizone(start=True, test_type='name')
+
+    def testMultiZoneEndName(self):
+        self._testMultizone(start=False, test_type='name')
+
+    def testMultiZoneStartOffset(self):
+        self._testMultizone(start=True, test_type='offset')
+
+    def testMultiZoneEndOffset(self):
+        self._testMultizone(start=False, test_type='offset')
+
+    def testMultiZoneStartDST(self):
+        self._testMultizone(start=True, test_type='dst')
+
+    def testMultiZoneEndDST(self):
+        self._testMultizone(start=False, test_type='dst')
+
+    def testMultiZoneKeys(self):
+        est_str = self._gettz_str('America/New_York')
+        pst_str = self._gettz_str('America/Los_Angeles')
+        tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str))))
+
+        # Sort keys because they are in a random order, being dictionary keys
+        keys = sorted(tzic.keys())
+
+        self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
+
+    # Test error conditions
+    def testEmptyString(self):
+        with self.assertRaises(ValueError):
+            tz.tzical(StringIO(""))
+
+    def testMultiZoneGet(self):
+        tzic = tz.tzical(StringIO(TZICAL_EST5EDT + TZICAL_PST8PDT))
+
+        with self.assertRaises(ValueError):
+            tzic.get()
+
+    def testDtstartDate(self):
+        tz_str = self._tzstr_dtstart_with_params('America/New_York',
+                                                 'VALUE=DATE')
+        with self.assertRaises(ValueError):
+            tz.tzical(StringIO(tz_str))
+
+    def testDtstartTzid(self):
+        tz_str = self._tzstr_dtstart_with_params('America/New_York',
+                                                 'TZID=UTC')
+        with self.assertRaises(ValueError):
+            tz.tzical(StringIO(tz_str))
+
+    def testDtstartBadParam(self):
+        tz_str = self._tzstr_dtstart_with_params('America/New_York',
+                                                 'FOO=BAR')
+        with self.assertRaises(ValueError):
+            tz.tzical(StringIO(tz_str))
+
+    # Test Parsing
+    def testGap(self):
+        tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT))))
+
+        keys = sorted(tzic.keys())
+        self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
+
+
+class TZTest(unittest.TestCase):
+    def testFileStart1(self):
+        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+        self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST")
+        self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT")
+
+    def testFileEnd1(self):
+        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+        self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
+                         "EDT")
+        end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc))
+        self.assertEqual(end_est.tzname(), "EST")
+
+    def testFileLastTransition(self):
+        # After the last transition, it goes to standard time in perpetuity
+        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+        self.assertEqual(datetime(2037, 10, 25, 0, 59, tzinfo=tzc).tzname(),
+                         "EDT")
+
+        last_date = tz.enfold(datetime(2037, 10, 25, 1, 00, tzinfo=tzc), fold=1)
+        self.assertEqual(last_date.tzname(),
+                         "EST")
+
+        self.assertEqual(datetime(2038, 5, 25, 12, 0, tzinfo=tzc).tzname(),
+                         "EST")
+
+    def testInvalidFile(self):
+        # Should throw a ValueError if an invalid file is passed
+        with self.assertRaises(ValueError):
+            tz.tzfile(BytesIO(b'BadFile'))
+
+    def testFilestreamWithNameRepr(self):
+        # If fileobj is a filestream with a "name" attribute this name should
+        # be reflected in the tz object's repr
+        fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT))
+        fileobj.name = 'foo'
+        tzc = tz.tzfile(fileobj)
+        self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')')
+
+    def testRoundNonFullMinutes(self):
+        # This timezone has an offset of 5992 seconds in 1900-01-01.
+        tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+        self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)),
+                             "1900-01-01 00:00:00+01:40")
+
+    def testLeapCountDecodesProperly(self):
+        # This timezone has leapcnt, and failed to decode until
+        # Eugene Oden notified about the issue.
+
+        # As leap information is currently unused (and unstored) by tzfile() we
+        # can only indirectly test this: Take advantage of tzfile() not closing
+        # the input file if handed in as an opened file and assert that the
+        # full file content has been read by tzfile(). Note: For this test to
+        # work NEW_YORK must be in TZif version 1 format i.e. no more data
+        # after TZif v1 header + data has been read
+        fileobj = BytesIO(base64.b64decode(NEW_YORK))
+        tz.tzfile(fileobj)
+        # we expect no remaining file content now, i.e. zero-length; if there's
+        # still data we haven't read the file format correctly
+        remaining_tzfile_content = fileobj.read()
+        self.assertEqual(len(remaining_tzfile_content), 0)
+
+    def testIsStd(self):
+        # NEW_YORK tzfile contains this isstd information:
+        isstd_expected = (0, 0, 0, 1)
+        tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
+        # gather the actual information as parsed by the tzfile class
+        isstd = []
+        for ttinfo in tzc._ttinfo_list:
+            # ttinfo objects contain boolean values
+            isstd.append(int(ttinfo.isstd))
+        # ttinfo list may contain more entries than isstd file content
+        isstd = tuple(isstd[:len(isstd_expected)])
+        self.assertEqual(
+            isstd_expected, isstd,
+            "isstd UTC/local indicators parsed: %s != tzfile contents: %s"
+            % (isstd, isstd_expected))
+
+    def testGMTHasNoDaylight(self):
+        # tz.tzstr("GMT+2") improperly considered daylight saving time.
+        # Issue reported by Lennart Regebro.
+        dt = datetime(2007, 8, 6, 4, 10)
+        self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0))
+
+    def testGMTOffset(self):
+        # GMT and UTC offsets have inverted signal when compared to the
+        # usual TZ variable handling.
+        dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc())
+        self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")),
+                          datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
+        self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")),
+                          datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2")))
+
+    @unittest.skipIf(IS_WIN, "requires Unix")
+    @unittest.skipUnless(TZEnvContext.tz_change_allowed(),
+                         TZEnvContext.tz_change_disallowed_message())
+    def testTZSetDoesntCorrupt(self):
+        # if we start in non-UTC then tzset UTC make sure parse doesn't get
+        # confused
+        with TZEnvContext('UTC'):
+            # this should parse to UTC timezone not the original timezone
+            dt = parse('2014-07-20T12:34:56+00:00')
+            self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00')
+
+
+@unittest.skipUnless(IS_WIN, "Requires Windows")
+class TzWinTest(unittest.TestCase, TzWinFoldMixin):
+    def setUp(self):
+        self.tzclass = tzwin.tzwin
+
+    def testTzResLoadName(self):
+        # This may not work right on non-US locales.
+        tzr = tzwin.tzres()
+        self.assertEqual(tzr.load_name(112), "Eastern Standard Time")
+
+    def testTzResNameFromString(self):
+        tzr = tzwin.tzres()
+        self.assertEqual(tzr.name_from_string('@tzres.dll,-221'),
+                         'Alaskan Daylight Time')
+
+        self.assertEqual(tzr.name_from_string('Samoa Daylight Time'),
+                         'Samoa Daylight Time')
+
+        with self.assertRaises(ValueError):
+            tzr.name_from_string('@tzres.dll,100')
+
+    def testIsdstZoneWithNoDaylightSaving(self):
+        tz = tzwin.tzwin("UTC")
+        dt = parse("2013-03-06 19:08:15")
+        self.assertFalse(tz._isdst(dt))
+
+    def testOffset(self):
+        tz = tzwin.tzwin("Cape Verde Standard Time")
+        self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)),
+                         timedelta(-1, 82800))
+
+    def testTzwinName(self):
+        # https://github.com/dateutil/dateutil/issues/143
+        tw = tz.tzwin('Eastern Standard Time')
+
+        # Cover the transitions for at least two years.
+        ESTs = 'Eastern Standard Time'
+        EDTs = 'Eastern Daylight Time'
+        transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
+                            (datetime(2015, 3, 8, 3, 1), EDTs),
+                            (datetime(2015, 11, 1, 0, 59), EDTs),
+                            (datetime(2015, 11, 1, 3, 1), ESTs),
+                            (datetime(2016, 3, 13, 0, 59), ESTs),
+                            (datetime(2016, 3, 13, 3, 1), EDTs),
+                            (datetime(2016, 11, 6, 0, 59), EDTs),
+                            (datetime(2016, 11, 6, 3, 1), ESTs)]
+
+        for t_date, expected in transition_dates:
+            self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
+
+    def testTzwinRepr(self):
+        tw = tz.tzwin('Yakutsk Standard Time')
+        self.assertEqual(repr(tw), 'tzwin(' +
+                                   repr('Yakutsk Standard Time') + ')')
+
+    def testTzWinEquality(self):
+        # https://github.com/dateutil/dateutil/issues/151
+        tzwin_names = ('Eastern Standard Time',
+                       'West Pacific Standard Time',
+                       'Yakutsk Standard Time',
+                       'Iran Standard Time',
+                       'UTC')
+
+        for tzwin_name in tzwin_names:
+            # Get two different instances to compare
+            tw1 = tz.tzwin(tzwin_name)
+            tw2 = tz.tzwin(tzwin_name)
+
+            self.assertEqual(tw1, tw2)
+
+    def testTzWinInequality(self):
+        # https://github.com/dateutil/dateutil/issues/151
+        # Note these last two currently differ only in their name.
+        tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'),
+                       ('Greenwich Standard Time', 'GMT Standard Time'),
+                       ('GMT Standard Time', 'UTC'),
+                       ('E. South America Standard Time',
+                        'Argentina Standard Time'))
+
+        for tzwn1, tzwn2 in tzwin_names:
+            # Get two different instances to compare
+            tw1 = tz.tzwin(tzwn1)
+            tw2 = tz.tzwin(tzwn2)
+
+            self.assertNotEqual(tw1, tw2)
+
+    def testTzWinEqualityInvalid(self):
+        # Compare to objects that do not implement comparison with this
+        # (should default to False)
+        UTC = tz.tzutc()
+        EST = tz.tzwin('Eastern Standard Time')
+
+        self.assertFalse(EST == UTC)
+        self.assertFalse(EST == 1)
+        self.assertFalse(UTC == EST)
+
+        self.assertTrue(EST != UTC)
+        self.assertTrue(EST != 1)
+
+    def testTzWinInequalityUnsupported(self):
+        # Compare it to an object that is promiscuous about equality, but for
+        # which tzwin does not implement an equality operator.
+        EST = tz.tzwin('Eastern Standard Time')
+        self.assertTrue(EST == ComparesEqual)
+        self.assertFalse(EST != ComparesEqual)
+
+    def testTzwinTimeOnlyDST(self):
+        # For zones with DST, .dst() should return None
+        tw_est = tz.tzwin('Eastern Standard Time')
+        self.assertIs(dt_time(14, 10, tzinfo=tw_est).dst(), None)
+
+        # This zone has no DST, so .dst() can return 0
+        tw_sast = tz.tzwin('South Africa Standard Time')
+        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).dst(),
+                         timedelta(0))
+
+    def testTzwinTimeOnlyUTCOffset(self):
+        # For zones with DST, .utcoffset() should return None
+        tw_est = tz.tzwin('Eastern Standard Time')
+        self.assertIs(dt_time(14, 10, tzinfo=tw_est).utcoffset(), None)
+
+        # This zone has no DST, so .utcoffset() returns standard offset
+        tw_sast = tz.tzwin('South Africa Standard Time')
+        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).utcoffset(),
+                         timedelta(hours=2))
+
+    def testTzwinTimeOnlyTZName(self):
+        # For zones with DST, the name defaults to standard time
+        tw_est = tz.tzwin('Eastern Standard Time')
+        self.assertEqual(dt_time(14, 10, tzinfo=tw_est).tzname(),
+                         'Eastern Standard Time')
+
+        # For zones with no DST, this should work normally.
+        tw_sast = tz.tzwin('South Africa Standard Time')
+        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).tzname(),
+                         'South Africa Standard Time')
+
+
+@unittest.skipUnless(IS_WIN, "Requires Windows")
+@unittest.skipUnless(TZWinContext.tz_change_allowed(),
+                     TZWinContext.tz_change_disallowed_message())
+class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin):
+
+    def setUp(self):
+        self.tzclass = tzwin.tzwinlocal
+        self.context = TZWinContext
+
+    def get_args(self, tzname):
+        return ()
+
+    def testLocal(self):
+        # Not sure how to pin a local time zone, so for now we're just going
+        # to run this and make sure it doesn't raise an error
+        # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135
+        datetime.now(tzwin.tzwinlocal())
+
+    def testTzwinLocalUTCOffset(self):
+        with TZWinContext('Eastern Standard Time'):
+            tzwl = tzwin.tzwinlocal()
+            self.assertEqual(datetime(2014, 3, 11, tzinfo=tzwl).utcoffset(),
+                             timedelta(hours=-4))
+
+    def testTzwinLocalName(self):
+        # https://github.com/dateutil/dateutil/issues/143
+        ESTs = 'Eastern Standard Time'
+        EDTs = 'Eastern Daylight Time'
+        transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
+                            (datetime(2015, 3, 8, 3, 1), EDTs),
+                            (datetime(2015, 11, 1, 0, 59), EDTs),
+                            (datetime(2015, 11, 1, 3, 1), ESTs),
+                            (datetime(2016, 3, 13, 0, 59), ESTs),
+                            (datetime(2016, 3, 13, 3, 1), EDTs),
+                            (datetime(2016, 11, 6, 0, 59), EDTs),
+                            (datetime(2016, 11, 6, 3, 1), ESTs)]
+
+        with TZWinContext('Eastern Standard Time'):
+            tw = tz.tzwinlocal()
+
+            for t_date, expected in transition_dates:
+                self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
+
+    def testTzWinLocalRepr(self):
+        tw = tz.tzwinlocal()
+        self.assertEqual(repr(tw), 'tzwinlocal()')
+
+    def testTzwinLocalRepr(self):
+        # https://github.com/dateutil/dateutil/issues/143
+        with TZWinContext('Eastern Standard Time'):
+            tw = tz.tzwinlocal()
+
+            self.assertEqual(str(tw), 'tzwinlocal(' +
+                                      repr('Eastern Standard Time') + ')')
+
+        with TZWinContext('Pacific Standard Time'):
+            tw = tz.tzwinlocal()
+
+            self.assertEqual(str(tw), 'tzwinlocal(' +
+                                      repr('Pacific Standard Time') + ')')
+
+    def testTzwinLocalEquality(self):
+        tw_est = tz.tzwin('Eastern Standard Time')
+        tw_pst = tz.tzwin('Pacific Standard Time')
+
+        with TZWinContext('Eastern Standard Time'):
+            twl1 = tz.tzwinlocal()
+            twl2 = tz.tzwinlocal()
+
+            self.assertEqual(twl1, twl2)
+            self.assertEqual(twl1, tw_est)
+            self.assertNotEqual(twl1, tw_pst)
+
+        with TZWinContext('Pacific Standard Time'):
+            twl1 = tz.tzwinlocal()
+            twl2 = tz.tzwinlocal()
+            tw = tz.tzwin('Pacific Standard Time')
+
+            self.assertEqual(twl1, twl2)
+            self.assertEqual(twl1, tw)
+            self.assertEqual(twl1, tw_pst)
+            self.assertNotEqual(twl1, tw_est)
+
+    def testTzwinLocalTimeOnlyDST(self):
+        # For zones with DST, .dst() should return None
+        with TZWinContext('Eastern Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertIs(dt_time(14, 10, tzinfo=twl).dst(), None)
+
+        # This zone has no DST, so .dst() can return 0
+        with TZWinContext('South Africa Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertEqual(dt_time(14, 10, tzinfo=twl).dst(), timedelta(0))
+
+    def testTzwinLocalTimeOnlyUTCOffset(self):
+        # For zones with DST, .utcoffset() should return None
+        with TZWinContext('Eastern Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertIs(dt_time(14, 10, tzinfo=twl).utcoffset(), None)
+
+        # This zone has no DST, so .utcoffset() returns standard offset
+        with TZWinContext('South Africa Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertEqual(dt_time(14, 10, tzinfo=twl).utcoffset(),
+                             timedelta(hours=2))
+
+    def testTzwinLocalTimeOnlyTZName(self):
+        # For zones with DST, the name defaults to standard time
+        with TZWinContext('Eastern Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
+                             'Eastern Standard Time')
+
+        # For zones with no DST, this should work normally.
+        with TZWinContext('South Africa Standard Time'):
+            twl = tz.tzwinlocal()
+            self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
+                             'South Africa Standard Time')
+
+
+class TzPickleTest(PicklableMixin, unittest.TestCase):
+    _asfile = False
+
+    def setUp(self):
+        self.assertPicklable = partial(self.assertPicklable,
+                                       asfile=self._asfile)
+
+    def testPickleTzUTC(self):
+        self.assertPicklable(tz.tzutc(), singleton=True)
+
+    def testPickleTzOffsetZero(self):
+        self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True)
+
+    def testPickleTzOffsetPos(self):
+        self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True)
+
+    def testPickleTzOffsetNeg(self):
+        self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True)
+
+    @pytest.mark.tzlocal
+    def testPickleTzLocal(self):
+        self.assertPicklable(tz.tzlocal())
+
+    def testPickleTzFileEST5EDT(self):
+        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
+        self.assertPicklable(tzc)
+
+    def testPickleTzFileEurope_Helsinki(self):
+        tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+        self.assertPicklable(tzc)
+
+    def testPickleTzFileNew_York(self):
+        tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
+        self.assertPicklable(tzc)
+
+    @unittest.skip("Known failure")
+    def testPickleTzICal(self):
+        tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get()
+        self.assertPicklable(tzc)
+
+    def testPickleTzGettz(self):
+        self.assertPicklable(tz.gettz('America/New_York'))
+
+    def testPickleZoneFileGettz(self):
+        zoneinfo_file = zoneinfo.get_zonefile_instance()
+        tzi = zoneinfo_file.get('America/New_York')
+        self.assertIsNot(tzi, None)
+        self.assertPicklable(tzi)
+
+
+class TzPickleFileTest(TzPickleTest):
+    """ Run all the TzPickleTest tests, using a temporary file """
+    _asfile = True
+
+
+class DatetimeAmbiguousTest(unittest.TestCase):
+    """ Test the datetime_exists / datetime_ambiguous functions """
+
+    def testNoTzSpecified(self):
+        with self.assertRaises(ValueError):
+            tz.datetime_ambiguous(datetime(2016, 4, 1, 2, 9))
+
+    def _get_no_support_tzinfo_class(self, dt_start, dt_end, dst_only=False):
+        # Generates a class of tzinfo with no support for is_ambiguous
+        # where dates between dt_start and dt_end are ambiguous.
+
+        class FoldingTzInfo(tzinfo):
+            def utcoffset(self, dt):
+                if not dst_only:
+                    dt_n = dt.replace(tzinfo=None)
+
+                    if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
+                        return timedelta(hours=-1)
+
+                return timedelta(hours=0)
+
+            def dst(self, dt):
+                dt_n = dt.replace(tzinfo=None)
+
+                if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
+                    return timedelta(hours=1)
+                else:
+                    return timedelta(0)
+
+        return FoldingTzInfo
+
+    def _get_no_support_tzinfo(self, dt_start, dt_end, dst_only=False):
+        return self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)()
+
+    def testNoSupportAmbiguityFoldNaive(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+                                              tz=tzi))
+
+    def testNoSupportAmbiguityFoldAware(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
+                                                       tzinfo=tzi)))
+
+    def testNoSupportAmbiguityUnambiguousNaive(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+                                              tz=tzi))
+
+    def testNoSupportAmbiguityUnambiguousAware(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
+                                                        tzinfo=tzi)))
+
+    def testNoSupportAmbiguityFoldDSTOnly(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+                                              tz=tzi))
+
+    def testNoSupportAmbiguityUnambiguousDSTOnly(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+                                               tz=tzi))
+
+    def testSupportAmbiguityFoldNaive(self):
+        tzi = tz.gettz('US/Eastern')
+
+        dt = datetime(2011, 11, 6, 1, 30)
+
+        self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
+
+    def testSupportAmbiguityFoldAware(self):
+        tzi = tz.gettz('US/Eastern')
+
+        dt = datetime(2011, 11, 6, 1, 30, tzinfo=tzi)
+
+        self.assertTrue(tz.datetime_ambiguous(dt))
+
+    def testSupportAmbiguityUnambiguousAware(self):
+        tzi = tz.gettz('US/Eastern')
+
+        dt = datetime(2011, 11, 6, 4, 30)
+
+        self.assertFalse(tz.datetime_ambiguous(dt, tz=tzi))
+
+    def testSupportAmbiguityUnambiguousNaive(self):
+        tzi = tz.gettz('US/Eastern')
+
+        dt = datetime(2011, 11, 6, 4, 30, tzinfo=tzi)
+
+        self.assertFalse(tz.datetime_ambiguous(dt))
+
+    def _get_ambig_error_tzinfo(self, dt_start, dt_end, dst_only=False):
+        cTzInfo = self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)
+
+        # Takes the wrong number of arguments and raises an error anyway.
+        class FoldTzInfoRaises(cTzInfo):
+            def is_ambiguous(self, dt, other_arg):
+                raise NotImplementedError('This is not implemented')
+
+        return FoldTzInfoRaises()
+
+    def testIncompatibleAmbiguityFoldNaive(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+                                              tz=tzi))
+
+    def testIncompatibleAmbiguityFoldAware(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
+                                                       tzinfo=tzi)))
+
+    def testIncompatibleAmbiguityUnambiguousNaive(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+                                              tz=tzi))
+
+    def testIncompatibleAmbiguityUnambiguousAware(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
+                                                        tzinfo=tzi)))
+
+    def testIncompatibleAmbiguityFoldDSTOnly(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
+
+        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
+                                              tz=tzi))
+
+    def testIncompatibleAmbiguityUnambiguousDSTOnly(self):
+        dt_start = datetime(2018, 9, 1, 1, 0)
+        dt_end = datetime(2018, 9, 1, 2, 0)
+
+        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
+
+        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
+                                               tz=tzi))
+
+    def testSpecifiedTzOverridesAttached(self):
+        # If a tz is specified, the datetime will be treated as naive.
+
+        # This is not ambiguous in the local zone
+        dt = datetime(2011, 11, 6, 1, 30, tzinfo=tz.gettz('Australia/Sydney'))
+
+        self.assertFalse(tz.datetime_ambiguous(dt))
+
+        tzi = tz.gettz('US/Eastern')
+        self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
+
+
+class DatetimeExistsTest(unittest.TestCase):
+    def testNoTzSpecified(self):
+        with self.assertRaises(ValueError):
+            tz.datetime_exists(datetime(2016, 4, 1, 2, 9))
+
+    def testInGapNaive(self):
+        tzi = tz.gettz('Australia/Sydney')
+
+        dt = datetime(2012, 10, 7, 2, 30)
+
+        self.assertFalse(tz.datetime_exists(dt, tz=tzi))
+
+    def testInGapAware(self):
+        tzi = tz.gettz('Australia/Sydney')
+
+        dt = datetime(2012, 10, 7, 2, 30, tzinfo=tzi)
+
+        self.assertFalse(tz.datetime_exists(dt))
+
+    def testExistsNaive(self):
+        tzi = tz.gettz('Australia/Sydney')
+
+        dt = datetime(2012, 10, 7, 10, 30)
+
+        self.assertTrue(tz.datetime_exists(dt, tz=tzi))
+
+    def testExistsAware(self):
+        tzi = tz.gettz('Australia/Sydney')
+
+        dt = datetime(2012, 10, 7, 10, 30, tzinfo=tzi)
+
+        self.assertTrue(tz.datetime_exists(dt))
+
+    def testSpecifiedTzOverridesAttached(self):
+        EST = tz.gettz('US/Eastern')
+        AEST = tz.gettz('Australia/Sydney')
+
+        dt = datetime(2012, 10, 7, 2, 30, tzinfo=EST)  # This time exists
+
+        self.assertFalse(tz.datetime_exists(dt, tz=AEST))
+
+
+class EnfoldTest(unittest.TestCase):
+    def testEnterFoldDefault(self):
+        dt = tz.enfold(datetime(2020, 1, 19, 3, 32))
+
+        self.assertEqual(dt.fold, 1)
+
+    def testEnterFold(self):
+        dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
+
+        self.assertEqual(dt.fold, 1)
+
+    def testExitFold(self):
+        dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0)
+
+        # Before Python 3.6, dt.fold won't exist if fold is 0.
+        self.assertEqual(getattr(dt, 'fold', 0), 0)
+
+
+@pytest.mark.tz_resolve_imaginary
+class ImaginaryDateTest(unittest.TestCase):
+    def testCanberraForward(self):
+        tzi = tz.gettz('Australia/Canberra')
+        dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi)
+        dt_act = tz.resolve_imaginary(dt)
+        dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi)
+        self.assertEqual(dt_act, dt_exp)
+
+    def testLondonForward(self):
+        tzi = tz.gettz('Europe/London')
+        dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi)
+        dt_act = tz.resolve_imaginary(dt)
+        dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi)
+        self.assertEqual(dt_act, dt_exp)
+
+    def testKeivForward(self):
+        tzi = tz.gettz('Europe/Kiev')
+        dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi)
+        dt_act = tz.resolve_imaginary(dt)
+        dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi)
+        self.assertEqual(dt_act, dt_exp)
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('dt', [
+    datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')),
+    datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')),
+    datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')),
+])
+def test_resolve_imaginary_ambiguous(dt):
+    assert tz.resolve_imaginary(dt) is dt
+
+    dt_f = tz.enfold(dt)
+    assert dt is not dt_f
+    assert tz.resolve_imaginary(dt_f) is dt_f
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('dt', [
+    datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
+    datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
+    datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
+    datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
+    datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
+    datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
+    datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzutc()),
+    datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)),
+    datetime(2019, 3, 4, tzinfo=None)
+])
+def test_resolve_imaginary_existing(dt):
+    assert tz.resolve_imaginary(dt) is dt
+
+
+def __get_kiritimati_resolve_imaginary_test():
+    # In the 2018d release of the IANA database, the Kiritimati "imaginary day"
+    # data was corrected, so if the system zoneinfo is older than 2018d, the
+    # Kiritimati test will fail.
+
+    tzi = tz.gettz('Pacific/Kiritimati')
+    new_version = False
+    if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi):
+        zif = zoneinfo.get_zonefile_instance()
+        if zif.metadata is not None:
+            new_version = zif.metadata['tzversion'] >= '2018d'
+
+        if new_version:
+            tzi = zif.get('Pacific/Kiritimati')
+    else:
+        new_version = True
+
+    if new_version:
+        dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30))
+    else:
+        dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30))
+
+    return (tzi, ) + dates
+
+
+@pytest.mark.tz_resolve_imaginary
+@pytest.mark.parametrize('tzi, dt, dt_exp', [
+    (tz.gettz('Europe/London'),
+     datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)),
+    (tz.gettz('America/New_York'),
+     datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)),
+    (tz.gettz('Australia/Sydney'),
+     datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)),
+    __get_kiritimati_resolve_imaginary_test(),
+])
+def test_resolve_imaginary(tzi, dt, dt_exp):
+    dt = dt.replace(tzinfo=tzi)
+    dt_exp = dt_exp.replace(tzinfo=tzi)
+
+    dt_r = tz.resolve_imaginary(dt)
+    assert dt_r == dt_exp
+    assert dt_r.tzname() == dt_exp.tzname()
+    assert dt_r.utcoffset() == dt_exp.utcoffset()
+
+
+@pytest.mark.xfail
+@pytest.mark.tz_resolve_imaginary
+def test_resolve_imaginary_monrovia():
+    # See GH #582 - When that is resolved, move this into test_resolve_imaginary
+    tzi = tz.gettz('Africa/Monrovia')
+    dt = datetime(1972, 1, 7, hour=0, minute=30, second=0, tzinfo=tzi)
+    dt_exp = datetime(1972, 1, 7, hour=1, minute=14, second=30, tzinfo=tzi)
+
+    dt_r = tz.resolve_imaginary(dt)
+    assert dt_r == dt_exp
+    assert dt_r.tzname() == dt_exp.tzname()
+    assert dt_r.utcoffset() == dt_exp.utcoffset()
diff --git a/resources/lib/libraries/dateutil/test/test_utils.py b/resources/lib/libraries/dateutil/test/test_utils.py
new file mode 100644
index 00000000..fcdec1a5
--- /dev/null
+++ b/resources/lib/libraries/dateutil/test/test_utils.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from datetime import timedelta, datetime
+
+import unittest
+
+from dateutil import tz
+from dateutil import utils
+from dateutil.utils import within_delta
+
+from freezegun import freeze_time
+
+UTC = tz.tzutc()
+NYC = tz.gettz("America/New_York")
+
+
+class UtilsTest(unittest.TestCase):
+    @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003))
+    def testToday(self):
+        self.assertEqual(utils.today(), datetime(2014, 12, 15, 0, 0, 0))
+
+    @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5)
+    def testTodayTzInfo(self):
+        self.assertEqual(utils.today(NYC),
+                         datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC))
+
+    @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5)
+    def testTodayTzInfoDifferentDay(self):
+        self.assertEqual(utils.today(UTC),
+                         datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC))
+
+    def testDefaultTZInfoNaive(self):
+        dt = datetime(2014, 9, 14, 9, 30)
+        self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo,
+                      NYC)
+
+    def testDefaultTZInfoAware(self):
+        dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC)
+        self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo,
+                      UTC)
+
+    def testWithinDelta(self):
+        d1 = datetime(2016, 1, 1, 12, 14, 1, 9)
+        d2 = d1.replace(microsecond=15)
+
+        self.assertTrue(within_delta(d1, d2, timedelta(seconds=1)))
+        self.assertFalse(within_delta(d1, d2, timedelta(microseconds=1)))
+
+    def testWithinDeltaWithNegativeDelta(self):
+        d1 = datetime(2016, 1, 1)
+        d2 = datetime(2015, 12, 31)
+
+        self.assertTrue(within_delta(d2, d1, timedelta(days=-1)))
diff --git a/resources/lib/libraries/dateutil/tz/__init__.py b/resources/lib/libraries/dateutil/tz/__init__.py
new file mode 100644
index 00000000..5a2d9cd6
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tz/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from .tz import *
+from .tz import __doc__
+
+#: Convenience constant providing a :class:`tzutc()` instance
+#:
+#: .. versionadded:: 2.7.0
+UTC = tzutc()
+
+__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+           "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
+           "enfold", "datetime_ambiguous", "datetime_exists",
+           "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
+
+
+class DeprecatedTzFormatWarning(Warning):
+    """Warning raised when time zones are parsed from deprecated formats."""
diff --git a/resources/lib/libraries/dateutil/tz/_common.py b/resources/lib/libraries/dateutil/tz/_common.py
new file mode 100644
index 00000000..ccabb7da
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tz/_common.py
@@ -0,0 +1,415 @@
+from six import PY3
+
+from functools import wraps
+
+from datetime import datetime, timedelta, tzinfo
+
+
+ZERO = timedelta(0)
+
+__all__ = ['tzname_in_python2', 'enfold']
+
+
+def tzname_in_python2(namefunc):
+    """Change unicode output into bytestrings in Python 2
+
+    tzname() API changed in Python 3. It used to return bytes, but was changed
+    to unicode strings
+    """
+    def adjust_encoding(*args, **kwargs):
+        name = namefunc(*args, **kwargs)
+        if name is not None and not PY3:
+            name = name.encode()
+
+        return name
+
+    return adjust_encoding
+
+
+# The following is adapted from Alexander Belopolsky's tz library
+# https://github.com/abalkin/tz
+if hasattr(datetime, 'fold'):
+    # This is the pre-python 3.6 fold situation
+    def enfold(dt, fold=1):
+        """
+        Provides a unified interface for assigning the ``fold`` attribute to
+        datetimes both before and after the implementation of PEP-495.
+
+        :param fold:
+            The value for the ``fold`` attribute in the returned datetime. This
+            should be either 0 or 1.
+
+        :return:
+            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+            ``fold`` for all versions of Python. In versions prior to
+            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+            subclass of :py:class:`datetime.datetime` with the ``fold``
+            attribute added, if ``fold`` is 1.
+
+        .. versionadded:: 2.6.0
+        """
+        return dt.replace(fold=fold)
+
+else:
+    class _DatetimeWithFold(datetime):
+        """
+        This is a class designed to provide a PEP 495-compliant interface for
+        Python versions before 3.6. It is used only for dates in a fold, so
+        the ``fold`` attribute is fixed at ``1``.
+
+        .. versionadded:: 2.6.0
+        """
+        __slots__ = ()
+
+        def replace(self, *args, **kwargs):
+            """
+            Return a datetime with the same attributes, except for those
+            attributes given new values by whichever keyword arguments are
+            specified. Note that tzinfo=None can be specified to create a naive
+            datetime from an aware datetime with no conversion of date and time
+            data.
+
+            This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
+            return a ``datetime.datetime`` even if ``fold`` is unchanged.
+            """
+            argnames = (
+                'year', 'month', 'day', 'hour', 'minute', 'second',
+                'microsecond', 'tzinfo'
+            )
+
+            for arg, argname in zip(args, argnames):
+                if argname in kwargs:
+                    raise TypeError('Duplicate argument: {}'.format(argname))
+
+                kwargs[argname] = arg
+
+            for argname in argnames:
+                if argname not in kwargs:
+                    kwargs[argname] = getattr(self, argname)
+
+            dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
+
+            return dt_class(**kwargs)
+
+        @property
+        def fold(self):
+            return 1
+
+    def enfold(dt, fold=1):
+        """
+        Provides a unified interface for assigning the ``fold`` attribute to
+        datetimes both before and after the implementation of PEP-495.
+
+        :param fold:
+            The value for the ``fold`` attribute in the returned datetime. This
+            should be either 0 or 1.
+
+        :return:
+            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
+            ``fold`` for all versions of Python. In versions prior to
+            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
+            subclass of :py:class:`datetime.datetime` with the ``fold``
+            attribute added, if ``fold`` is 1.
+
+        .. versionadded:: 2.6.0
+        """
+        if getattr(dt, 'fold', 0) == fold:
+            return dt
+
+        args = dt.timetuple()[:6]
+        args += (dt.microsecond, dt.tzinfo)
+
+        if fold:
+            return _DatetimeWithFold(*args)
+        else:
+            return datetime(*args)
+
+
+def _validate_fromutc_inputs(f):
+    """
+    The CPython version of ``fromutc`` checks that the input is a ``datetime``
+    object and that ``self`` is attached as its ``tzinfo``.
+    """
+    @wraps(f)
+    def fromutc(self, dt):
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        return f(self, dt)
+
+    return fromutc
+
+
+class _tzinfo(tzinfo):
+    """
+    Base class for all ``dateutil`` ``tzinfo`` objects.
+    """
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+
+        dt = dt.replace(tzinfo=self)
+
+        wall_0 = enfold(dt, fold=0)
+        wall_1 = enfold(dt, fold=1)
+
+        same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+        same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
+
+        return same_dt and not same_offset
+
+    def _fold_status(self, dt_utc, dt_wall):
+        """
+        Determine the fold status of a "wall" datetime, given a representation
+        of the same datetime as a (naive) UTC datetime. This is calculated based
+        on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
+        datetimes, and that this offset is the actual number of hours separating
+        ``dt_utc`` and ``dt_wall``.
+
+        :param dt_utc:
+            Representation of the datetime as UTC
+
+        :param dt_wall:
+            Representation of the datetime as "wall time". This parameter must
+            either have a `fold` attribute or have a fold-naive
+            :class:`datetime.tzinfo` attached, otherwise the calculation may
+            fail.
+        """
+        if self.is_ambiguous(dt_wall):
+            delta_wall = dt_wall - dt_utc
+            _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
+        else:
+            _fold = 0
+
+        return _fold
+
+    def _fold(self, dt):
+        return getattr(dt, 'fold', 0)
+
+    def _fromutc(self, dt):
+        """
+        Given a timezone-aware datetime in a given timezone, calculates a
+        timezone-aware datetime in a new timezone.
+
+        Since this is the one time that we *know* we have an unambiguous
+        datetime object, we take this opportunity to determine whether the
+        datetime is ambiguous and in a "fold" state (e.g. if it's the first
+        occurence, chronologically, of the ambiguous datetime).
+
+        :param dt:
+            A timezone-aware :class:`datetime.datetime` object.
+        """
+
+        # Re-implement the algorithm from Python's datetime.py
+        dtoff = dt.utcoffset()
+        if dtoff is None:
+            raise ValueError("fromutc() requires a non-None utcoffset() "
+                             "result")
+
+        # The original datetime.py code assumes that `dst()` defaults to
+        # zero during ambiguous times. PEP 495 inverts this presumption, so
+        # for pre-PEP 495 versions of python, we need to tweak the algorithm.
+        dtdst = dt.dst()
+        if dtdst is None:
+            raise ValueError("fromutc() requires a non-None dst() result")
+        delta = dtoff - dtdst
+
+        dt += delta
+        # Set fold=1 so we can default to being in the fold for
+        # ambiguous dates.
+        dtdst = enfold(dt, fold=1).dst()
+        if dtdst is None:
+            raise ValueError("fromutc(): dt.dst gave inconsistent "
+                             "results; cannot convert")
+        return dt + dtdst
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        """
+        Given a timezone-aware datetime in a given timezone, calculates a
+        timezone-aware datetime in a new timezone.
+
+        Since this is the one time that we *know* we have an unambiguous
+        datetime object, we take this opportunity to determine whether the
+        datetime is ambiguous and in a "fold" state (e.g. if it's the first
+        occurance, chronologically, of the ambiguous datetime).
+
+        :param dt:
+            A timezone-aware :class:`datetime.datetime` object.
+        """
+        dt_wall = self._fromutc(dt)
+
+        # Calculate the fold status given the two datetimes.
+        _fold = self._fold_status(dt, dt_wall)
+
+        # Set the default fold value for ambiguous dates
+        return enfold(dt_wall, fold=_fold)
+
+
+class tzrangebase(_tzinfo):
+    """
+    This is an abstract base class for time zones represented by an annual
+    transition into and out of DST. Child classes should implement the following
+    methods:
+
+        * ``__init__(self, *args, **kwargs)``
+        * ``transitions(self, year)`` - this is expected to return a tuple of
+          datetimes representing the DST on and off transitions in standard
+          time.
+
+    A fully initialized ``tzrangebase`` subclass should also provide the
+    following attributes:
+        * ``hasdst``: Boolean whether or not the zone uses DST.
+        * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
+          representing the respective UTC offsets.
+        * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
+          abbreviations in DST and STD, respectively.
+        * ``_hasdst``: Whether or not the zone has DST.
+
+    .. versionadded:: 2.6.0
+    """
+    def __init__(self):
+        raise NotImplementedError('tzrangebase is an abstract base class')
+
+    def utcoffset(self, dt):
+        isdst = self._isdst(dt)
+
+        if isdst is None:
+            return None
+        elif isdst:
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        isdst = self._isdst(dt)
+
+        if isdst is None:
+            return None
+        elif isdst:
+            return self._dst_base_offset
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        if self._isdst(dt):
+            return self._dst_abbr
+        else:
+            return self._std_abbr
+
+    def fromutc(self, dt):
+        """ Given a datetime in UTC, return local time """
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        # Get transitions - if there are none, fixed offset
+        transitions = self.transitions(dt.year)
+        if transitions is None:
+            return dt + self.utcoffset(dt)
+
+        # Get the transition times in UTC
+        dston, dstoff = transitions
+
+        dston -= self._std_offset
+        dstoff -= self._std_offset
+
+        utc_transitions = (dston, dstoff)
+        dt_utc = dt.replace(tzinfo=None)
+
+        isdst = self._naive_isdst(dt_utc, utc_transitions)
+
+        if isdst:
+            dt_wall = dt + self._dst_offset
+        else:
+            dt_wall = dt + self._std_offset
+
+        _fold = int(not isdst and self.is_ambiguous(dt_wall))
+
+        return enfold(dt_wall, fold=_fold)
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        if not self.hasdst:
+            return False
+
+        start, end = self.transitions(dt.year)
+
+        dt = dt.replace(tzinfo=None)
+        return (end <= dt < end + self._dst_base_offset)
+
+    def _isdst(self, dt):
+        if not self.hasdst:
+            return False
+        elif dt is None:
+            return None
+
+        transitions = self.transitions(dt.year)
+
+        if transitions is None:
+            return False
+
+        dt = dt.replace(tzinfo=None)
+
+        isdst = self._naive_isdst(dt, transitions)
+
+        # Handle ambiguous dates
+        if not isdst and self.is_ambiguous(dt):
+            return not self._fold(dt)
+        else:
+            return isdst
+
+    def _naive_isdst(self, dt, transitions):
+        dston, dstoff = transitions
+
+        dt = dt.replace(tzinfo=None)
+
+        if dston < dstoff:
+            isdst = dston <= dt < dstoff
+        else:
+            isdst = not dstoff <= dt < dston
+
+        return isdst
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_offset - self._std_offset
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(...)" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
diff --git a/resources/lib/libraries/dateutil/tz/_factories.py b/resources/lib/libraries/dateutil/tz/_factories.py
new file mode 100644
index 00000000..de2e0c1d
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tz/_factories.py
@@ -0,0 +1,49 @@
+from datetime import timedelta
+
+
+class _TzSingleton(type):
+    def __init__(cls, *args, **kwargs):
+        cls.__instance = None
+        super(_TzSingleton, cls).__init__(*args, **kwargs)
+
+    def __call__(cls):
+        if cls.__instance is None:
+            cls.__instance = super(_TzSingleton, cls).__call__()
+        return cls.__instance
+
+class _TzFactory(type):
+    def instance(cls, *args, **kwargs):
+        """Alternate constructor that returns a fresh instance"""
+        return type.__call__(cls, *args, **kwargs)
+
+
+class _TzOffsetFactory(_TzFactory):
+    def __init__(cls, *args, **kwargs):
+        cls.__instances = {}
+
+    def __call__(cls, name, offset):
+        if isinstance(offset, timedelta):
+            key = (name, offset.total_seconds())
+        else:
+            key = (name, offset)
+
+        instance = cls.__instances.get(key, None)
+        if instance is None:
+            instance = cls.__instances.setdefault(key,
+                                                  cls.instance(name, offset))
+        return instance
+
+
+class _TzStrFactory(_TzFactory):
+    def __init__(cls, *args, **kwargs):
+        cls.__instances = {}
+
+    def __call__(cls, s, posix_offset=False):
+        key = (s, posix_offset)
+        instance = cls.__instances.get(key, None)
+
+        if instance is None:
+            instance = cls.__instances.setdefault(key,
+                cls.instance(s, posix_offset))
+        return instance
+
diff --git a/resources/lib/libraries/dateutil/tz/tz.py b/resources/lib/libraries/dateutil/tz/tz.py
new file mode 100644
index 00000000..4c23242a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tz/tz.py
@@ -0,0 +1,1785 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers timezone implementations subclassing the abstract
+:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
+files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
+etc), TZ environment string (in all known formats), given ranges (with help
+from relative deltas), local machine timezone, fixed offset timezone, and UTC
+timezone.
+"""
+import datetime
+import struct
+import time
+import sys
+import os
+import bisect
+
+from .. import six
+from ..six import string_types
+from ..six.moves import _thread
+from ._common import tzname_in_python2, _tzinfo
+from ._common import tzrangebase, enfold
+from ._common import _validate_fromutc_inputs
+
+from ._factories import _TzSingleton, _TzOffsetFactory
+from ._factories import _TzStrFactory
+try:
+    from .win import tzwin, tzwinlocal
+except ImportError:
+    tzwin = tzwinlocal = None
+
+ZERO = datetime.timedelta(0)
+EPOCH = datetime.datetime.utcfromtimestamp(0)
+EPOCHORDINAL = EPOCH.toordinal()
+
+
+@six.add_metaclass(_TzSingleton)
+class tzutc(datetime.tzinfo):
+    """
+    This is a tzinfo object that represents the UTC time zone.
+
+    **Examples:**
+
+    .. doctest::
+
+        >>> from datetime import *
+        >>> from dateutil.tz import *
+
+        >>> datetime.now()
+        datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
+
+        >>> datetime.now(tzutc())
+        datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
+
+        >>> datetime.now(tzutc()).tzname()
+        'UTC'
+
+    .. versionchanged:: 2.7.0
+        ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
+        always return the same object.
+
+        .. doctest::
+
+            >>> from dateutil.tz import tzutc, UTC
+            >>> tzutc() is tzutc()
+            True
+            >>> tzutc() is UTC
+            True
+    """
+    def utcoffset(self, dt):
+        return ZERO
+
+    def dst(self, dt):
+        return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return "UTC"
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        return False
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        """
+        Fast track version of fromutc() returns the original ``dt`` object for
+        any valid :py:class:`datetime.datetime` object.
+        """
+        return dt
+
+    def __eq__(self, other):
+        if not isinstance(other, (tzutc, tzoffset)):
+            return NotImplemented
+
+        return (isinstance(other, tzutc) or
+                (isinstance(other, tzoffset) and other._offset == ZERO))
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+
+@six.add_metaclass(_TzOffsetFactory)
+class tzoffset(datetime.tzinfo):
+    """
+    A simple class for representing a fixed offset from UTC.
+
+    :param name:
+        The timezone name, to be returned when ``tzname()`` is called.
+    :param offset:
+        The time zone offset in seconds, or (since version 2.6.0, represented
+        as a :py:class:`datetime.timedelta` object).
+    """
+    def __init__(self, name, offset):
+        self._name = name
+
+        try:
+            # Allow a timedelta
+            offset = offset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+        self._offset = datetime.timedelta(seconds=offset)
+
+    def utcoffset(self, dt):
+        return self._offset
+
+    def dst(self, dt):
+        return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._name
+
+    @_validate_fromutc_inputs
+    def fromutc(self, dt):
+        return dt + self._offset
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        return False
+
+    def __eq__(self, other):
+        if not isinstance(other, tzoffset):
+            return NotImplemented
+
+        return self._offset == other._offset
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(%s, %s)" % (self.__class__.__name__,
+                               repr(self._name),
+                               int(self._offset.total_seconds()))
+
+    __reduce__ = object.__reduce__
+
+
+class tzlocal(_tzinfo):
+    """
+    A :class:`tzinfo` subclass built around the ``time`` timezone functions.
+    """
+    def __init__(self):
+        super(tzlocal, self).__init__()
+
+        self._std_offset = datetime.timedelta(seconds=-time.timezone)
+        if time.daylight:
+            self._dst_offset = datetime.timedelta(seconds=-time.altzone)
+        else:
+            self._dst_offset = self._std_offset
+
+        self._dst_saved = self._dst_offset - self._std_offset
+        self._hasdst = bool(self._dst_saved)
+        self._tznames = tuple(time.tzname)
+
+    def utcoffset(self, dt):
+        if dt is None and self._hasdst:
+            return None
+
+        if self._isdst(dt):
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        if dt is None and self._hasdst:
+            return None
+
+        if self._isdst(dt):
+            return self._dst_offset - self._std_offset
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._tznames[self._isdst(dt)]
+
+    def is_ambiguous(self, dt):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        naive_dst = self._naive_is_dst(dt)
+        return (not naive_dst and
+                (naive_dst != self._naive_is_dst(dt - self._dst_saved)))
+
+    def _naive_is_dst(self, dt):
+        timestamp = _datetime_to_timestamp(dt)
+        return time.localtime(timestamp + time.timezone).tm_isdst
+
+    def _isdst(self, dt, fold_naive=True):
+        # We can't use mktime here. It is unstable when deciding if
+        # the hour near to a change is DST or not.
+        #
+        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
+        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
+        # return time.localtime(timestamp).tm_isdst
+        #
+        # The code above yields the following result:
+        #
+        # >>> import tz, datetime
+        # >>> t = tz.tzlocal()
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRDT'
+        # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
+        # 'BRST'
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRST'
+        # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
+        # 'BRDT'
+        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        # 'BRDT'
+        #
+        # Here is a more stable implementation:
+        #
+        if not self._hasdst:
+            return False
+
+        # Check for ambiguous times:
+        dstval = self._naive_is_dst(dt)
+        fold = getattr(dt, 'fold', None)
+
+        if self.is_ambiguous(dt):
+            if fold is not None:
+                return not self._fold(dt)
+            else:
+                return True
+
+        return dstval
+
+    def __eq__(self, other):
+        if isinstance(other, tzlocal):
+            return (self._std_offset == other._std_offset and
+                    self._dst_offset == other._dst_offset)
+        elif isinstance(other, tzutc):
+            return (not self._hasdst and
+                    self._tznames[0] in {'UTC', 'GMT'} and
+                    self._std_offset == ZERO)
+        elif isinstance(other, tzoffset):
+            return (not self._hasdst and
+                    self._tznames[0] == other._name and
+                    self._std_offset == other._offset)
+        else:
+            return NotImplemented
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+
+class _ttinfo(object):
+    __slots__ = ["offset", "delta", "isdst", "abbr",
+                 "isstd", "isgmt", "dstoffset"]
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def __repr__(self):
+        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)" % (self.__class__.__name__, ", ".join(l))
+
+    def __eq__(self, other):
+        if not isinstance(other, _ttinfo):
+            return NotImplemented
+
+        return (self.offset == other.offset and
+                self.delta == other.delta and
+                self.isdst == other.isdst and
+                self.abbr == other.abbr and
+                self.isstd == other.isstd and
+                self.isgmt == other.isgmt and
+                self.dstoffset == other.dstoffset)
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __getstate__(self):
+        state = {}
+        for name in self.__slots__:
+            state[name] = getattr(self, name, None)
+        return state
+
+    def __setstate__(self, state):
+        for name in self.__slots__:
+            if name in state:
+                setattr(self, name, state[name])
+
+
+class _tzfile(object):
+    """
+    Lightweight class for holding the relevant transition and time zone
+    information read from binary tzfiles.
+    """
+    attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
+             'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
+
+    def __init__(self, **kwargs):
+        for attr in self.attrs:
+            setattr(self, attr, kwargs.get(attr, None))
+
+
+class tzfile(_tzinfo):
+    """
+    This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)``
+    format timezone files to extract current and historical zone information.
+
+    :param fileobj:
+        This can be an opened file stream or a file name that the time zone
+        information can be read from.
+
+    :param filename:
+        This is an optional parameter specifying the source of the time zone
+        information in the event that ``fileobj`` is a file object. If omitted
+        and ``fileobj`` is a file stream, this parameter will be set either to
+        ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
+
+    See `Sources for Time Zone and Daylight Saving Time Data
+    <https://data.iana.org/time-zones/tz-link.html>`_ for more information.
+    Time zone files can be compiled from the `IANA Time Zone database files
+    <https://www.iana.org/time-zones>`_ with the `zic time zone compiler
+    <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
+
+    .. note::
+
+        Only construct a ``tzfile`` directly if you have a specific timezone
+        file on disk that you want to read into a Python ``tzinfo`` object.
+        If you want to get a ``tzfile`` representing a specific IANA zone,
+        (e.g. ``'America/New_York'``), you should call
+        :func:`dateutil.tz.gettz` with the zone identifier.
+
+
+    **Examples:**
+
+    Using the US Eastern time zone as an example, we can see that a ``tzfile``
+    provides time zone information for the standard Daylight Saving offsets:
+
+    .. testsetup:: tzfile
+
+        from dateutil.tz import gettz
+        from datetime import datetime
+
+    .. doctest:: tzfile
+
+        >>> NYC = gettz('America/New_York')
+        >>> NYC
+        tzfile('/usr/share/zoneinfo/America/New_York')
+
+        >>> print(datetime(2016, 1, 3, tzinfo=NYC))     # EST
+        2016-01-03 00:00:00-05:00
+
+        >>> print(datetime(2016, 7, 7, tzinfo=NYC))     # EDT
+        2016-07-07 00:00:00-04:00
+
+
+    The ``tzfile`` structure contains a fully history of the time zone,
+    so historical dates will also have the right offsets. For example, before
+    the adoption of the UTC standards, New York used local solar  mean time:
+
+    .. doctest:: tzfile
+
+       >>> print(datetime(1901, 4, 12, tzinfo=NYC))    # LMT
+       1901-04-12 00:00:00-04:56
+
+    And during World War II, New York was on "Eastern War Time", which was a
+    state of permanent daylight saving time:
+
+    .. doctest:: tzfile
+
+        >>> print(datetime(1944, 2, 7, tzinfo=NYC))    # EWT
+        1944-02-07 00:00:00-04:00
+
+    """
+
+    def __init__(self, fileobj, filename=None):
+        super(tzfile, self).__init__()
+
+        file_opened_here = False
+        if isinstance(fileobj, string_types):
+            self._filename = fileobj
+            fileobj = open(fileobj, 'rb')
+            file_opened_here = True
+        elif filename is not None:
+            self._filename = filename
+        elif hasattr(fileobj, "name"):
+            self._filename = fileobj.name
+        else:
+            self._filename = repr(fileobj)
+
+        if fileobj is not None:
+            if not file_opened_here:
+                fileobj = _ContextWrapper(fileobj)
+
+            with fileobj as file_stream:
+                tzobj = self._read_tzfile(file_stream)
+
+            self._set_tzdata(tzobj)
+
+    def _set_tzdata(self, tzobj):
+        """ Set the time zone data of this object from a _tzfile object """
+        # Copy the relevant attributes over as private attributes
+        for attr in _tzfile.attrs:
+            setattr(self, '_' + attr, getattr(tzobj, attr))
+
+    def _read_tzfile(self, fileobj):
+        out = _tzfile()
+
+        # From tzfile(5):
+        #
+        # The time zone information files used by tzset(3)
+        # begin with the magic characters "TZif" to identify
+        # them as time zone information files, followed by
+        # sixteen bytes reserved for future use, followed by
+        # six four-byte values of type long, written in a
+        # ``standard'' byte order (the high-order  byte
+        # of the value is written first).
+        if fileobj.read(4).decode() != "TZif":
+            raise ValueError("magic not found")
+
+        fileobj.read(16)
+
+        (
+            # The number of UTC/local indicators stored in the file.
+            ttisgmtcnt,
+
+            # The number of standard/wall indicators stored in the file.
+            ttisstdcnt,
+
+            # The number of leap seconds for which data is
+            # stored in the file.
+            leapcnt,
+
+            # The number of "transition times" for which data
+            # is stored in the file.
+            timecnt,
+
+            # The number of "local time types" for which data
+            # is stored in the file (must not be zero).
+            typecnt,
+
+            # The  number  of  characters  of "time zone
+            # abbreviation strings" stored in the file.
+            charcnt,
+
+        ) = struct.unpack(">6l", fileobj.read(24))
+
+        # The above header is followed by tzh_timecnt four-byte
+        # values  of  type long,  sorted  in ascending order.
+        # These values are written in ``standard'' byte order.
+        # Each is used as a transition time (as  returned  by
+        # time(2)) at which the rules for computing local time
+        # change.
+
+        if timecnt:
+            out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
+                                                    fileobj.read(timecnt*4)))
+        else:
+            out.trans_list_utc = []
+
+        # Next come tzh_timecnt one-byte values of type unsigned
+        # char; each one tells which of the different types of
+        # ``local time'' types described in the file is associated
+        # with the same-indexed transition time. These values
+        # serve as indices into an array of ttinfo structures that
+        # appears next in the file.
+
+        if timecnt:
+            out.trans_idx = struct.unpack(">%dB" % timecnt,
+                                          fileobj.read(timecnt))
+        else:
+            out.trans_idx = []
+
+        # Each ttinfo structure is written as a four-byte value
+        # for tt_gmtoff  of  type long,  in  a  standard  byte
+        # order, followed  by a one-byte value for tt_isdst
+        # and a one-byte  value  for  tt_abbrind.   In  each
+        # structure, tt_gmtoff  gives  the  number  of
+        # seconds to be added to UTC, tt_isdst tells whether
+        # tm_isdst should be set by  localtime(3),  and
+        # tt_abbrind serves  as an index into the array of
+        # time zone abbreviation characters that follow the
+        # ttinfo structure(s) in the file.
+
+        ttinfo = []
+
+        for i in range(typecnt):
+            ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
+
+        abbr = fileobj.read(charcnt).decode()
+
+        # Then there are tzh_leapcnt pairs of four-byte
+        # values, written in  standard byte  order;  the
+        # first  value  of  each pair gives the time (as
+        # returned by time(2)) at which a leap second
+        # occurs;  the  second  gives the  total  number of
+        # leap seconds to be applied after the given time.
+        # The pairs of values are sorted in ascending order
+        # by time.
+
+        # Not used, for now (but seek for correct file position)
+        if leapcnt:
+            fileobj.seek(leapcnt * 8, os.SEEK_CUR)
+
+        # Then there are tzh_ttisstdcnt standard/wall
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as standard
+        # time or wall clock time, and are used when
+        # a time zone file is used in handling POSIX-style
+        # time zone environment variables.
+
+        if ttisstdcnt:
+            isstd = struct.unpack(">%db" % ttisstdcnt,
+                                  fileobj.read(ttisstdcnt))
+
+        # Finally, there are tzh_ttisgmtcnt UTC/local
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as UTC or
+        # local time, and are used when a time zone file
+        # is used in handling POSIX-style time zone envi-
+        # ronment variables.
+
+        if ttisgmtcnt:
+            isgmt = struct.unpack(">%db" % ttisgmtcnt,
+                                  fileobj.read(ttisgmtcnt))
+
+        # Build ttinfo list
+        out.ttinfo_list = []
+        for i in range(typecnt):
+            gmtoff, isdst, abbrind = ttinfo[i]
+            # Round to full-minutes if that's not the case. Python's
+            # datetime doesn't accept sub-minute timezones. Check
+            # http://python.org/sf/1447945 for some information.
+            gmtoff = 60 * ((gmtoff + 30) // 60)
+            tti = _ttinfo()
+            tti.offset = gmtoff
+            tti.dstoffset = datetime.timedelta(0)
+            tti.delta = datetime.timedelta(seconds=gmtoff)
+            tti.isdst = isdst
+            tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
+            tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
+            tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
+            out.ttinfo_list.append(tti)
+
+        # Replace ttinfo indexes for ttinfo objects.
+        out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx]
+
+        # Set standard, dst, and before ttinfos. before will be
+        # used when a given time is before any transitions,
+        # and will be set to the first non-dst ttinfo, or to
+        # the first dst, if all of them are dst.
+        out.ttinfo_std = None
+        out.ttinfo_dst = None
+        out.ttinfo_before = None
+        if out.ttinfo_list:
+            if not out.trans_list_utc:
+                out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
+            else:
+                for i in range(timecnt-1, -1, -1):
+                    tti = out.trans_idx[i]
+                    if not out.ttinfo_std and not tti.isdst:
+                        out.ttinfo_std = tti
+                    elif not out.ttinfo_dst and tti.isdst:
+                        out.ttinfo_dst = tti
+
+                    if out.ttinfo_std and out.ttinfo_dst:
+                        break
+                else:
+                    if out.ttinfo_dst and not out.ttinfo_std:
+                        out.ttinfo_std = out.ttinfo_dst
+
+                for tti in out.ttinfo_list:
+                    if not tti.isdst:
+                        out.ttinfo_before = tti
+                        break
+                else:
+                    out.ttinfo_before = out.ttinfo_list[0]
+
+        # Now fix transition times to become relative to wall time.
+        #
+        # I'm not sure about this. In my tests, the tz source file
+        # is setup to wall time, and in the binary file isstd and
+        # isgmt are off, so it should be in wall time. OTOH, it's
+        # always in gmt time. Let me know if you have comments
+        # about this.
+        laststdoffset = None
+        out.trans_list = []
+        for i, tti in enumerate(out.trans_idx):
+            if not tti.isdst:
+                offset = tti.offset
+                laststdoffset = offset
+            else:
+                if laststdoffset is not None:
+                    # Store the DST offset as well and update it in the list
+                    tti.dstoffset = tti.offset - laststdoffset
+                    out.trans_idx[i] = tti
+
+                offset = laststdoffset or 0
+
+            out.trans_list.append(out.trans_list_utc[i] + offset)
+
+        # In case we missed any DST offsets on the way in for some reason, make
+        # a second pass over the list, looking for the /next/ DST offset.
+        laststdoffset = None
+        for i in reversed(range(len(out.trans_idx))):
+            tti = out.trans_idx[i]
+            if tti.isdst:
+                if not (tti.dstoffset or laststdoffset is None):
+                    tti.dstoffset = tti.offset - laststdoffset
+            else:
+                laststdoffset = tti.offset
+
+            if not isinstance(tti.dstoffset, datetime.timedelta):
+                tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset)
+
+            out.trans_idx[i] = tti
+
+        out.trans_idx = tuple(out.trans_idx)
+        out.trans_list = tuple(out.trans_list)
+        out.trans_list_utc = tuple(out.trans_list_utc)
+
+        return out
+
+    def _find_last_transition(self, dt, in_utc=False):
+        # If there's no list, there are no transitions to find
+        if not self._trans_list:
+            return None
+
+        timestamp = _datetime_to_timestamp(dt)
+
+        # Find where the timestamp fits in the transition list - if the
+        # timestamp is a transition time, it's part of the "after" period.
+        trans_list = self._trans_list_utc if in_utc else self._trans_list
+        idx = bisect.bisect_right(trans_list, timestamp)
+
+        # We want to know when the previous transition was, so subtract off 1
+        return idx - 1
+
+    def _get_ttinfo(self, idx):
+        # For no list or after the last transition, default to _ttinfo_std
+        if idx is None or (idx + 1) >= len(self._trans_list):
+            return self._ttinfo_std
+
+        # If there is a list and the time is before it, return _ttinfo_before
+        if idx < 0:
+            return self._ttinfo_before
+
+        return self._trans_idx[idx]
+
+    def _find_ttinfo(self, dt):
+        idx = self._resolve_ambiguous_time(dt)
+
+        return self._get_ttinfo(idx)
+
+    def fromutc(self, dt):
+        """
+        The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
+
+        :param dt:
+            A :py:class:`datetime.datetime` object.
+
+        :raises TypeError:
+            Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
+
+        :raises ValueError:
+            Raised if this is called with a ``dt`` which does not have this
+            ``tzinfo`` attached.
+
+        :return:
+            Returns a :py:class:`datetime.datetime` object representing the
+            wall time in ``self``'s time zone.
+        """
+        # These isinstance checks are in datetime.tzinfo, so we'll preserve
+        # them, even if we don't care about duck typing.
+        if not isinstance(dt, datetime.datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        # First treat UTC as wall time and get the transition we're in.
+        idx = self._find_last_transition(dt, in_utc=True)
+        tti = self._get_ttinfo(idx)
+
+        dt_out = dt + datetime.timedelta(seconds=tti.offset)
+
+        fold = self.is_ambiguous(dt_out, idx=idx)
+
+        return enfold(dt_out, fold=int(fold))
+
+    def is_ambiguous(self, dt, idx=None):
+        """
+        Whether or not the "wall time" of a given datetime is ambiguous in this
+        zone.
+
+        :param dt:
+            A :py:class:`datetime.datetime`, naive or time zone aware.
+
+
+        :return:
+            Returns ``True`` if ambiguous, ``False`` otherwise.
+
+        .. versionadded:: 2.6.0
+        """
+        if idx is None:
+            idx = self._find_last_transition(dt)
+
+        # Calculate the difference in offsets from current to previous
+        timestamp = _datetime_to_timestamp(dt)
+        tti = self._get_ttinfo(idx)
+
+        if idx is None or idx <= 0:
+            return False
+
+        od = self._get_ttinfo(idx - 1).offset - tti.offset
+        tt = self._trans_list[idx]          # Transition time
+
+        return timestamp < tt + od
+
+    def _resolve_ambiguous_time(self, dt):
+        idx = self._find_last_transition(dt)
+
+        # If we have no transitions, return the index
+        _fold = self._fold(dt)
+        if idx is None or idx == 0:
+            return idx
+
+        # If it's ambiguous and we're in a fold, shift to a different index.
+        idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
+
+        return idx - idx_offset
+
+    def utcoffset(self, dt):
+        if dt is None:
+            return None
+
+        if not self._ttinfo_std:
+            return ZERO
+
+        return self._find_ttinfo(dt).delta
+
+    def dst(self, dt):
+        if dt is None:
+            return None
+
+        if not self._ttinfo_dst:
+            return ZERO
+
+        tti = self._find_ttinfo(dt)
+
+        if not tti.isdst:
+            return ZERO
+
+        # The documentation says that utcoffset()-dst() must
+        # be constant for every dt.
+        return tti.dstoffset
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        if not self._ttinfo_std or dt is None:
+            return None
+        return self._find_ttinfo(dt).abbr
+
+    def __eq__(self, other):
+        if not isinstance(other, tzfile):
+            return NotImplemented
+        return (self._trans_list == other._trans_list and
+                self._trans_idx == other._trans_idx and
+                self._ttinfo_list == other._ttinfo_list)
+
+    __hash__ = None
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
+
+    def __reduce__(self):
+        return self.__reduce_ex__(None)
+
+    def __reduce_ex__(self, protocol):
+        return (self.__class__, (None, self._filename), self.__dict__)
+
+
+class tzrange(tzrangebase):
+    """
+    The ``tzrange`` object is a time zone specified by a set of offsets and
+    abbreviations, equivalent to the way the ``TZ`` variable can be specified
+    in POSIX-like systems, but using Python delta objects to specify DST
+    start, end and offsets.
+
+    :param stdabbr:
+        The abbreviation for standard time (e.g. ``'EST'``).
+
+    :param stdoffset:
+        An integer or :class:`datetime.timedelta` object or equivalent
+        specifying the base offset from UTC.
+
+        If unspecified, +00:00 is used.
+
+    :param dstabbr:
+        The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
+
+        If specified, with no other DST information, DST is assumed to occur
+        and the default behavior or ``dstoffset``, ``start`` and ``end`` is
+        used. If unspecified and no other DST information is specified, it
+        is assumed that this zone has no DST.
+
+        If this is unspecified and other DST information is *is* specified,
+        DST occurs in the zone but the time zone abbreviation is left
+        unchanged.
+
+    :param dstoffset:
+        A an integer or :class:`datetime.timedelta` object or equivalent
+        specifying the UTC offset during DST. If unspecified and any other DST
+        information is specified, it is assumed to be the STD offset +1 hour.
+
+    :param start:
+        A :class:`relativedelta.relativedelta` object or equivalent specifying
+        the time and time of year that daylight savings time starts. To
+        specify, for example, that DST starts at 2AM on the 2nd Sunday in
+        March, pass:
+
+            ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
+
+        If unspecified and any other DST information is specified, the default
+        value is 2 AM on the first Sunday in April.
+
+    :param end:
+        A :class:`relativedelta.relativedelta` object or equivalent
+        representing the time and time of year that daylight savings time
+        ends, with the same specification method as in ``start``. One note is
+        that this should point to the first time in the *standard* zone, so if
+        a transition occurs at 2AM in the DST zone and the clocks are set back
+        1 hour to 1AM, set the ``hours`` parameter to +1.
+
+
+    **Examples:**
+
+    .. testsetup:: tzrange
+
+        from dateutil.tz import tzrange, tzstr
+
+    .. doctest:: tzrange
+
+        >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
+        True
+
+        >>> from dateutil.relativedelta import *
+        >>> range1 = tzrange("EST", -18000, "EDT")
+        >>> range2 = tzrange("EST", -18000, "EDT", -14400,
+        ...                  relativedelta(hours=+2, month=4, day=1,
+        ...                                weekday=SU(+1)),
+        ...                  relativedelta(hours=+1, month=10, day=31,
+        ...                                weekday=SU(-1)))
+        >>> tzstr('EST5EDT') == range1 == range2
+        True
+
+    """
+    def __init__(self, stdabbr, stdoffset=None,
+                 dstabbr=None, dstoffset=None,
+                 start=None, end=None):
+
+        global relativedelta
+        from dateutil import relativedelta
+
+        self._std_abbr = stdabbr
+        self._dst_abbr = dstabbr
+
+        try:
+            stdoffset = stdoffset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            dstoffset = dstoffset.total_seconds()
+        except (TypeError, AttributeError):
+            pass
+
+        if stdoffset is not None:
+            self._std_offset = datetime.timedelta(seconds=stdoffset)
+        else:
+            self._std_offset = ZERO
+
+        if dstoffset is not None:
+            self._dst_offset = datetime.timedelta(seconds=dstoffset)
+        elif dstabbr and stdoffset is not None:
+            self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
+        else:
+            self._dst_offset = ZERO
+
+        if dstabbr and start is None:
+            self._start_delta = relativedelta.relativedelta(
+                hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
+        else:
+            self._start_delta = start
+
+        if dstabbr and end is None:
+            self._end_delta = relativedelta.relativedelta(
+                hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
+        else:
+            self._end_delta = end
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = bool(self._start_delta)
+
+    def transitions(self, year):
+        """
+        For a given year, get the DST on and off transition times, expressed
+        always on the standard time side. For zones with no transitions, this
+        function returns ``None``.
+
+        :param year:
+            The year whose transitions you would like to query.
+
+        :return:
+            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+            ``(dston, dstoff)`` for zones with an annual DST transition, or
+            ``None`` for fixed offset zones.
+        """
+        if not self.hasdst:
+            return None
+
+        base_year = datetime.datetime(year, 1, 1)
+
+        start = base_year + self._start_delta
+        end = base_year + self._end_delta
+
+        return (start, end)
+
+    def __eq__(self, other):
+        if not isinstance(other, tzrange):
+            return NotImplemented
+
+        return (self._std_abbr == other._std_abbr and
+                self._dst_abbr == other._dst_abbr and
+                self._std_offset == other._std_offset and
+                self._dst_offset == other._dst_offset and
+                self._start_delta == other._start_delta and
+                self._end_delta == other._end_delta)
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_base_offset_
+
+
+@six.add_metaclass(_TzStrFactory)
+class tzstr(tzrange):
+    """
+    ``tzstr`` objects are time zone objects specified by a time-zone string as
+    it would be passed to a ``TZ`` variable on POSIX-style systems (see
+    the `GNU C Library: TZ Variable`_ for more details).
+
+    There is one notable exception, which is that POSIX-style time zones use an
+    inverted offset format, so normally ``GMT+3`` would be parsed as an offset
+    3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
+    offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
+    behavior, pass a ``True`` value to ``posix_offset``.
+
+    The :class:`tzrange` object provides the same functionality, but is
+    specified using :class:`relativedelta.relativedelta` objects. rather than
+    strings.
+
+    :param s:
+        A time zone string in ``TZ`` variable format. This can be a
+        :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
+        :class:`unicode`) or a stream emitting unicode characters
+        (e.g. :class:`StringIO`).
+
+    :param posix_offset:
+        Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
+        ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
+        POSIX standard.
+
+    .. caution::
+
+        Prior to version 2.7.0, this function also supported time zones
+        in the format:
+
+            * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
+            * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
+
+        This format is non-standard and has been deprecated; this function
+        will raise a :class:`DeprecatedTZFormatWarning` until
+        support is removed in a future version.
+
+    .. _`GNU C Library: TZ Variable`:
+        https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+    """
+    def __init__(self, s, posix_offset=False):
+        global parser
+        from dateutil.parser import _parser as parser
+
+        self._s = s
+
+        res = parser._parsetz(s)
+        if res is None or res.any_unused_tokens:
+            raise ValueError("unknown string format")
+
+        # Here we break the compatibility with the TZ variable handling.
+        # GMT-3 actually *means* the timezone -3.
+        if res.stdabbr in ("GMT", "UTC") and not posix_offset:
+            res.stdoffset *= -1
+
+        # We must initialize it first, since _delta() needs
+        # _std_offset and _dst_offset set. Use False in start/end
+        # to avoid building it two times.
+        tzrange.__init__(self, res.stdabbr, res.stdoffset,
+                         res.dstabbr, res.dstoffset,
+                         start=False, end=False)
+
+        if not res.dstabbr:
+            self._start_delta = None
+            self._end_delta = None
+        else:
+            self._start_delta = self._delta(res.start)
+            if self._start_delta:
+                self._end_delta = self._delta(res.end, isend=1)
+
+        self.hasdst = bool(self._start_delta)
+
+    def _delta(self, x, isend=0):
+        from dateutil import relativedelta
+        kwargs = {}
+        if x.month is not None:
+            kwargs["month"] = x.month
+            if x.weekday is not None:
+                kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
+                if x.week > 0:
+                    kwargs["day"] = 1
+                else:
+                    kwargs["day"] = 31
+            elif x.day:
+                kwargs["day"] = x.day
+        elif x.yday is not None:
+            kwargs["yearday"] = x.yday
+        elif x.jyday is not None:
+            kwargs["nlyearday"] = x.jyday
+        if not kwargs:
+            # Default is to start on first sunday of april, and end
+            # on last sunday of october.
+            if not isend:
+                kwargs["month"] = 4
+                kwargs["day"] = 1
+                kwargs["weekday"] = relativedelta.SU(+1)
+            else:
+                kwargs["month"] = 10
+                kwargs["day"] = 31
+                kwargs["weekday"] = relativedelta.SU(-1)
+        if x.time is not None:
+            kwargs["seconds"] = x.time
+        else:
+            # Default is 2AM.
+            kwargs["seconds"] = 7200
+        if isend:
+            # Convert to standard time, to follow the documented way
+            # of working with the extra hour. See the documentation
+            # of the tzinfo class.
+            delta = self._dst_offset - self._std_offset
+            kwargs["seconds"] -= delta.seconds + delta.days * 86400
+        return relativedelta.relativedelta(**kwargs)
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+class _tzicalvtzcomp(object):
+    def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
+                 tzname=None, rrule=None):
+        self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
+        self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
+        self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
+        self.isdst = isdst
+        self.tzname = tzname
+        self.rrule = rrule
+
+
+class _tzicalvtz(_tzinfo):
+    def __init__(self, tzid, comps=[]):
+        super(_tzicalvtz, self).__init__()
+
+        self._tzid = tzid
+        self._comps = comps
+        self._cachedate = []
+        self._cachecomp = []
+        self._cache_lock = _thread.allocate_lock()
+
+    def _find_comp(self, dt):
+        if len(self._comps) == 1:
+            return self._comps[0]
+
+        dt = dt.replace(tzinfo=None)
+
+        try:
+            with self._cache_lock:
+                return self._cachecomp[self._cachedate.index(
+                    (dt, self._fold(dt)))]
+        except ValueError:
+            pass
+
+        lastcompdt = None
+        lastcomp = None
+
+        for comp in self._comps:
+            compdt = self._find_compdt(comp, dt)
+
+            if compdt and (not lastcompdt or lastcompdt < compdt):
+                lastcompdt = compdt
+                lastcomp = comp
+
+        if not lastcomp:
+            # RFC says nothing about what to do when a given
+            # time is before the first onset date. We'll look for the
+            # first standard component, or the first component, if
+            # none is found.
+            for comp in self._comps:
+                if not comp.isdst:
+                    lastcomp = comp
+                    break
+            else:
+                lastcomp = comp[0]
+
+        with self._cache_lock:
+            self._cachedate.insert(0, (dt, self._fold(dt)))
+            self._cachecomp.insert(0, lastcomp)
+
+            if len(self._cachedate) > 10:
+                self._cachedate.pop()
+                self._cachecomp.pop()
+
+        return lastcomp
+
+    def _find_compdt(self, comp, dt):
+        if comp.tzoffsetdiff < ZERO and self._fold(dt):
+            dt -= comp.tzoffsetdiff
+
+        compdt = comp.rrule.before(dt, inc=True)
+
+        return compdt
+
+    def utcoffset(self, dt):
+        if dt is None:
+            return None
+
+        return self._find_comp(dt).tzoffsetto
+
+    def dst(self, dt):
+        comp = self._find_comp(dt)
+        if comp.isdst:
+            return comp.tzoffsetdiff
+        else:
+            return ZERO
+
+    @tzname_in_python2
+    def tzname(self, dt):
+        return self._find_comp(dt).tzname
+
+    def __repr__(self):
+        return "<tzicalvtz %s>" % repr(self._tzid)
+
+    __reduce__ = object.__reduce__
+
+
+class tzical(object):
+    """
+    This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
+    as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
+
+    :param `fileobj`:
+        A file or stream in iCalendar format, which should be UTF-8 encoded
+        with CRLF endings.
+
+    .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
+    """
+    def __init__(self, fileobj):
+        global rrule
+        from dateutil import rrule
+
+        if isinstance(fileobj, string_types):
+            self._s = fileobj
+            # ical should be encoded in UTF-8 with CRLF
+            fileobj = open(fileobj, 'r')
+        else:
+            self._s = getattr(fileobj, 'name', repr(fileobj))
+            fileobj = _ContextWrapper(fileobj)
+
+        self._vtz = {}
+
+        with fileobj as fobj:
+            self._parse_rfc(fobj.read())
+
+    def keys(self):
+        """
+        Retrieves the available time zones as a list.
+        """
+        return list(self._vtz.keys())
+
+    def get(self, tzid=None):
+        """
+        Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
+
+        :param tzid:
+            If there is exactly one time zone available, omitting ``tzid``
+            or passing :py:const:`None` value returns it. Otherwise a valid
+            key (which can be retrieved from :func:`keys`) is required.
+
+        :raises ValueError:
+            Raised if ``tzid`` is not specified but there are either more
+            or fewer than 1 zone defined.
+
+        :returns:
+            Returns either a :py:class:`datetime.tzinfo` object representing
+            the relevant time zone or :py:const:`None` if the ``tzid`` was
+            not found.
+        """
+        if tzid is None:
+            if len(self._vtz) == 0:
+                raise ValueError("no timezones defined")
+            elif len(self._vtz) > 1:
+                raise ValueError("more than one timezone available")
+            tzid = next(iter(self._vtz))
+
+        return self._vtz.get(tzid)
+
+    def _parse_offset(self, s):
+        s = s.strip()
+        if not s:
+            raise ValueError("empty offset")
+        if s[0] in ('+', '-'):
+            signal = (-1, +1)[s[0] == '+']
+            s = s[1:]
+        else:
+            signal = +1
+        if len(s) == 4:
+            return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
+        elif len(s) == 6:
+            return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
+        else:
+            raise ValueError("invalid offset: " + s)
+
+    def _parse_rfc(self, s):
+        lines = s.splitlines()
+        if not lines:
+            raise ValueError("empty string")
+
+        # Unfold
+        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
+
+        tzid = None
+        comps = []
+        invtz = False
+        comptype = None
+        for line in lines:
+            if not line:
+                continue
+            name, value = line.split(':', 1)
+            parms = name.split(';')
+            if not parms:
+                raise ValueError("empty property name")
+            name = parms[0].upper()
+            parms = parms[1:]
+            if invtz:
+                if name == "BEGIN":
+                    if value in ("STANDARD", "DAYLIGHT"):
+                        # Process component
+                        pass
+                    else:
+                        raise ValueError("unknown component: "+value)
+                    comptype = value
+                    founddtstart = False
+                    tzoffsetfrom = None
+                    tzoffsetto = None
+                    rrulelines = []
+                    tzname = None
+                elif name == "END":
+                    if value == "VTIMEZONE":
+                        if comptype:
+                            raise ValueError("component not closed: "+comptype)
+                        if not tzid:
+                            raise ValueError("mandatory TZID not found")
+                        if not comps:
+                            raise ValueError(
+                                "at least one component is needed")
+                        # Process vtimezone
+                        self._vtz[tzid] = _tzicalvtz(tzid, comps)
+                        invtz = False
+                    elif value == comptype:
+                        if not founddtstart:
+                            raise ValueError("mandatory DTSTART not found")
+                        if tzoffsetfrom is None:
+                            raise ValueError(
+                                "mandatory TZOFFSETFROM not found")
+                        if tzoffsetto is None:
+                            raise ValueError(
+                                "mandatory TZOFFSETFROM not found")
+                        # Process component
+                        rr = None
+                        if rrulelines:
+                            rr = rrule.rrulestr("\n".join(rrulelines),
+                                                compatible=True,
+                                                ignoretz=True,
+                                                cache=True)
+                        comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
+                                              (comptype == "DAYLIGHT"),
+                                              tzname, rr)
+                        comps.append(comp)
+                        comptype = None
+                    else:
+                        raise ValueError("invalid component end: "+value)
+                elif comptype:
+                    if name == "DTSTART":
+                        # DTSTART in VTIMEZONE takes a subset of valid RRULE
+                        # values under RFC 5545.
+                        for parm in parms:
+                            if parm != 'VALUE=DATE-TIME':
+                                msg = ('Unsupported DTSTART param in ' +
+                                       'VTIMEZONE: ' + parm)
+                                raise ValueError(msg)
+                        rrulelines.append(line)
+                        founddtstart = True
+                    elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
+                        rrulelines.append(line)
+                    elif name == "TZOFFSETFROM":
+                        if parms:
+                            raise ValueError(
+                                "unsupported %s parm: %s " % (name, parms[0]))
+                        tzoffsetfrom = self._parse_offset(value)
+                    elif name == "TZOFFSETTO":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZOFFSETTO parm: "+parms[0])
+                        tzoffsetto = self._parse_offset(value)
+                    elif name == "TZNAME":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZNAME parm: "+parms[0])
+                        tzname = value
+                    elif name == "COMMENT":
+                        pass
+                    else:
+                        raise ValueError("unsupported property: "+name)
+                else:
+                    if name == "TZID":
+                        if parms:
+                            raise ValueError(
+                                "unsupported TZID parm: "+parms[0])
+                        tzid = value
+                    elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
+                        pass
+                    else:
+                        raise ValueError("unsupported property: "+name)
+            elif name == "BEGIN" and value == "VTIMEZONE":
+                tzid = None
+                comps = []
+                invtz = True
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
+
+
+if sys.platform != "win32":
+    TZFILES = ["/etc/localtime", "localtime"]
+    TZPATHS = ["/usr/share/zoneinfo",
+               "/usr/lib/zoneinfo",
+               "/usr/share/lib/zoneinfo",
+               "/etc/zoneinfo"]
+else:
+    TZFILES = []
+    TZPATHS = []
+
+
+def __get_gettz():
+    tzlocal_classes = (tzlocal,)
+    if tzwinlocal is not None:
+        tzlocal_classes += (tzwinlocal,)
+
+    class GettzFunc(object):
+        """
+        Retrieve a time zone object from a string representation
+
+        This function is intended to retrieve the :py:class:`tzinfo` subclass
+        that best represents the time zone that would be used if a POSIX
+        `TZ variable`_ were set to the same value.
+
+        If no argument or an empty string is passed to ``gettz``, local time
+        is returned:
+
+        .. code-block:: python3
+
+            >>> gettz()
+            tzfile('/etc/localtime')
+
+        This function is also the preferred way to map IANA tz database keys
+        to :class:`tzfile` objects:
+
+        .. code-block:: python3
+
+            >>> gettz('Pacific/Kiritimati')
+            tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
+
+        On Windows, the standard is extended to include the Windows-specific
+        zone names provided by the operating system:
+
+        .. code-block:: python3
+
+            >>> gettz('Egypt Standard Time')
+            tzwin('Egypt Standard Time')
+
+        Passing a GNU ``TZ`` style string time zone specification returns a
+        :class:`tzstr` object:
+
+        .. code-block:: python3
+
+            >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+            tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+
+        :param name:
+            A time zone name (IANA, or, on Windows, Windows keys), location of
+            a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
+            specifier. An empty string, no argument or ``None`` is interpreted
+            as local time.
+
+        :return:
+            Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
+            subclasses.
+
+        .. versionchanged:: 2.7.0
+
+            After version 2.7.0, any two calls to ``gettz`` using the same
+            input strings will return the same object:
+
+            .. code-block:: python3
+
+                >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
+                True
+
+            In addition to improving performance, this ensures that
+            `"same zone" semantics`_ are used for datetimes in the same zone.
+
+
+        .. _`TZ variable`:
+            https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+
+        .. _`"same zone" semantics`:
+            https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
+        """
+        def __init__(self):
+
+            self.__instances = {}
+            self._cache_lock = _thread.allocate_lock()
+
+        def __call__(self, name=None):
+            with self._cache_lock:
+                rv = self.__instances.get(name, None)
+
+                if rv is None:
+                    rv = self.nocache(name=name)
+                    if not (name is None or isinstance(rv, tzlocal_classes)):
+                        # tzlocal is slightly more complicated than the other
+                        # time zone providers because it depends on environment
+                        # at construction time, so don't cache that.
+                        self.__instances[name] = rv
+
+            return rv
+
+        def cache_clear(self):
+            with self._cache_lock:
+                self.__instances = {}
+
+        @staticmethod
+        def nocache(name=None):
+            """A non-cached version of gettz"""
+            tz = None
+            if not name:
+                try:
+                    name = os.environ["TZ"]
+                except KeyError:
+                    pass
+            if name is None or name == ":":
+                for filepath in TZFILES:
+                    if not os.path.isabs(filepath):
+                        filename = filepath
+                        for path in TZPATHS:
+                            filepath = os.path.join(path, filename)
+                            if os.path.isfile(filepath):
+                                break
+                        else:
+                            continue
+                    if os.path.isfile(filepath):
+                        try:
+                            tz = tzfile(filepath)
+                            break
+                        except (IOError, OSError, ValueError):
+                            pass
+                else:
+                    tz = tzlocal()
+            else:
+                if name.startswith(":"):
+                    name = name[1:]
+                if os.path.isabs(name):
+                    if os.path.isfile(name):
+                        tz = tzfile(name)
+                    else:
+                        tz = None
+                else:
+                    for path in TZPATHS:
+                        filepath = os.path.join(path, name)
+                        if not os.path.isfile(filepath):
+                            filepath = filepath.replace(' ', '_')
+                            if not os.path.isfile(filepath):
+                                continue
+                        try:
+                            tz = tzfile(filepath)
+                            break
+                        except (IOError, OSError, ValueError):
+                            pass
+                    else:
+                        tz = None
+                        if tzwin is not None:
+                            try:
+                                tz = tzwin(name)
+                            except WindowsError:
+                                tz = None
+
+                        if not tz:
+                            from dateutil.zoneinfo import get_zonefile_instance
+                            tz = get_zonefile_instance().get(name)
+
+                        if not tz:
+                            for c in name:
+                                # name is not a tzstr unless it has at least
+                                # one offset. For short values of "name", an
+                                # explicit for loop seems to be the fastest way
+                                # To determine if a string contains a digit
+                                if c in "0123456789":
+                                    try:
+                                        tz = tzstr(name)
+                                    except ValueError:
+                                        pass
+                                    break
+                            else:
+                                if name in ("GMT", "UTC"):
+                                    tz = tzutc()
+                                elif name in time.tzname:
+                                    tz = tzlocal()
+            return tz
+
+    return GettzFunc()
+
+
+gettz = __get_gettz()
+del __get_gettz
+
+
+def datetime_exists(dt, tz=None):
+    """
+    Given a datetime and a time zone, determine whether or not a given datetime
+    would fall in a gap.
+
+    :param dt:
+        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+        is provided.)
+
+    :param tz:
+        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+        ``None`` or not provided, the datetime's own time zone will be used.
+
+    :return:
+        Returns a boolean value whether or not the "wall time" exists in
+        ``tz``.
+
+    .. versionadded:: 2.7.0
+    """
+    if tz is None:
+        if dt.tzinfo is None:
+            raise ValueError('Datetime is naive and no time zone provided.')
+        tz = dt.tzinfo
+
+    dt = dt.replace(tzinfo=None)
+
+    # This is essentially a test of whether or not the datetime can survive
+    # a round trip to UTC.
+    dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz)
+    dt_rt = dt_rt.replace(tzinfo=None)
+
+    return dt == dt_rt
+
+
+def datetime_ambiguous(dt, tz=None):
+    """
+    Given a datetime and a time zone, determine whether or not a given datetime
+    is ambiguous (i.e if there are two times differentiated only by their DST
+    status).
+
+    :param dt:
+        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
+        is provided.)
+
+    :param tz:
+        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
+        ``None`` or not provided, the datetime's own time zone will be used.
+
+    :return:
+        Returns a boolean value whether or not the "wall time" is ambiguous in
+        ``tz``.
+
+    .. versionadded:: 2.6.0
+    """
+    if tz is None:
+        if dt.tzinfo is None:
+            raise ValueError('Datetime is naive and no time zone provided.')
+
+        tz = dt.tzinfo
+
+    # If a time zone defines its own "is_ambiguous" function, we'll use that.
+    is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
+    if is_ambiguous_fn is not None:
+        try:
+            return tz.is_ambiguous(dt)
+        except Exception:
+            pass
+
+    # If it doesn't come out and tell us it's ambiguous, we'll just check if
+    # the fold attribute has any effect on this particular date and time.
+    dt = dt.replace(tzinfo=tz)
+    wall_0 = enfold(dt, fold=0)
+    wall_1 = enfold(dt, fold=1)
+
+    same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+    same_dst = wall_0.dst() == wall_1.dst()
+
+    return not (same_offset and same_dst)
+
+
+def resolve_imaginary(dt):
+    """
+    Given a datetime that may be imaginary, return an existing datetime.
+
+    This function assumes that an imaginary datetime represents what the
+    wall time would be in a zone had the offset transition not occurred, so
+    it will always fall forward by the transition's change in offset.
+
+    .. doctest::
+
+        >>> from dateutil import tz
+        >>> from datetime import datetime
+        >>> NYC = tz.gettz('America/New_York')
+        >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
+        2017-03-12 03:30:00-04:00
+
+        >>> KIR = tz.gettz('Pacific/Kiritimati')
+        >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
+        1995-01-02 12:30:00+14:00
+
+    As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
+    existing datetime, so a round-trip to and from UTC is sufficient to get
+    an extant datetime, however, this generally "falls back" to an earlier time
+    rather than falling forward to the STD side (though no guarantees are made
+    about this behavior).
+
+    :param dt:
+        A :class:`datetime.datetime` which may or may not exist.
+
+    :return:
+        Returns an existing :class:`datetime.datetime`. If ``dt`` was not
+        imaginary, the datetime returned is guaranteed to be the same object
+        passed to the function.
+
+    .. versionadded:: 2.7.0
+    """
+    if dt.tzinfo is not None and not datetime_exists(dt):
+
+        curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
+        old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
+
+        dt += curr_offset - old_offset
+
+    return dt
+
+
+def _datetime_to_timestamp(dt):
+    """
+    Convert a :class:`datetime.datetime` object to an epoch timestamp in
+    seconds since January 1, 1970, ignoring the time zone.
+    """
+    return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
+
+
+class _ContextWrapper(object):
+    """
+    Class for wrapping contexts so that they are passed through in a
+    with statement.
+    """
+    def __init__(self, context):
+        self.context = context
+
+    def __enter__(self):
+        return self.context
+
+    def __exit__(*args, **kwargs):
+        pass
+
+# vim:ts=4:sw=4:et
diff --git a/resources/lib/libraries/dateutil/tz/win.py b/resources/lib/libraries/dateutil/tz/win.py
new file mode 100644
index 00000000..def4353a
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tz/win.py
@@ -0,0 +1,331 @@
+# This code was originally contributed by Jeffrey Harris.
+import datetime
+import struct
+
+from six.moves import winreg
+from six import text_type
+
+try:
+    import ctypes
+    from ctypes import wintypes
+except ValueError:
+    # ValueError is raised on non-Windows systems for some horrible reason.
+    raise ImportError("Running tzwin on non-Windows system")
+
+from ._common import tzrangebase
+
+__all__ = ["tzwin", "tzwinlocal", "tzres"]
+
+ONEWEEK = datetime.timedelta(7)
+
+TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
+TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+
+
+def _settzkeyname():
+    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+    try:
+        winreg.OpenKey(handle, TZKEYNAMENT).Close()
+        TZKEYNAME = TZKEYNAMENT
+    except WindowsError:
+        TZKEYNAME = TZKEYNAME9X
+    handle.Close()
+    return TZKEYNAME
+
+
+TZKEYNAME = _settzkeyname()
+
+
+class tzres(object):
+    """
+    Class for accessing `tzres.dll`, which contains timezone name related
+    resources.
+
+    .. versionadded:: 2.5.0
+    """
+    p_wchar = ctypes.POINTER(wintypes.WCHAR)        # Pointer to a wide char
+
+    def __init__(self, tzres_loc='tzres.dll'):
+        # Load the user32 DLL so we can load strings from tzres
+        user32 = ctypes.WinDLL('user32')
+
+        # Specify the LoadStringW function
+        user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
+                                       wintypes.UINT,
+                                       wintypes.LPWSTR,
+                                       ctypes.c_int)
+
+        self.LoadStringW = user32.LoadStringW
+        self._tzres = ctypes.WinDLL(tzres_loc)
+        self.tzres_loc = tzres_loc
+
+    def load_name(self, offset):
+        """
+        Load a timezone name from a DLL offset (integer).
+
+        >>> from dateutil.tzwin import tzres
+        >>> tzr = tzres()
+        >>> print(tzr.load_name(112))
+        'Eastern Standard Time'
+
+        :param offset:
+            A positive integer value referring to a string from the tzres dll.
+
+        ..note:
+            Offsets found in the registry are generally of the form
+            `@tzres.dll,-114`. The offset in this case if 114, not -114.
+
+        """
+        resource = self.p_wchar()
+        lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
+        nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
+        return resource[:nchar]
+
+    def name_from_string(self, tzname_str):
+        """
+        Parse strings as returned from the Windows registry into the time zone
+        name as defined in the registry.
+
+        >>> from dateutil.tzwin import tzres
+        >>> tzr = tzres()
+        >>> print(tzr.name_from_string('@tzres.dll,-251'))
+        'Dateline Daylight Time'
+        >>> print(tzr.name_from_string('Eastern Standard Time'))
+        'Eastern Standard Time'
+
+        :param tzname_str:
+            A timezone name string as returned from a Windows registry key.
+
+        :return:
+            Returns the localized timezone string from tzres.dll if the string
+            is of the form `@tzres.dll,-offset`, else returns the input string.
+        """
+        if not tzname_str.startswith('@'):
+            return tzname_str
+
+        name_splt = tzname_str.split(',-')
+        try:
+            offset = int(name_splt[1])
+        except:
+            raise ValueError("Malformed timezone string.")
+
+        return self.load_name(offset)
+
+
+class tzwinbase(tzrangebase):
+    """tzinfo class based on win32's timezones available in the registry."""
+    def __init__(self):
+        raise NotImplementedError('tzwinbase is an abstract base class')
+
+    def __eq__(self, other):
+        # Compare on all relevant dimensions, including name.
+        if not isinstance(other, tzwinbase):
+            return NotImplemented
+
+        return  (self._std_offset == other._std_offset and
+                 self._dst_offset == other._dst_offset and
+                 self._stddayofweek == other._stddayofweek and
+                 self._dstdayofweek == other._dstdayofweek and
+                 self._stdweeknumber == other._stdweeknumber and
+                 self._dstweeknumber == other._dstweeknumber and
+                 self._stdhour == other._stdhour and
+                 self._dsthour == other._dsthour and
+                 self._stdminute == other._stdminute and
+                 self._dstminute == other._dstminute and
+                 self._std_abbr == other._std_abbr and
+                 self._dst_abbr == other._dst_abbr)
+
+    @staticmethod
+    def list():
+        """Return a list of all time zones known to the system."""
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
+                result = [winreg.EnumKey(tzkey, i)
+                          for i in range(winreg.QueryInfoKey(tzkey)[0])]
+        return result
+
+    def display(self):
+        return self._display
+
+    def transitions(self, year):
+        """
+        For a given year, get the DST on and off transition times, expressed
+        always on the standard time side. For zones with no transitions, this
+        function returns ``None``.
+
+        :param year:
+            The year whose transitions you would like to query.
+
+        :return:
+            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
+            ``(dston, dstoff)`` for zones with an annual DST transition, or
+            ``None`` for fixed offset zones.
+        """
+
+        if not self.hasdst:
+            return None
+
+        dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
+                               self._dsthour, self._dstminute,
+                               self._dstweeknumber)
+
+        dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
+                                self._stdhour, self._stdminute,
+                                self._stdweeknumber)
+
+        # Ambiguous dates default to the STD side
+        dstoff -= self._dst_base_offset
+
+        return dston, dstoff
+
+    def _get_hasdst(self):
+        return self._dstmonth != 0
+
+    @property
+    def _dst_base_offset(self):
+        return self._dst_base_offset_
+
+
+class tzwin(tzwinbase):
+
+    def __init__(self, name):
+        self._name = name
+
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
+            with winreg.OpenKey(handle, tzkeyname) as tzkey:
+                keydict = valuestodict(tzkey)
+
+        self._std_abbr = keydict["Std"]
+        self._dst_abbr = keydict["Dlt"]
+
+        self._display = keydict["Display"]
+
+        # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+        tup = struct.unpack("=3l16h", keydict["TZI"])
+        stdoffset = -tup[0]-tup[1]          # Bias + StandardBias * -1
+        dstoffset = stdoffset-tup[2]        # + DaylightBias * -1
+        self._std_offset = datetime.timedelta(minutes=stdoffset)
+        self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+        # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
+        # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
+        (self._stdmonth,
+         self._stddayofweek,   # Sunday = 0
+         self._stdweeknumber,  # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[4:9]
+
+        (self._dstmonth,
+         self._dstdayofweek,   # Sunday = 0
+         self._dstweeknumber,  # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[12:17]
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = self._get_hasdst()
+
+    def __repr__(self):
+        return "tzwin(%s)" % repr(self._name)
+
+    def __reduce__(self):
+        return (self.__class__, (self._name,))
+
+
+class tzwinlocal(tzwinbase):
+    def __init__(self):
+        with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
+            with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
+                keydict = valuestodict(tzlocalkey)
+
+            self._std_abbr = keydict["StandardName"]
+            self._dst_abbr = keydict["DaylightName"]
+
+            try:
+                tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
+                                                          sn=self._std_abbr)
+                with winreg.OpenKey(handle, tzkeyname) as tzkey:
+                    _keydict = valuestodict(tzkey)
+                    self._display = _keydict["Display"]
+            except OSError:
+                self._display = None
+
+        stdoffset = -keydict["Bias"]-keydict["StandardBias"]
+        dstoffset = stdoffset-keydict["DaylightBias"]
+
+        self._std_offset = datetime.timedelta(minutes=stdoffset)
+        self._dst_offset = datetime.timedelta(minutes=dstoffset)
+
+        # For reasons unclear, in this particular key, the day of week has been
+        # moved to the END of the SYSTEMTIME structure.
+        tup = struct.unpack("=8h", keydict["StandardStart"])
+
+        (self._stdmonth,
+         self._stdweeknumber,  # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[1:5]
+
+        self._stddayofweek = tup[7]
+
+        tup = struct.unpack("=8h", keydict["DaylightStart"])
+
+        (self._dstmonth,
+         self._dstweeknumber,  # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[1:5]
+
+        self._dstdayofweek = tup[7]
+
+        self._dst_base_offset_ = self._dst_offset - self._std_offset
+        self.hasdst = self._get_hasdst()
+
+    def __repr__(self):
+        return "tzwinlocal()"
+
+    def __str__(self):
+        # str will return the standard name, not the daylight name.
+        return "tzwinlocal(%s)" % repr(self._std_abbr)
+
+    def __reduce__(self):
+        return (self.__class__, ())
+
+
+def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
+    """ dayofweek == 0 means Sunday, whichweek 5 means last instance """
+    first = datetime.datetime(year, month, 1, hour, minute)
+
+    # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
+    # Because 7 % 7 = 0
+    weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
+    wd = weekdayone + ((whichweek - 1) * ONEWEEK)
+    if (wd.month != month):
+        wd -= ONEWEEK
+
+    return wd
+
+
+def valuestodict(key):
+    """Convert a registry key's values to a dictionary."""
+    dout = {}
+    size = winreg.QueryInfoKey(key)[1]
+    tz_res = None
+
+    for i in range(size):
+        key_name, value, dtype = winreg.EnumValue(key, i)
+        if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
+            # If it's a DWORD (32-bit integer), it's stored as unsigned - convert
+            # that to a proper signed integer
+            if value & (1 << 31):
+                value = value - (1 << 32)
+        elif dtype == winreg.REG_SZ:
+            # If it's a reference to the tzres DLL, load the actual string
+            if value.startswith('@tzres'):
+                tz_res = tz_res or tzres()
+                value = tz_res.name_from_string(value)
+
+            value = value.rstrip('\x00')    # Remove trailing nulls
+
+        dout[key_name] = value
+
+    return dout
diff --git a/resources/lib/libraries/dateutil/tzwin.py b/resources/lib/libraries/dateutil/tzwin.py
new file mode 100644
index 00000000..cebc673e
--- /dev/null
+++ b/resources/lib/libraries/dateutil/tzwin.py
@@ -0,0 +1,2 @@
+# tzwin has moved to dateutil.tz.win
+from .tz.win import *
diff --git a/resources/lib/libraries/dateutil/utils.py b/resources/lib/libraries/dateutil/utils.py
new file mode 100644
index 00000000..ebcce6aa
--- /dev/null
+++ b/resources/lib/libraries/dateutil/utils.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+"""
+This module offers general convenience and utility functions for dealing with
+datetimes.
+
+.. versionadded:: 2.7.0
+"""
+from __future__ import unicode_literals
+
+from datetime import datetime, time
+
+
+def today(tzinfo=None):
+    """
+    Returns a :py:class:`datetime` representing the current day at midnight
+
+    :param tzinfo:
+        The time zone to attach (also used to determine the current day).
+
+    :return:
+        A :py:class:`datetime.datetime` object representing the current day
+        at midnight.
+    """
+
+    dt = datetime.now(tzinfo)
+    return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
+
+
+def default_tzinfo(dt, tzinfo):
+    """
+    Sets the the ``tzinfo`` parameter on naive datetimes only
+
+    This is useful for example when you are provided a datetime that may have
+    either an implicit or explicit time zone, such as when parsing a time zone
+    string.
+
+    .. doctest::
+
+        >>> from dateutil.tz import tzoffset
+        >>> from dateutil.parser import parse
+        >>> from dateutil.utils import default_tzinfo
+        >>> dflt_tz = tzoffset("EST", -18000)
+        >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
+        2014-01-01 12:30:00+00:00
+        >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
+        2014-01-01 12:30:00-05:00
+
+    :param dt:
+        The datetime on which to replace the time zone
+
+    :param tzinfo:
+        The :py:class:`datetime.tzinfo` subclass instance to assign to
+        ``dt`` if (and only if) it is naive.
+
+    :return:
+        Returns an aware :py:class:`datetime.datetime`.
+    """
+    if dt.tzinfo is not None:
+        return dt
+    else:
+        return dt.replace(tzinfo=tzinfo)
+
+
+def within_delta(dt1, dt2, delta):
+    """
+    Useful for comparing two datetimes that may a negilible difference
+    to be considered equal.
+    """
+    delta = abs(delta)
+    difference = dt1 - dt2
+    return -delta <= difference <= delta
diff --git a/resources/lib/libraries/dateutil/zoneinfo/__init__.py b/resources/lib/libraries/dateutil/zoneinfo/__init__.py
new file mode 100644
index 00000000..34f11ad6
--- /dev/null
+++ b/resources/lib/libraries/dateutil/zoneinfo/__init__.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+import warnings
+import json
+
+from tarfile import TarFile
+from pkgutil import get_data
+from io import BytesIO
+
+from dateutil.tz import tzfile as _tzfile
+
+__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
+
+ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
+METADATA_FN = 'METADATA'
+
+
+class tzfile(_tzfile):
+    def __reduce__(self):
+        return (gettz, (self._filename,))
+
+
+def getzoneinfofile_stream():
+    try:
+        return BytesIO(get_data(__name__, ZONEFILENAME))
+    except IOError as e:  # TODO  switch to FileNotFoundError?
+        warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
+        return None
+
+
+class ZoneInfoFile(object):
+    def __init__(self, zonefile_stream=None):
+        if zonefile_stream is not None:
+            with TarFile.open(fileobj=zonefile_stream) as tf:
+                self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
+                              for zf in tf.getmembers()
+                              if zf.isfile() and zf.name != METADATA_FN}
+                # deal with links: They'll point to their parent object. Less
+                # waste of memory
+                links = {zl.name: self.zones[zl.linkname]
+                         for zl in tf.getmembers() if
+                         zl.islnk() or zl.issym()}
+                self.zones.update(links)
+                try:
+                    metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
+                    metadata_str = metadata_json.read().decode('UTF-8')
+                    self.metadata = json.loads(metadata_str)
+                except KeyError:
+                    # no metadata in tar file
+                    self.metadata = None
+        else:
+            self.zones = {}
+            self.metadata = None
+
+    def get(self, name, default=None):
+        """
+        Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
+        for retrieving zones from the zone dictionary.
+
+        :param name:
+            The name of the zone to retrieve. (Generally IANA zone names)
+
+        :param default:
+            The value to return in the event of a missing key.
+
+        .. versionadded:: 2.6.0
+
+        """
+        return self.zones.get(name, default)
+
+
+# The current API has gettz as a module function, although in fact it taps into
+# a stateful class. So as a workaround for now, without changing the API, we
+# will create a new "global" class instance the first time a user requests a
+# timezone. Ugly, but adheres to the api.
+#
+# TODO: Remove after deprecation period.
+_CLASS_ZONE_INSTANCE = []
+
+
+def get_zonefile_instance(new_instance=False):
+    """
+    This is a convenience function which provides a :class:`ZoneInfoFile`
+    instance using the data provided by the ``dateutil`` package. By default, it
+    caches a single instance of the ZoneInfoFile object and returns that.
+
+    :param new_instance:
+        If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
+        used as the cached instance for the next call. Otherwise, new instances
+        are created only as necessary.
+
+    :return:
+        Returns a :class:`ZoneInfoFile` object.
+
+    .. versionadded:: 2.6
+    """
+    if new_instance:
+        zif = None
+    else:
+        zif = getattr(get_zonefile_instance, '_cached_instance', None)
+
+    if zif is None:
+        zif = ZoneInfoFile(getzoneinfofile_stream())
+
+        get_zonefile_instance._cached_instance = zif
+
+    return zif
+
+
+def gettz(name):
+    """
+    This retrieves a time zone from the local zoneinfo tarball that is packaged
+    with dateutil.
+
+    :param name:
+        An IANA-style time zone name, as found in the zoneinfo file.
+
+    :return:
+        Returns a :class:`dateutil.tz.tzfile` time zone object.
+
+    .. warning::
+        It is generally inadvisable to use this function, and it is only
+        provided for API compatibility with earlier versions. This is *not*
+        equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
+        time zone based on the inputs, favoring system zoneinfo. This is ONLY
+        for accessing the dateutil-specific zoneinfo (which may be out of
+        date compared to the system zoneinfo).
+
+    .. deprecated:: 2.6
+        If you need to use a specific zoneinfofile over the system zoneinfo,
+        instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
+        :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
+
+        Use :func:`get_zonefile_instance` to retrieve an instance of the
+        dateutil-provided zoneinfo.
+    """
+    warnings.warn("zoneinfo.gettz() will be removed in future versions, "
+                  "to use the dateutil-provided zoneinfo files, instantiate a "
+                  "ZoneInfoFile object and use ZoneInfoFile.zones.get() "
+                  "instead. See the documentation for details.",
+                  DeprecationWarning)
+
+    if len(_CLASS_ZONE_INSTANCE) == 0:
+        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+    return _CLASS_ZONE_INSTANCE[0].zones.get(name)
+
+
+def gettz_db_metadata():
+    """ Get the zonefile metadata
+
+    See `zonefile_metadata`_
+
+    :returns:
+        A dictionary with the database metadata
+
+    .. deprecated:: 2.6
+        See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
+        query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
+    """
+    warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
+                  "versions, to use the dateutil-provided zoneinfo files, "
+                  "ZoneInfoFile object and query the 'metadata' attribute "
+                  "instead. See the documentation for details.",
+                  DeprecationWarning)
+
+    if len(_CLASS_ZONE_INSTANCE) == 0:
+        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
+    return _CLASS_ZONE_INSTANCE[0].metadata
diff --git a/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
new file mode 100644
index 00000000..e86b54fe
Binary files /dev/null and b/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz differ
diff --git a/resources/lib/libraries/dateutil/zoneinfo/rebuild.py b/resources/lib/libraries/dateutil/zoneinfo/rebuild.py
new file mode 100644
index 00000000..78f0d1a0
--- /dev/null
+++ b/resources/lib/libraries/dateutil/zoneinfo/rebuild.py
@@ -0,0 +1,53 @@
+import logging
+import os
+import tempfile
+import shutil
+import json
+from subprocess import check_call
+from tarfile import TarFile
+
+from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
+
+
+def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
+    """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
+
+    filename is the timezone tarball from ``ftp.iana.org/tz``.
+
+    """
+    tmpdir = tempfile.mkdtemp()
+    zonedir = os.path.join(tmpdir, "zoneinfo")
+    moduledir = os.path.dirname(__file__)
+    try:
+        with TarFile.open(filename) as tf:
+            for name in zonegroups:
+                tf.extract(name, tmpdir)
+            filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
+            try:
+                check_call(["zic", "-d", zonedir] + filepaths)
+            except OSError as e:
+                _print_on_nosuchfile(e)
+                raise
+        # write metadata file
+        with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
+            json.dump(metadata, f, indent=4, sort_keys=True)
+        target = os.path.join(moduledir, ZONEFILENAME)
+        with TarFile.open(target, "w:%s" % format) as tf:
+            for entry in os.listdir(zonedir):
+                entrypath = os.path.join(zonedir, entry)
+                tf.add(entrypath, entry)
+    finally:
+        shutil.rmtree(tmpdir)
+
+
+def _print_on_nosuchfile(e):
+    """Print helpful troubleshooting message
+
+    e is an exception raised by subprocess.check_call()
+
+    """
+    if e.errno == 2:
+        logging.error(
+            "Could not find zic. Perhaps you need to install "
+            "libc-bin or some other package that provides it, "
+            "or it's not in your PATH?")