[med-svn] [Git][med-team/python-ciso8601][master] 3 commits: Watch github repo instead of pypi.debian.net

Malihe Asemani gitlab at salsa.debian.org
Sun Apr 19 18:56:36 BST 2020



Malihe Asemani pushed to branch master at Debian Med / python-ciso8601


Commits:
5acdf28e by Malihe Asemani at 2020-04-19T08:39:28-05:00
Watch github repo instead of pypi.debian.net

- - - - -
26ebae52 by Malihe Asemani at 2020-04-19T12:52:45-05:00
New upstream version 2.1.3
- - - - -
b2881fe5 by Malihe Asemani at 2020-04-19T12:52:46-05:00
Update upstream source from tag 'upstream/2.1.3'

Update to upstream version '2.1.3'
with Debian dir 8c4797a95c088806260ffec6f2afe8b7080917b2
- - - - -


21 changed files:

- + .circleci/config.yml
- + .clang-format
- + .gitignore
- + CONTRIBUTING.md
- − PKG-INFO
- + benchmarking/README.rst
- + benchmarking/format_results.py
- + benchmarking/perform_comparison.py
- + benchmarking/requirements.txt
- + benchmarking/rst_include_replace.py
- + benchmarking/run_benchmarks.sh
- + benchmarking/tox.ini
- − ciso8601.egg-info/PKG-INFO
- − ciso8601.egg-info/SOURCES.txt
- − ciso8601.egg-info/dependency_links.txt
- − ciso8601.egg-info/top_level.txt
- debian/watch
- + generate_test_timestamps.py
- − setup.cfg
- + tests.py
- + tox.ini


Changes:

=====================================
.circleci/config.yml
=====================================
@@ -0,0 +1,67 @@
+version: 2
+
+workflows:
+  version: 2
+  workflow:
+    jobs:
+      - test-2.7
+      - test-3.4
+      - test-3.5
+      - test-3.6
+      - test-3.7
+      - test-3.8
+      - lint-rst
+
+defaults: &defaults
+  working_directory: ~/code
+  environment:
+    STRICT_WARNINGS: '1'
+  steps:
+  - checkout
+  - run:
+      name: Test
+      command: python setup.py test
+
+jobs:
+  test-2.7:
+    <<: *defaults
+    docker:
+    - image: circleci/python:2.7
+  test-3.4:
+    <<: *defaults
+    docker:
+    - image: circleci/python:3.4
+  test-3.5:
+    <<: *defaults
+    docker:
+    - image: circleci/python:3.5
+  test-3.6:
+    <<: *defaults
+    docker:
+    - image: circleci/python:3.6
+  test-3.7:
+    <<: *defaults
+    docker:
+    - image: circleci/python:3.7
+  test-3.8:
+    <<: *defaults
+    docker:
+    - image: circleci/python:3.8
+
+  lint-rst:
+    working_directory: ~/code
+    steps:
+    - checkout
+    - run:
+        name: Install lint tools
+        command: |
+            python3 -m venv venv
+            . venv/bin/activate
+            pip install Pygments restructuredtext-lint
+    - run:
+        name: Lint
+        command:  |
+            . venv/bin/activate
+            rst-lint --encoding=utf-8 README.rst
+    docker:
+    - image: circleci/python:3.8


=====================================
.clang-format
=====================================
@@ -0,0 +1,17 @@
+# A clang-format style that approximates Python's PEP 7
+# Useful for IDE integration
+BasedOnStyle: Google
+AlwaysBreakAfterReturnType: All
+AllowShortIfStatementsOnASingleLine: false
+AlignAfterOpenBracket: Align
+BreakBeforeBraces: Stroustrup
+ColumnLimit: 79
+DerivePointerAlignment: false
+IndentWidth: 4
+Language: Cpp
+PointerAlignment: Right
+ReflowComments: true
+SpaceBeforeParens: ControlStatements
+SpacesInParentheses: false
+TabWidth: 4
+UseTab: Never


=====================================
.gitignore
=====================================
@@ -0,0 +1,55 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+bin/
+build/
+develop-eggs/
+dist/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+*.eggs
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+
+# Translations
+*.mo
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# Rope
+.ropeproject
+
+# Django stuff:
+*.log
+*.pot
+
+# Sphinx documentation
+docs/_build/
+


=====================================
CONTRIBUTING.md
=====================================
@@ -0,0 +1,168 @@
+# Contributing to ciso8601
+
+:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
+
+The following is a set of guidelines for contributing to ciso8601, which are hosted in the [Close.io Organization](https://github.com/closeio) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+
+#### Table Of Contents
+
+[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
+
+* [Design Philosophy](#design-philosophy)
+
+[How Can I Contribute?](#how-can-i-contribute)
+  * [Reporting Bugs](#reporting-bugs)
+  * [Suggesting Enhancements](#suggesting-enhancements)
+  * [Developing ciso8601 code](#developing-ciso8601-code)
+    * [General Workflow](#general-workflow)
+    * [C Coding Style](#c-coding-style)
+    * [Supported Python Versions](#supported-python-versions)
+    * [Supported Operating Systems](#supported-operating-systems)
+    * [Functional Testing](#functional-testing)
+    * [Performance Benchmarking](#performance-benchmarking)
+    * [Documentation](#documentation)
+    * [Pull Requests](#pull-requests)
+
+## I don't want to read this whole thing I just have a question!!!
+
+Sure. First [search the existing issues](https://github.com/closeio/ciso8601/issues?utf8=%E2%9C%93&q=is%3Aissue) to see if one of the existing issues answers it. If not, simply [create an issue](https://github.com/closeio/ciso8601/issues/new) and ask your question.
+
+## Design Philosophy
+
+ciso8601's goal is to be the fastest ISO 8601 parser available for Python. It probably will never support the complete grammar of ISO 8601, but it will be correct for the chosen subset of the grammar. It will also be robust against non-conforming inputs. Beyond that, performance is king. 
+
+That said, some care should still be taken to ensure cross-platform compatibility and maintainability. For example, this means that we do not hand-code assembly instructions for a specific CPUs/architectures, and instead rely on the native C compilers to take advantage of specific hardware. We are not against the idea of platform-specific code in principle, but it would have to be shown to be produce sufficient benefits to warrant the additional maintenance overhead.
+
+## How Can I Contribute?
+
+### Reporting Bugs
+
+This section guides you through submitting a bug report for ciso8601. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
+
+Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
+
+> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
+
+#### Before Submitting A Bug Report
+
+* **Perform a [cursory search](https://github.com/closeio/ciso8601/issues?utf8=%E2%9C%93&q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
+
+#### How Do I Submit A (Good) Bug Report?
+
+Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on the repository and provide the following information.
+
+Explain the problem and include additional details to help maintainers reproduce the problem:
+
+* **Use a clear and descriptive title** for the issue to identify the problem.
+* **Describe the exact steps which reproduce the problem** in as many details as possible.
+* **Provide specific examples to demonstrate the steps**. Include snippets of code that reproduce the problem (Make sure to use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines) so that it gets formatted in a readable way).
+* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
+* **Explain which behavior you expected to see instead and why.**
+* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
+
+Include details about your configuration and environment:
+
+* **Which version of ciso8601 are you using?** You can get the exact version by running `pip list` in your terminal. If you are not using [the latest version](https://github.com/closeio/ciso8601/releases), does the problem still happen in the latest version?
+* **What's the name and version of the OS you're using**?
+
+### Suggesting Enhancements
+
+This section guides you through submitting an enhancement suggestion for ciso8601, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.
+
+Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion).
+
+#### Before Submitting An Enhancement Suggestion
+
+* **Perform a [cursory search](https://github.com/closeio/ciso8601/issues?utf8=%E2%9C%93&q=is%3Aissue)** to see if the enhancement has already been suggested. 
+
+If it has, don't create a new issue. Consider adding a :+1: [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the issue description. If you feel that your use case is sufficiently different, add a comment to the existing issue instead of opening a new one.
+
+#### How Do I Submit A (Good) Enhancement Suggestion?
+
+Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on the repository and provide the following information:
+
+* **Use a clear and descriptive title** for the issue to identify the suggestion.
+* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
+* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
+* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
+* **Explain why this enhancement would be useful** to most ciso8601 users and therefore should be implemented in ciso8601.
+* **List some other libraries where this enhancement exists** (if you know of any).
+* **Specify which version of ciso8601 you're using.** You can get the exact version by running `pip list` in your terminal. If you are not using [the latest version](https://github.com/closeio/ciso8601/releases), is the enhancement still needed in the latest version?
+* **Specify the name and version of the OS you're using.**
+
+### Developing ciso8601 code
+
+#### General Workflow
+
+ciso8601 uses the same contributor workflow as many other projects hosted on GitHub.
+
+1. Fork the [ciso8601 repo](https://github.com/closeio/ciso8601) (so it becomes `yourname/ciso8601`).
+1. Clone that repo (`git clone https://github.com/yourname/ciso8601.git`).
+1. Create a new branch (`git checkout -b my-descriptive-branch-name`).
+1. Make your changes and commit to that branch (`git commit`).
+1. Push your changes to GitHub (`git push`).
+1. Create a Pull Request within GitHub's UI.
+
+See [this guide](https://opensource.guide/how-to-contribute/#opening-a-pull-request) for more information about each step.
+
+#### C Coding Style
+
+ciso8601 tries to adhere to the [Python PEP 7](https://www.python.org/dev/peps/pep-0007/) style guide. 
+
+You can use [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html) to make this mostly automatic. The auto-formatting rules are defined in the [.clang-format](.clang-format) file. If you are using Visual Studio Code as your editor, you can use the ["C/C++"](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension and it will automatically start auto-formatting.
+
+#### Supported Python Versions
+
+ciso8601 supports a variety of cPython versions, including Python 2.7 (for the full list see the [README](README.rst)). Please make sure that you do not accidentally make use of features that are specific to certain versions of Python. Feel free to make use of modern features of the languages, but you also need to provide mechanisms to support the other versions as well.
+
+You can make use of `#ifdef` blocks within the code to make use of version specific features (there are already several examples throughout the code).   
+
+#### Supported Operating Systems
+
+ciso8601 supports running on multiple operating systems, including Windows. Notably, for Python 2.7 on Windows, the compiler (MSVC) places additional restrictions on the C language constructs you can use. Make sure to test changes on both a Windows (MSVC) and Linux (gcc) machine to ensure compatibility.
+
+#### Functional Testing
+
+ciso8601's functionality/unit tests are found in the [tests.py](tests.py) file. The [`tox`](https://tox.readthedocs.io/en/latest/) command can be used to run the tests:
+
+```bash
+pip install tox
+...
+tox
+```
+
+This will automatically run [nosetests](https://nose.readthedocs.io/en/latest/man.html) command (as specified in the [`tox.ini`](tox.ini) file) to find and run all the tests. Make sure that you have at least the latest stable Python 3 interpreter and the latest Python 2.7 interpreter installed.
+
+Any new functionality being developed for ciso8601 should also have tests being written for it. Tests should cover both the "sunny day" (expected, valid input) and "rainy day" (invalid input or error) cases.
+
+Many of ciso8601's functionality tests are auto-generated. The code that does this generation is found in the [`generate_test_timestamps.py`](generate_test_timestamps.py) file. It can sometimes be useful to print out all of the test cases and their expected outputs:
+
+```python
+from generate_test_timestamps import generate_valid_timestamp_and_datetime
+
+for timestamp, expected_datetime in generate_valid_timestamp_and_datetime():
+    print("Input: {0}, Expected: {1}".format(timestamp, expected_datetime))
+```
+
+#### Performance Benchmarking
+
+The ciso8601 project was born out of a need for a fast ISO 8601 parser. Therefore the project is concerned with the performance of the library.
+
+Changes should be assessed for their performance impact, and the results should be included as part of the Pull Request.
+
+#### Documentation
+
+All changes in functionality should be documented in the [`README.rst`](README.rst) file. Note that this file uses the [reStructuredText](https://en.wikipedia.org/wiki/ReStructuredText) format, since the file is rendered as part of [ciso8601's entry in PyPI](https://pypi.org/project/ciso8601/), which only supports reStructuredText.
+
+You can check your reStructured text for syntax errors using (restructuredtext-lint)[https://github.com/twolfson/restructuredtext-lint]:
+
+```
+pip install Pygments restructuredtext-lint
+rst-lint --encoding=utf-8 README.rst
+```
+
+#### Pull Requests
+
+* Follow the [C Code](#c-coding-style) style guide.
+* Document new code and functionality [See "Documentation"](#documentation)
+


=====================================
PKG-INFO deleted
=====================================
@@ -1,360 +0,0 @@
-Metadata-Version: 1.1
-Name: ciso8601
-Version: 2.1.3
-Summary: Fast ISO8601 date time parser for Python written in C
-Home-page: https://github.com/closeio/ciso8601
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: MIT
-Description: ========
-        ciso8601
-        ========
-        
-        .. image:: https://img.shields.io/circleci/project/github/closeio/ciso8601.svg
-            :target: https://circleci.com/gh/closeio/ciso8601/tree/master
-        
-        .. image:: https://img.shields.io/pypi/v/ciso8601.svg
-            :target: https://pypi.org/project/ciso8601/
-        
-        .. image:: https://img.shields.io/pypi/pyversions/ciso8601.svg
-            :target: https://pypi.org/project/ciso8601/
-        
-        ``ciso8601`` converts `ISO 8601`_ or `RFC 3339`_ date time strings into Python datetime objects.
-        
-        Since it's written as a C module, it is much faster than other Python libraries.
-        Tested with Python 2.7, 3.4, 3.5, 3.6, 3.7, 3.8.
-        
-        **Note:** ciso8601 doesn't support the entirety of the ISO 8601 spec, `only a popular subset`_.
-        
-        .. _ISO 8601: https://en.wikipedia.org/wiki/ISO_8601
-        .. _RFC 3339: https://tools.ietf.org/html/rfc3339
-        
-        .. _`only a popular subset`: https://github.com/closeio/ciso8601#supported-subset-of-iso-8601
-        
-        (Interested in working on projects like this? `Close`_ is looking for `great engineers`_ to join our team)
-        
-        .. _Close: https://close.com
-        .. _great engineers: https://jobs.close.com
-        
-        
-        .. contents:: Contents
-        
-        
-        Quick Start
-        -----------
-        
-        .. code:: bash
-        
-          % pip install ciso8601
-        
-        .. code:: python
-        
-          In [1]: import ciso8601
-        
-          In [2]: ciso8601.parse_datetime('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: datetime.datetime(2014, 12, 5, 12, 30, 45, 123456, tzinfo=pytz.FixedOffset(330))
-        
-          In [3]: ciso8601.parse_datetime('20141205T123045')
-          Out[3]: datetime.datetime(2014, 12, 5, 12, 30, 45)
-        
-        Migration to v2
-        ---------------
-        
-        Version 2.0.0 of ``ciso8601`` changed the core implementation. This was not entirely backwards compatible, and care should be taken when migrating
-        See `CHANGELOG`_ for the Migration Guide.
-        
-        .. _CHANGELOG: https://github.com/closeio/ciso8601/blob/master/CHANGELOG.md
-        
-        Error Handling
-        --------------
-        
-        Starting in v2.0.0, ``ciso8601`` offers strong guarantees when it comes to parsing strings.
-        
-        ``parse_datetime(dt: String): datetime`` is a function that takes a string and either:
-        
-        * Returns a properly parsed Python datetime, **if and only if** the **entire** string conforms to the supported subset of ISO 8601
-        * Raises a ``ValueError`` with a description of the reason why the string doesn't conform to the supported subset of ISO 8601
-        
-        If time zone information is provided, an aware datetime object will be returned. Otherwise, a naive datetime is returned.
-        
-        Benchmark
-        ---------
-        
-        Parsing a timestamp with no time zone information (ex. ``2014-01-09T21:48:00``):
-        
-        .. <include:benchmark_with_no_time_zone.rst>
-        
-        .. table:: 
-        
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |    Module     |Python 3.8|Python 3.7|Python 3.6|Python 3.5|Python 3.4|          Python 2.7           |Relative Slowdown (versus ciso8601, Python 3.8)|
-            +===============+==========+==========+==========+==========+==========+===============================+===============================================+
-            |ciso8601       |201 nsec  |157 nsec  |160 nsec  |139 nsec  |148 nsec  |147 nsec                       |N/A                                            |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |pendulum       |215 nsec  |232 nsec  |234 nsec  |205 nsec  |192 nsec  |9.44 usec                      |1.1x                                           |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |udatetime      |906 nsec  |1.06 usec |767 nsec  |702 nsec  |819 nsec  |923 nsec                       |4.5x                                           |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |str2date       |5.96 usec |7.75 usec |7.27 usec |6.84 usec |7.6 usec  |**Incorrect Result** (``None``)|29.7x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |isodate        |10.3 usec |10 usec   |11.1 usec |11.9 usec |12.3 usec |43.6 usec                      |51.3x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601utils   |10.3 usec |8.63 usec |9.16 usec |10.3 usec |9.58 usec |11.1 usec                      |51.5x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601        |10.9 usec |11.1 usec |10.5 usec |11.2 usec |11.5 usec |25.6 usec                      |54.2x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |PySO8601       |13.9 usec |21.9 usec |20.2 usec |15.9 usec |23.7 usec |16.4 usec                      |69.4x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |aniso8601      |14.5 usec |15 usec   |15.8 usec |15.9 usec |16.1 usec |17.2 usec                      |72.5x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |zulu           |25.3 usec |29.9 usec |28.2 usec |27.4 usec |33 usec   |N/A                            |126.3x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |maya           |42.9 usec |57.4 usec |58.2 usec |67.5 usec |87.6 usec |100 usec                       |213.7x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |arrow          |85.7 usec |81.8 usec |75.7 usec |78.7 usec |N/A       |93.9 usec                      |427.1x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |python-dateutil|122 usec  |82.7 usec |72.2 usec |77.1 usec |74.4 usec |131 usec                       |609.5x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |moment         |3.81 msec |4.46 msec |3.12 msec |3.66 msec |N/A       |3.59 msec                      |19011.9x                                       |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-        
-        ciso8601 takes 201 nsec, which is **1.1x faster than pendulum**, the next fastest ISO 8601 parser in this comparison.
-        
-        .. </include:benchmark_with_no_time_zone.rst>
-        
-        Parsing a timestamp with time zone information (ex. ``2014-01-09T21:48:00-05:30``):
-        
-        .. <include:benchmark_with_time_zone.rst>
-        
-        .. table:: 
-        
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |    Module     |          Python 3.8           |          Python 3.7           |          Python 3.6           |          Python 3.5           |Python 3.4|          Python 2.7           |Relative Slowdown (versus ciso8601, Python 3.8)|
-            +===============+===============================+===============================+===============================+===============================+==========+===============================+===============================================+
-            |ciso8601       |207 nsec                       |219 nsec                       |282 nsec                       |262 nsec                       |264 nsec  |360 nsec                       |N/A                                            |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |pendulum       |249 nsec                       |225 nsec                       |209 nsec                       |212 nsec                       |209 nsec  |12.9 usec                      |1.2x                                           |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |udatetime      |806 nsec                       |866 nsec                       |817 nsec                       |827 nsec                       |792 nsec  |835 nsec                       |3.9x                                           |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |str2date       |7.57 usec                      |10.7 usec                      |7.98 usec                      |8.48 usec                      |9.06 usec |**Incorrect Result** (``None``)|36.7x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |isodate        |12 usec                        |13.5 usec                      |14.7 usec                      |15.4 usec                      |18.8 usec |47.6 usec                      |58.3x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601        |12.8 usec                      |14.6 usec                      |14.6 usec                      |15.2 usec                      |17.7 usec |30 usec                        |61.8x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |aniso8601      |19.4 usec                      |30.4 usec                      |22.1 usec                      |20.5 usec                      |21.9 usec |20.1 usec                      |94.0x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601utils   |22.5 usec                      |25.3 usec                      |26.4 usec                      |25.7 usec                      |27 usec   |26.9 usec                      |108.9x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |zulu           |25.6 usec                      |31.2 usec                      |30 usec                        |32.3 usec                      |30.7 usec |N/A                            |124.1x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |PySO8601       |25.9 usec                      |35.4 usec                      |25.6 usec                      |29.5 usec                      |27.7 usec |25.7 usec                      |125.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |maya           |48.5 usec                      |46.6 usec                      |51.3 usec                      |63.2 usec                      |68.1 usec |125 usec                       |234.9x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |python-dateutil|79.3 usec                      |88.5 usec                      |101 usec                       |89.8 usec                      |91.9 usec |160 usec                       |384.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |arrow          |86.2 usec                      |95.2 usec                      |95 usec                        |101 usec                       |N/A       |103 usec                       |417.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |moment         |**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|N/A       |**Incorrect Result** (``None``)|3442935.3x                                     |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-        
-        ciso8601 takes 207 nsec, which is **1.2x faster than pendulum**, the next fastest ISO 8601 parser in this comparison.
-        
-        .. </include:benchmark_with_time_zone.rst>
-        
-        .. <include:benchmark_module_versions.rst>
-        
-        Tested on Darwin 18.7.0 using the following modules:
-        
-        .. code:: python
-        
-          aniso8601==8.0.0
-          arrow==0.15.2
-          ciso8601==2.1.2
-          iso8601==0.1.12
-          iso8601utils==0.1.2
-          isodate==0.6.0
-          maya==0.6.1
-          moment==0.8.2
-          pendulum==2.0.5
-          PySO8601==0.2.0
-          python-dateutil==2.8.0
-          str2date==0.905
-          udatetime==0.0.16
-          zulu==1.1.1
-        
-        .. </include:benchmark_module_versions.rst>
-        
-        **Note:** ciso8601 doesn't support the entirety of the ISO 8601 spec, `only a popular subset`_.
-        
-        For full benchmarking details (or to run the benchmark yourself), see `benchmarking/README.rst`_
-        
-        .. _`benchmarking/README.rst`: https://github.com/closeio/ciso8601/blob/master/benchmarking/README.rst
-        
-        Dependency on pytz (Python 2)
-        -----------------------------
-        
-        In Python 2, ``ciso8601`` uses the `pytz`_ library while parsing timestamps with time zone information. This means that if you wish to parse such timestamps, you must first install ``pytz``:
-        
-        .. _pytz: http://pytz.sourceforge.net/
-        
-        .. code:: python
-          
-          pip install pytz
-        
-        Otherwise, ``ciso8601`` will raise an exception when you try to parse a timestamp with time zone information:
-        
-        .. code:: python
-          
-          In [2]: ciso8601.parse_datetime('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: ImportError: Cannot parse a timestamp with time zone information without the pytz dependency. Install it with `pip install pytz`.
-        
-        ``pytz`` is intentionally not an explicit dependency of ``ciso8601``. This is because many users use ``ciso8601`` to parse only naive timestamps, and therefore don't need this extra dependency.
-        In Python 3, ``ciso8601`` makes use of the built-in `datetime.timezone`_ class instead, so ``pytz`` is not necessary.
-        
-        .. _datetime.timezone: https://docs.python.org/3/library/datetime.html#timezone-objects
-        
-        Supported Subset of ISO 8601
-        ----------------------------
-        
-        ``ciso8601`` only supports the most common subset of ISO 8601.
-        
-        Date Formats
-        ^^^^^^^^^^^^
-        
-        The following date formats are supported:
-        
-        .. table::
-           :widths: auto
-        
-           ============================= ============== ==================
-           Format                        Example        Supported
-           ============================= ============== ==================
-           ``YYYY-MM-DD``                ``2018-04-29`` ✅
-           ``YYYY-MM``                   ``2018-04``    ✅
-           ``YYYYMMDD``                  ``2018-04``    ✅
-           ``--MM-DD`` (omitted year)    ``--04-29``    ❌              
-           ``--MMDD`` (omitted year)     ``--0429``     ❌
-           ``±YYYYY-MM`` (>4 digit year) ``+10000-04``  ❌   
-           ``+YYYY-MM`` (leading +)      ``+2018-04``   ❌   
-           ``-YYYY-MM`` (negative -)     ``-2018-04``   ❌   
-           ============================= ============== ==================
-        
-        Week dates or ordinal dates are not currently supported.
-        
-        .. table::
-           :widths: auto
-        
-           ============================= ============== ==================
-           Format                        Example        Supported
-           ============================= ============== ==================
-           ``YYYY-Www`` (week date)      ``2009-W01``   ❌
-           ``YYYYWww`` (week date)       ``2009W01``    ❌
-           ``YYYY-Www-D`` (week date)    ``2009-W01-1`` ❌
-           ``YYYYWwwD`` (week date)      ``2009-W01-1`` ❌
-           ``YYYY-DDD`` (ordinal date)   ``1981-095``   ❌
-           ``YYYYDDD`` (ordinal date)    ``1981095``    ❌ 
-           ============================= ============== ==================
-        
-        Time Formats
-        ^^^^^^^^^^^^
-        
-        Times are optional and are separated from the date by the letter ``T``.
-        
-        Consistent with `RFC 3339`__, ``ciso860`` also allows either a space character, or a lower-case ``t``, to be used instead of a ``T``.
-        
-        __ https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats
-        
-        The following time formats are supported:
-        
-        .. table::
-           :widths: auto
-        
-           =================================== =================== ==============  
-           Format                              Example             Supported          
-           =================================== =================== ============== 
-           ``hh``                              ``11``              ✅ 
-           ``hhmm``                            ``1130``            ✅ 
-           ``hh:mm``                           ``11:30``           ✅ 
-           ``hhmmss``                          ``113059``          ✅ 
-           ``hh:mm:ss``                        ``11:30:59``        ✅ 
-           ``hhmmss.ssssss``                   ``113059.123456``   ✅ 
-           ``hh:mm:ss.ssssss``                 ``11:30:59.123456`` ✅ 
-           ``hhmmss,ssssss``                   ``113059,123456``   ✅ 
-           ``hh:mm:ss,ssssss``                 ``11:30:59,123456`` ✅ 
-           Midnight (special case)             ``24:00:00``        ✅               
-           ``hh.hhh`` (fractional hours)       ``11.5``            ❌               
-           ``hh:mm.mmm`` (fractional minutes)  ``11:30.5``         ❌               
-           =================================== =================== ============== 
-        
-        **Note:** Python datetime objects only have microsecond precision (6 digits). Any additional precision will be truncated.
-        
-        Time Zone Information
-        ^^^^^^^^^^^^^^^^^^^^^
-        
-        Time zone information may be provided in one of the following formats:
-        
-        .. table::
-           :widths: auto
-        
-           ========== ========== =========== 
-           Format     Example    Supported          
-           ========== ========== =========== 
-           ``Z``      ``Z``      ✅
-           ``z``      ``z``      ✅
-           ``±hh``    ``+11``    ✅
-           ``±hhmm``  ``+1130``  ✅
-           ``±hh:mm`` ``+11:30`` ✅
-           ========== ========== ===========
-        
-        While the ISO 8601 specification allows the use of MINUS SIGN (U+2212) in the time zone separator, ``ciso8601`` only supports the use of the HYPHEN-MINUS (U+002D) character.
-        
-        Consistent with `RFC 3339`_, ``ciso860`` also allows a lower-case ``z`` to be used instead of a ``Z``.
-        
-        Strict RFC 3339 Parsing
-        -----------------------
-        
-        ``ciso8601`` parses ISO 8601 datetimes, which can be thought of as a superset of `RFC 3339`_ (`roughly`_). In cases where you might want strict RFC 3339 parsing, ``ciso8601`` offers a ``parse_rfc3339`` method, which behaves in a similar manner to ``parse_datetime``:
-        
-        .. _roughly: https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats
-        
-        ``parse_rfc3339(dt: String): datetime`` is a function that takes a string and either:
-        
-        * Returns a properly parsed Python datetime, **if and only if** the **entire** string conforms to RFC 3339.
-        * Raises a ``ValueError`` with a description of the reason why the string doesn't conform to RFC 3339.
-        
-        Ignoring Timezone Information While Parsing
-        -------------------------------------------
-        
-        It takes more time to parse timestamps with time zone information, especially if they're not in UTC. However, there are times when you don't care about time zone information, and wish to produce naive datetimes instead.
-        For example, if you are certain that your program will only parse timestamps from a single time zone, you might want to strip the time zone information and only output naive datetimes.
-        
-        In these limited cases, there is a second function provided.
-        ``parse_datetime_as_naive`` will ignore any time zone information it finds and, as a result, is faster for timestamps containing time zone information.
-        
-        .. code:: python
-        
-          In [1]: import ciso8601
-        
-          In [2]: ciso8601.parse_datetime_as_naive('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: datetime.datetime(2014, 12, 5, 12, 30, 45, 123456)
-        
-        NOTE: ``parse_datetime_as_naive`` is only useful in the case where your timestamps have time zone information, but you want to ignore it. This is somewhat unusual.
-        If your timestamps don't have time zone information (i.e. are naive), simply use ``parse_datetime``. It is just as fast.
-        
-Platform: UNKNOWN
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Topic :: Software Development :: Libraries :: Python Modules


=====================================
benchmarking/README.rst
=====================================
@@ -0,0 +1,88 @@
+=====================
+Benchmarking ciso8601
+=====================
+
+.. contents:: Contents
+
+Introduction
+------------
+
+``ciso8601``'s goal is to be the world's fastest ISO 8601 datetime parser for Python (**Note:** ciso8601 `only supports a subset of ISO 8601`_).
+
+.. _`only supports a subset of ISO 8601`: https://github.com/closeio/ciso8601#supported-subset-of-iso-8601
+
+In order to see how we compare, we run benchmarks against each other known ISO 8601 parser.
+
+**Note:** We only run benchmarks against open-source parsers that are published as part of Python modules on `PyPI`_.
+
+.. _`PyPI`: https://pypi.org/
+
+Quick start: Running the standard benchmarks
+--------------------------------------------
+
+If you just want to run the standard benchmarks we run for each release, there is a convenience script.
+
+.. code:: bash
+
+  % python -m venv env
+  % source env/bin/activate
+  % pip install -r requirements.txt
+  % ./run_benchmarks.sh
+
+This runs the benchmarks and generates reStructuredText files. The contents of these files are then automatically copy-pasted into ciso8601's `README.rst`_.
+
+.. _`README.rst`: https://github.com/closeio/ciso8601/blob/master/README.rst
+
+Running custom benchmarks
+-------------------------
+
+Running a custom benchmark is done by supplying `tox`_ with your custom timestamp: 
+
+.. code:: bash
+
+  % python -m venv env
+  % source env/bin/activate
+  % pip install -r requirements.txt
+  % tox '2014-01-09T21:48:00'
+
+It calls `perform_comparison.py`_ in each of the supported Python interpreters on your machine.
+This in turn calls `timeit`_ for each of the modules defined in ``ISO_8601_MODULES``. 
+
+.. _`tox`: https://tox.readthedocs.io/en/latest/index.html
+.. _`timeit`: https://docs.python.org/3/library/timeit.html
+
+Results are dumped into a collection of CSV files (in the ``benchmark_results`` directory by default).
+
+These CSV files can then formatted into reStructuredText tables by `format_results.py`_:
+
+.. _`perform_comparison.py`: https://github.com/closeio/ciso8601/blob/master/benchmarking/perform_comparison.py
+.. _`format_results.py`: https://github.com/closeio/ciso8601/blob/master/benchmarking/format_results.py
+
+.. code:: bash
+
+  % cd benchmarking
+  % python format_results.py benchmark_results/2014-01-09T214800 benchmark_results/benchmark_with_no_time_zone.rst
+  % python format_results.py benchmark_results/2014-01-09T214800-0530 benchmark_results/benchmark_with_time_zone.rst
+
+Disclaimer
+-----------
+
+Because of the way that ``tox`` works (and the way the benchmark is structured more generally), it doesn't make sense to compare the results for a given module across different Python versions.
+Comparisons between modules within the same Python version are still valid, and indeed, are the goal of the benchmarks.
+
+FAQs
+----
+
+"What about <missing module>?"
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We only run benchmarks against open-source parsers that are published as part of Python modules on PyPI.
+
+Do you know of a competing module missing from these benchmarks? We made it easy to add additional modules to our benchmarking:
+
+1. Add the dependency to ``tox.ini``
+1. Add the import statement and the parse statement for the module to ``ISO_8601_MODULES`` in `perform_comparison.py`_
+
+`Submit a pull request`_ and we'll probably add it to our official benchmarks.
+
+.. _`Submit a pull request`: https://github.com/closeio/ciso8601/blob/master/CONTRIBUTING.md


=====================================
benchmarking/format_results.py
=====================================
@@ -0,0 +1,167 @@
+import argparse
+import csv
+import os
+import platform
+import pytablewriter
+import re
+import sys
+
+from collections import defaultdict, namedtuple
+
+Result = namedtuple('Result', ['timing', 'parsed_value', 'exception', 'matched_expected'])
+
+FILENAME_REGEX_RAW = r"benchmark_timings_python(\d)(\d).csv"
+FILENAME_REGEX = re.compile(FILENAME_REGEX_RAW)
+
+MODULE_VERSION_FILENAME_REGEX_RAW = r"module_versions_python(\d)(\d).csv"
+MODULE_VERSION_FILENAME_REGEX = re.compile(MODULE_VERSION_FILENAME_REGEX_RAW)
+
+UNITS = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
+SCALES = sorted([(scale, unit) for unit, scale in UNITS.items()], reverse=True)
+
+NOT_APPLICABLE = 'N/A'
+
+def format_duration(duration):
+    # Based on cPython's `timeit` CLI formatting
+    scale, unit = next(((scale, unit) for scale, unit in SCALES if duration >= scale), SCALES[-1])
+    precision = 3
+    return "%.*g %s" % (precision, duration / scale, unit)
+
+
+def format_relative(d1, d2):
+    if d1 is None or d2 is None:
+        return NOT_APPLICABLE
+    precision = 1
+    return "%.*fx" % (precision, d1 / d2)
+
+
+def determine_used_module_versions(results_directory):
+    module_versions_used = defaultdict(dict)
+    for parent, _dirs, files in os.walk(results_directory):
+        files_to_process = [f for f in files if MODULE_VERSION_FILENAME_REGEX.match(f)]
+        for csv_file in files_to_process:
+            with open(os.path.join(parent, csv_file), 'r') as fin:
+                reader = csv.reader(fin, delimiter=",", quotechar='"')
+                major, minor = next(reader)
+                for module, version in reader:
+                    if version not in module_versions_used[module]:
+                        module_versions_used[module][version] = set()
+                    module_versions_used[module][version].add('.'.join((major, minor)))
+    return module_versions_used
+
+
+def format_used_module_versions(module_versions_used):
+    results = []
+    for module, versions in sorted(module_versions_used.items(), key=lambda x: x[0].lower()):
+        if len(versions) == 1:
+            results.append(f"{module}=={next(iter(versions.keys()))}")
+        else:
+            results.append(", ".join([f"{module}=={version} (on Python {', '.join(sorted(py_versions))})" for version, py_versions in versions.items()]))
+    return results
+
+
+def format_result(result):
+    if result == NOT_APPLICABLE:
+        return NOT_APPLICABLE
+    elif result.exception:
+        return f"Raised  ``{result.exception}`` Exception"
+    elif not result.matched_expected:
+        return f"**Incorrect Result** (``{result.parsed_value}``)"
+    else:
+        return format_duration(result.timing)
+
+
+def main(results_directory, output_file, compare_to, include_call, module_version_output):
+    calling_code = {}
+    timestamps = set()
+    all_results = defaultdict(dict)
+    timing_results = defaultdict(dict)
+
+    for parent, _dirs, files in os.walk(results_directory):
+        files_to_process = [f for f in files if FILENAME_REGEX.match(f)]
+        for csv_file in files_to_process:
+            try:
+                with open(os.path.join(parent, csv_file), 'r') as fin:
+                    reader = csv.reader(fin, delimiter=",", quotechar='"')
+                    major, minor, timestamp = next(reader)
+                    timestamps.add(timestamp)
+                    for module, _setup, stmt, parse_result, count, time_taken, matched, exception in reader:
+                        all_results[(major, minor)][module] = Result(float(time_taken) / int(count),
+                                                                    parse_result,
+                                                                    exception,
+                                                                    True if matched == "True" else False
+                                                                    )
+                        timing_results[(major, minor)][module] = all_results[(major, minor)][module].timing
+                        calling_code[module] = f"``{stmt.format(timestamp=timestamp)}``"
+            except:
+                print(f"Problem while parsing `{os.path.join(parent, csv_file)}`")
+                raise
+
+
+    if len(timestamps) > 1:
+        raise NotImplementedError(f"Found a mix of files in the results directory. Found files that represent the parsing of {timestamps}. Support for handling multiple timestamps is not implemented.")
+
+    all_modules = set([module for value in timing_results.values() for module in value.keys()])
+    python_versions_by_modernity = sorted(timing_results.keys(), reverse=True)
+    most_modern_python = python_versions_by_modernity[0]
+    modules_by_modern_speed = sorted(all_modules, key=lambda module: timing_results[most_modern_python][module])
+
+    writer = pytablewriter.RstGridTableWriter()
+    formatted_python_versions = ["Python {}".format(".".join(key)) for key in python_versions_by_modernity]
+    writer.header_list = ["Module"] + (["Call"] if include_call else []) + formatted_python_versions + [f"Relative Slowdown (versus {compare_to}, {formatted_python_versions[0]})"]
+    writer.type_hint_list = [pytablewriter.String] * len(writer.header_list)
+
+
+    calling_codes = [calling_code[module] for module in modules_by_modern_speed]
+    performance_results = [[format_result(all_results[python_version].get(module, NOT_APPLICABLE)) for python_version in python_versions_by_modernity] for module in modules_by_modern_speed]
+    relative_slowdowns = [format_relative(timing_results[most_modern_python].get(module), timing_results[most_modern_python].get(compare_to)) if module != compare_to else NOT_APPLICABLE for module in modules_by_modern_speed]
+    
+    writer.value_matrix = [
+        [module] + ([calling_code[module]] if include_call else []) + performance_by_version + [relative_slowdown] for module, calling_code, performance_by_version, relative_slowdown in zip(modules_by_modern_speed, calling_codes, performance_results, relative_slowdowns)
+    ]
+
+    with open(output_file, 'w') as fout:
+        writer.stream = fout
+        writer.write_table()
+        fout.write('\n')
+
+        if modules_by_modern_speed[0] == compare_to:
+            fout.write(f"{compare_to} takes {format_duration(timing_results[most_modern_python][compare_to])}, which is **{format_relative(timing_results[most_modern_python][modules_by_modern_speed[1]], timing_results[most_modern_python][compare_to])} faster than {modules_by_modern_speed[1]}**, the next fastest ISO 8601 parser in this comparison.\n")
+        else:
+            fout.write(f"{compare_to} takes {format_duration(timing_results[most_modern_python][compare_to])}, which is **{format_relative(timing_results[most_modern_python][compare_to], timing_results[most_modern_python][modules_by_modern_speed[0]])} slower than {modules_by_modern_speed[0]}**, the fastest ISO 8601 parser in this comparison.\n")
+
+    with open(os.path.join(os.path.dirname(output_file), module_version_output), 'w') as fout:
+        fout.write(f"Tested on {platform.system()} {platform.release()} using the following modules:\n")
+        fout.write('\n')
+        fout.write(".. code:: python\n")
+        fout.write('\n')
+        for module_version_line in format_used_module_versions(determine_used_module_versions(results_directory)):
+            fout.write(f"  {module_version_line}\n")
+
+
+if __name__ == '__main__':
+    OUTPUT_FILE_HELP = "The filepath to use when outputting the reStructuredText results."
+    RESULTS_DIR_HELP = f"Which directory the script should look in to find benchmarking results. Will process any file that match the regexes '{FILENAME_REGEX_RAW}' and '{MODULE_VERSION_FILENAME_REGEX_RAW}'."
+
+    BASE_LIBRARY_DEFAULT = "ciso8601"
+    BASE_LIBRARY_HELP = f"The module to make all relative calculations relative to (default: \"{BASE_LIBRARY_DEFAULT}\")."
+
+    INCLUDE_CALL_DEFAULT = False
+    INCLUDE_CALL_HELP = f"Whether or not to include a column showing the actual code call (default: {INCLUDE_CALL_DEFAULT})."
+
+    MODULE_VERSION_OUTPUT_FILE_DEFAULT = "benchmark_module_versions.rst"
+    MODULE_VERSION_OUTPUT_FILE_HELP = "The filename to use when outputting the reStructuredText list of module versions. Written to the same directory as `OUTPUT`"
+
+    parser = argparse.ArgumentParser("Formats the benchmarking results into a nicely formatted block of reStructuredText for use in the README.")
+    parser.add_argument("RESULTS", help=RESULTS_DIR_HELP)
+    parser.add_argument("OUTPUT", help=OUTPUT_FILE_HELP)
+    parser.add_argument("--base-module", required=False, default=BASE_LIBRARY_DEFAULT, help=BASE_LIBRARY_HELP)
+    parser.add_argument("--include-call", required=False, type=bool, default=INCLUDE_CALL_DEFAULT, help=INCLUDE_CALL_HELP)
+    parser.add_argument("--module-version-output", required=False, default=MODULE_VERSION_OUTPUT_FILE_DEFAULT, help=MODULE_VERSION_OUTPUT_FILE_HELP)
+
+    args = parser.parse_args()
+
+    if not os.path.exists(args.RESULTS):
+        raise ValueError(f'Results directory "{args.RESULTS}" does not exist.')
+
+    main(args.RESULTS, args.OUTPUT, args.base_module, args.include_call, args.module_version_output)


=====================================
benchmarking/perform_comparison.py
=====================================
@@ -0,0 +1,126 @@
+import argparse
+import csv
+import os
+import pytz
+import sys
+import timeit
+
+from datetime import datetime
+
+try:
+    from importlib.metadata import version as get_module_version
+except ImportError:
+    from importlib_metadata import version as get_module_version
+
+ISO_8601_MODULES = {
+    "aniso8601": ('import aniso8601', "aniso8601.parse_datetime('{timestamp}')"),
+    "ciso8601": ('import ciso8601', "ciso8601.parse_datetime('{timestamp}')"),
+    "python-dateutil": ('import dateutil.parser', "dateutil.parser.parse('{timestamp}')"),
+    "iso8601": ('import iso8601', "iso8601.parse_date('{timestamp}')"),
+    "iso8601utils": ('from iso8601utils import parsers', "parsers.datetime('{timestamp}')"),
+    "isodate": ('import isodate', "isodate.parse_datetime('{timestamp}')"),
+    "maya": ('import maya', "maya.parse('{timestamp}').datetime()"),
+    "pendulum": ('from pendulum.parsing import parse_iso8601', "parse_iso8601('{timestamp}')"),
+    "PySO8601": ('import PySO8601', "PySO8601.parse('{timestamp}')"),
+    "str2date": ('from str2date import str2date', "str2date('{timestamp}')"),
+}
+
+if os.name != 'nt':
+    # udatetime doesn't support Windows.
+    ISO_8601_MODULES["udatetime"] = ('import udatetime', "udatetime.from_string('{timestamp}')")
+
+if sys.version_info.major > 2:
+    # zulu no longer supports Python 2.7
+    ISO_8601_MODULES["zulu"] = ('import zulu', "zulu.parse('{timestamp}')")
+
+if (sys.version_info.major, sys.version_info.minor) != (3, 4):
+    # arrow no longer supports Python 3.4
+    ISO_8601_MODULES["arrow"] = ('import arrow', "arrow.get('{timestamp}').datetime")
+    # moment is built on `times`, which is built on `arrow`, which no longer supports Python 3.4
+    ISO_8601_MODULES["moment"] = ('import moment', "moment.date('{timestamp}').date")
+
+def check_roughly_equivalent(dt1, dt2):
+    # For the purposes of our benchmarking, we don't care if the datetime
+    # has tzinfo=UTC or is naive.
+    dt1 = dt1.replace(tzinfo=pytz.UTC) if isinstance(dt1, datetime) and dt1.tzinfo is None else dt1
+    dt2 = dt2.replace(tzinfo=pytz.UTC) if isinstance(dt2, datetime) and dt2.tzinfo is None else dt2
+    return dt1 == dt2
+
+
+def run_tests(timestamp, results_directory, compare_to):
+    # `Timer.autorange` only exists in Python 3.6+. We want the tests to run in a reasonable amount of time,
+    # but we don't want to have to hard-code how many times to run each test.
+    # So we make sure to call Python 3.6+ versions first. They output a file that the others use to know how many iterations to run.
+    test_interation_counts = {}
+    auto_range_file_obj = None
+    auto_range_file_writer = None
+    try:
+        if (sys.version_info.major == 3 and sys.version_info.minor >= 6) or sys.version_info.major > 3:
+            auto_range_file_obj = open(os.path.join(results_directory, "auto_range_counts.csv"), 'w')
+            auto_range_file_writer = csv.writer(auto_range_file_obj, delimiter=',', quotechar='"', lineterminator='\n')
+        else:
+            with open(os.path.join(results_directory, "auto_range_counts.csv"), "r") as fin:
+                reader = csv.reader(fin, delimiter=',', quotechar='"')
+                for module, count in reader:
+                    test_interation_counts[module] = int(count)
+
+        exec(ISO_8601_MODULES[compare_to][0])
+        expected_parse_result = eval(ISO_8601_MODULES[compare_to][1].format(timestamp=timestamp))
+
+        with open(os.path.join(results_directory, "benchmark_timings_python{major}{minor}.csv".format(major=sys.version_info.major, minor=sys.version_info.minor)), 'w') as fout:
+            writer = csv.writer(fout, delimiter=',', quotechar='"', lineterminator='\n')
+            writer.writerow([sys.version_info.major, sys.version_info.minor, timestamp])
+            for module, (setup, stmt) in ISO_8601_MODULES.items():
+                count = None
+                time_taken = None
+                exception = None
+                try:
+                    exec(setup)
+                    parse_result = eval(stmt.format(timestamp=timestamp))
+
+                    if module in test_interation_counts:
+                        count = test_interation_counts[module]
+                        timer = timeit.Timer(stmt=stmt.format(timestamp=timestamp), setup=setup)
+                        time_taken = timer.timeit(number=count)
+                    else:
+                        timer = timeit.Timer(stmt=stmt.format(timestamp=timestamp), setup=setup)
+                        count, time_taken = timer.autorange()
+                except Exception as exc:
+                    parse_result = None
+                    exception = type(exc)
+
+                writer.writerow([module, setup, stmt.format(timestamp=timestamp), parse_result if parse_result is not None else "None", count, time_taken, check_roughly_equivalent(parse_result, expected_parse_result), exception])
+
+                if auto_range_file_writer is not None:
+                    auto_range_file_writer.writerow([module, count])
+    finally:
+        if auto_range_file_obj is not None:
+            auto_range_file_obj.close()
+
+    with open(os.path.join(results_directory, "module_versions_python{major}{minor}.csv".format(major=sys.version_info.major, minor=sys.version_info.minor)), 'w') as fout:
+        module_version_writer = csv.writer(fout, delimiter=',', quotechar='"', lineterminator='\n')
+        module_version_writer.writerow([sys.version_info.major, sys.version_info.minor])
+        for module, (setup, stmt) in sorted(ISO_8601_MODULES.items(), key=lambda x: x[0].lower()):
+            module_version_writer.writerow([module, get_module_version(module)])
+
+
+if __name__ == '__main__':
+    TIMESTAMP_HELP = "Which ISO 8601 timestamp to parse"
+
+    BASE_LIBRARY_DEFAULT = "ciso8601"
+    BASE_LIBRARY_HELP = "The module to make correctness decisions relative to (default: \"{default}\").".format(default=BASE_LIBRARY_DEFAULT)
+
+    RESULTS_DIR_DEFAULT = "benchmark_results"
+    RESULTS_DIR_HELP = "Which directory the script should output benchmarking results. (default: \"{0}\")".format(RESULTS_DIR_DEFAULT)
+
+    parser = argparse.ArgumentParser("Runs `timeit` to benchmark a variety of ISO 8601 parsers.")
+    parser.add_argument("TIMESTAMP", help=TIMESTAMP_HELP)
+    parser.add_argument("--base-module", required=False, default=BASE_LIBRARY_DEFAULT, help=BASE_LIBRARY_HELP)
+    parser.add_argument("--results", required=False, default=RESULTS_DIR_DEFAULT, help=RESULTS_DIR_HELP)
+    args = parser.parse_args()
+
+    output_dir = os.path.join(args.results, args.TIMESTAMP.replace(":", ""))
+    if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+
+    run_tests(args.TIMESTAMP, output_dir, args.base_module)


=====================================
benchmarking/requirements.txt
=====================================
@@ -0,0 +1,3 @@
+importlib_metadata; python_version < '3.8'
+pytablewriter
+tox
\ No newline at end of file


=====================================
benchmarking/rst_include_replace.py
=====================================
@@ -0,0 +1,65 @@
+import argparse
+import os
+import re
+
+# Since GitHub doesn't support the use of the reStructuredText `include` directive,
+# we must copy-paste the results into README.rst. To do this automatically, we came
+# up with a special comment syntax. This script will replace everything between the
+# two special comments with the content requested.
+# For example:
+#
+# .. <include:benchmark_module_versions.rst>
+# This content will be replaced by the content of "benchmark_module_versions.rst"
+# .. </include:benchmark_module_versions.rst>
+#
+INCLUDE_BLOCK_START = ".. <include:{filename}>"
+INCLUDE_BLOCK_END = ".. </include:{filename}>"
+
+
+def replace_include(target_filepath, include_file, source_filepath):
+    start_block_regex = re.compile(INCLUDE_BLOCK_START.format(filename=include_file))
+    end_block_regex = re.compile(INCLUDE_BLOCK_END.format(filename=include_file))
+
+    with open(source_filepath, 'r') as fin:
+        replacement_lines = iter(fin.readlines())
+
+    with open(target_filepath, 'r') as fin:
+        target_lines = iter(fin.readlines())
+    with open(target_filepath, 'w') as fout:
+        for line in target_lines:
+            if start_block_regex.match(line):
+                fout.write(line)
+                fout.write("\n")  # rST requires a blank line after comment lines
+                for replacement_line in replacement_lines:
+                    fout.write(replacement_line)
+                next_line = next(target_lines)
+                while not end_block_regex.match(next_line):
+                    try:
+                        next_line = next(target_lines)
+                    except StopIteration:
+                        break
+                fout.write("\n")  # rST requires a blank line before comment lines
+                fout.write(next_line)
+            else:
+                fout.write(line)
+
+
+if __name__ == '__main__':
+    TARGET_HELP = "The filepath you wish to replace tags within."
+    INCLUDE_TAG_HELP = "The filename within the tag you are hoping to replace. (ex. 'benchmark_with_time_zone.rst')"
+    SOURCE_HELP = "The filepath whose contents should be included into the TARGET file."
+
+    parser = argparse.ArgumentParser("Formats the benchmarking results into a nicely formatted block of reStructuredText for use in the README.")
+    parser.add_argument("TARGET", help=TARGET_HELP)
+    parser.add_argument("INCLUDE_TAG", help=INCLUDE_TAG_HELP)
+    parser.add_argument("SOURCE", help=SOURCE_HELP)
+
+    args = parser.parse_args()
+
+    if not os.path.exists(args.TARGET):
+        raise ValueError(f'TARGET path {args.TARGET} does not exist')
+
+    if not os.path.exists(args.SOURCE):
+        raise ValueError(f'SOURCE path {args.SOURCE} does not exist')
+
+    replace_include(args.TARGET, args.INCLUDE_TAG, args.SOURCE)


=====================================
benchmarking/run_benchmarks.sh
=====================================
@@ -0,0 +1,7 @@
+tox '2014-01-09T21:48:00'
+tox '2014-01-09T21:48:00-05:30'
+python format_results.py benchmark_results/2014-01-09T214800 benchmark_results/benchmark_with_no_time_zone.rst
+python format_results.py benchmark_results/2014-01-09T214800-0530 benchmark_results/benchmark_with_time_zone.rst
+python rst_include_replace.py ../README.rst 'benchmark_with_no_time_zone.rst' benchmark_results/benchmark_with_no_time_zone.rst
+python rst_include_replace.py ../README.rst 'benchmark_with_time_zone.rst' benchmark_results/benchmark_with_time_zone.rst
+python rst_include_replace.py ../README.rst 'benchmark_module_versions.rst' benchmark_results/benchmark_module_versions.rst
\ No newline at end of file


=====================================
benchmarking/tox.ini
=====================================
@@ -0,0 +1,30 @@
+[tox]
+envlist = py38,py37,py36,py35,py34,py27
+setupdir=..
+
+[testenv]
+deps=
+    ; The libraries needed to run the benchmarking itself
+    -rrequirements.txt
+
+    ; The actual ISO 8601 parsing libraries
+    aniso8601
+    ; arrow no longer supports Python 3.4
+    arrow; python_version != '3.4'
+    iso8601
+    iso8601utils
+    isodate
+    maya
+    ; moment is built on `times`, which is built on `arrow`, which no longer supports Python 3.4 
+    moment; python_version != '3.4'
+    pendulum
+    pyso8601
+    python-dateutil
+    str2date
+    ; udatetime doesn't support Windows
+    udatetime; os_name != 'nt'
+    ; zulu no longer supports Python 2.7
+    zulu; python_version > '2.7'
+    pytz
+commands=
+    python -W ignore perform_comparison.py {posargs:DEFAULTS}
\ No newline at end of file


=====================================
ciso8601.egg-info/PKG-INFO deleted
=====================================
@@ -1,360 +0,0 @@
-Metadata-Version: 1.1
-Name: ciso8601
-Version: 2.1.3
-Summary: Fast ISO8601 date time parser for Python written in C
-Home-page: https://github.com/closeio/ciso8601
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: MIT
-Description: ========
-        ciso8601
-        ========
-        
-        .. image:: https://img.shields.io/circleci/project/github/closeio/ciso8601.svg
-            :target: https://circleci.com/gh/closeio/ciso8601/tree/master
-        
-        .. image:: https://img.shields.io/pypi/v/ciso8601.svg
-            :target: https://pypi.org/project/ciso8601/
-        
-        .. image:: https://img.shields.io/pypi/pyversions/ciso8601.svg
-            :target: https://pypi.org/project/ciso8601/
-        
-        ``ciso8601`` converts `ISO 8601`_ or `RFC 3339`_ date time strings into Python datetime objects.
-        
-        Since it's written as a C module, it is much faster than other Python libraries.
-        Tested with Python 2.7, 3.4, 3.5, 3.6, 3.7, 3.8.
-        
-        **Note:** ciso8601 doesn't support the entirety of the ISO 8601 spec, `only a popular subset`_.
-        
-        .. _ISO 8601: https://en.wikipedia.org/wiki/ISO_8601
-        .. _RFC 3339: https://tools.ietf.org/html/rfc3339
-        
-        .. _`only a popular subset`: https://github.com/closeio/ciso8601#supported-subset-of-iso-8601
-        
-        (Interested in working on projects like this? `Close`_ is looking for `great engineers`_ to join our team)
-        
-        .. _Close: https://close.com
-        .. _great engineers: https://jobs.close.com
-        
-        
-        .. contents:: Contents
-        
-        
-        Quick Start
-        -----------
-        
-        .. code:: bash
-        
-          % pip install ciso8601
-        
-        .. code:: python
-        
-          In [1]: import ciso8601
-        
-          In [2]: ciso8601.parse_datetime('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: datetime.datetime(2014, 12, 5, 12, 30, 45, 123456, tzinfo=pytz.FixedOffset(330))
-        
-          In [3]: ciso8601.parse_datetime('20141205T123045')
-          Out[3]: datetime.datetime(2014, 12, 5, 12, 30, 45)
-        
-        Migration to v2
-        ---------------
-        
-        Version 2.0.0 of ``ciso8601`` changed the core implementation. This was not entirely backwards compatible, and care should be taken when migrating
-        See `CHANGELOG`_ for the Migration Guide.
-        
-        .. _CHANGELOG: https://github.com/closeio/ciso8601/blob/master/CHANGELOG.md
-        
-        Error Handling
-        --------------
-        
-        Starting in v2.0.0, ``ciso8601`` offers strong guarantees when it comes to parsing strings.
-        
-        ``parse_datetime(dt: String): datetime`` is a function that takes a string and either:
-        
-        * Returns a properly parsed Python datetime, **if and only if** the **entire** string conforms to the supported subset of ISO 8601
-        * Raises a ``ValueError`` with a description of the reason why the string doesn't conform to the supported subset of ISO 8601
-        
-        If time zone information is provided, an aware datetime object will be returned. Otherwise, a naive datetime is returned.
-        
-        Benchmark
-        ---------
-        
-        Parsing a timestamp with no time zone information (ex. ``2014-01-09T21:48:00``):
-        
-        .. <include:benchmark_with_no_time_zone.rst>
-        
-        .. table:: 
-        
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |    Module     |Python 3.8|Python 3.7|Python 3.6|Python 3.5|Python 3.4|          Python 2.7           |Relative Slowdown (versus ciso8601, Python 3.8)|
-            +===============+==========+==========+==========+==========+==========+===============================+===============================================+
-            |ciso8601       |201 nsec  |157 nsec  |160 nsec  |139 nsec  |148 nsec  |147 nsec                       |N/A                                            |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |pendulum       |215 nsec  |232 nsec  |234 nsec  |205 nsec  |192 nsec  |9.44 usec                      |1.1x                                           |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |udatetime      |906 nsec  |1.06 usec |767 nsec  |702 nsec  |819 nsec  |923 nsec                       |4.5x                                           |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |str2date       |5.96 usec |7.75 usec |7.27 usec |6.84 usec |7.6 usec  |**Incorrect Result** (``None``)|29.7x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |isodate        |10.3 usec |10 usec   |11.1 usec |11.9 usec |12.3 usec |43.6 usec                      |51.3x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601utils   |10.3 usec |8.63 usec |9.16 usec |10.3 usec |9.58 usec |11.1 usec                      |51.5x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601        |10.9 usec |11.1 usec |10.5 usec |11.2 usec |11.5 usec |25.6 usec                      |54.2x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |PySO8601       |13.9 usec |21.9 usec |20.2 usec |15.9 usec |23.7 usec |16.4 usec                      |69.4x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |aniso8601      |14.5 usec |15 usec   |15.8 usec |15.9 usec |16.1 usec |17.2 usec                      |72.5x                                          |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |zulu           |25.3 usec |29.9 usec |28.2 usec |27.4 usec |33 usec   |N/A                            |126.3x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |maya           |42.9 usec |57.4 usec |58.2 usec |67.5 usec |87.6 usec |100 usec                       |213.7x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |arrow          |85.7 usec |81.8 usec |75.7 usec |78.7 usec |N/A       |93.9 usec                      |427.1x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |python-dateutil|122 usec  |82.7 usec |72.2 usec |77.1 usec |74.4 usec |131 usec                       |609.5x                                         |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-            |moment         |3.81 msec |4.46 msec |3.12 msec |3.66 msec |N/A       |3.59 msec                      |19011.9x                                       |
-            +---------------+----------+----------+----------+----------+----------+-------------------------------+-----------------------------------------------+
-        
-        ciso8601 takes 201 nsec, which is **1.1x faster than pendulum**, the next fastest ISO 8601 parser in this comparison.
-        
-        .. </include:benchmark_with_no_time_zone.rst>
-        
-        Parsing a timestamp with time zone information (ex. ``2014-01-09T21:48:00-05:30``):
-        
-        .. <include:benchmark_with_time_zone.rst>
-        
-        .. table:: 
-        
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |    Module     |          Python 3.8           |          Python 3.7           |          Python 3.6           |          Python 3.5           |Python 3.4|          Python 2.7           |Relative Slowdown (versus ciso8601, Python 3.8)|
-            +===============+===============================+===============================+===============================+===============================+==========+===============================+===============================================+
-            |ciso8601       |207 nsec                       |219 nsec                       |282 nsec                       |262 nsec                       |264 nsec  |360 nsec                       |N/A                                            |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |pendulum       |249 nsec                       |225 nsec                       |209 nsec                       |212 nsec                       |209 nsec  |12.9 usec                      |1.2x                                           |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |udatetime      |806 nsec                       |866 nsec                       |817 nsec                       |827 nsec                       |792 nsec  |835 nsec                       |3.9x                                           |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |str2date       |7.57 usec                      |10.7 usec                      |7.98 usec                      |8.48 usec                      |9.06 usec |**Incorrect Result** (``None``)|36.7x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |isodate        |12 usec                        |13.5 usec                      |14.7 usec                      |15.4 usec                      |18.8 usec |47.6 usec                      |58.3x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601        |12.8 usec                      |14.6 usec                      |14.6 usec                      |15.2 usec                      |17.7 usec |30 usec                        |61.8x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |aniso8601      |19.4 usec                      |30.4 usec                      |22.1 usec                      |20.5 usec                      |21.9 usec |20.1 usec                      |94.0x                                          |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |iso8601utils   |22.5 usec                      |25.3 usec                      |26.4 usec                      |25.7 usec                      |27 usec   |26.9 usec                      |108.9x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |zulu           |25.6 usec                      |31.2 usec                      |30 usec                        |32.3 usec                      |30.7 usec |N/A                            |124.1x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |PySO8601       |25.9 usec                      |35.4 usec                      |25.6 usec                      |29.5 usec                      |27.7 usec |25.7 usec                      |125.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |maya           |48.5 usec                      |46.6 usec                      |51.3 usec                      |63.2 usec                      |68.1 usec |125 usec                       |234.9x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |python-dateutil|79.3 usec                      |88.5 usec                      |101 usec                       |89.8 usec                      |91.9 usec |160 usec                       |384.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |arrow          |86.2 usec                      |95.2 usec                      |95 usec                        |101 usec                       |N/A       |103 usec                       |417.2x                                         |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-            |moment         |**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|**Incorrect Result** (``None``)|N/A       |**Incorrect Result** (``None``)|3442935.3x                                     |
-            +---------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+----------+-------------------------------+-----------------------------------------------+
-        
-        ciso8601 takes 207 nsec, which is **1.2x faster than pendulum**, the next fastest ISO 8601 parser in this comparison.
-        
-        .. </include:benchmark_with_time_zone.rst>
-        
-        .. <include:benchmark_module_versions.rst>
-        
-        Tested on Darwin 18.7.0 using the following modules:
-        
-        .. code:: python
-        
-          aniso8601==8.0.0
-          arrow==0.15.2
-          ciso8601==2.1.2
-          iso8601==0.1.12
-          iso8601utils==0.1.2
-          isodate==0.6.0
-          maya==0.6.1
-          moment==0.8.2
-          pendulum==2.0.5
-          PySO8601==0.2.0
-          python-dateutil==2.8.0
-          str2date==0.905
-          udatetime==0.0.16
-          zulu==1.1.1
-        
-        .. </include:benchmark_module_versions.rst>
-        
-        **Note:** ciso8601 doesn't support the entirety of the ISO 8601 spec, `only a popular subset`_.
-        
-        For full benchmarking details (or to run the benchmark yourself), see `benchmarking/README.rst`_
-        
-        .. _`benchmarking/README.rst`: https://github.com/closeio/ciso8601/blob/master/benchmarking/README.rst
-        
-        Dependency on pytz (Python 2)
-        -----------------------------
-        
-        In Python 2, ``ciso8601`` uses the `pytz`_ library while parsing timestamps with time zone information. This means that if you wish to parse such timestamps, you must first install ``pytz``:
-        
-        .. _pytz: http://pytz.sourceforge.net/
-        
-        .. code:: python
-          
-          pip install pytz
-        
-        Otherwise, ``ciso8601`` will raise an exception when you try to parse a timestamp with time zone information:
-        
-        .. code:: python
-          
-          In [2]: ciso8601.parse_datetime('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: ImportError: Cannot parse a timestamp with time zone information without the pytz dependency. Install it with `pip install pytz`.
-        
-        ``pytz`` is intentionally not an explicit dependency of ``ciso8601``. This is because many users use ``ciso8601`` to parse only naive timestamps, and therefore don't need this extra dependency.
-        In Python 3, ``ciso8601`` makes use of the built-in `datetime.timezone`_ class instead, so ``pytz`` is not necessary.
-        
-        .. _datetime.timezone: https://docs.python.org/3/library/datetime.html#timezone-objects
-        
-        Supported Subset of ISO 8601
-        ----------------------------
-        
-        ``ciso8601`` only supports the most common subset of ISO 8601.
-        
-        Date Formats
-        ^^^^^^^^^^^^
-        
-        The following date formats are supported:
-        
-        .. table::
-           :widths: auto
-        
-           ============================= ============== ==================
-           Format                        Example        Supported
-           ============================= ============== ==================
-           ``YYYY-MM-DD``                ``2018-04-29`` ✅
-           ``YYYY-MM``                   ``2018-04``    ✅
-           ``YYYYMMDD``                  ``2018-04``    ✅
-           ``--MM-DD`` (omitted year)    ``--04-29``    ❌              
-           ``--MMDD`` (omitted year)     ``--0429``     ❌
-           ``±YYYYY-MM`` (>4 digit year) ``+10000-04``  ❌   
-           ``+YYYY-MM`` (leading +)      ``+2018-04``   ❌   
-           ``-YYYY-MM`` (negative -)     ``-2018-04``   ❌   
-           ============================= ============== ==================
-        
-        Week dates or ordinal dates are not currently supported.
-        
-        .. table::
-           :widths: auto
-        
-           ============================= ============== ==================
-           Format                        Example        Supported
-           ============================= ============== ==================
-           ``YYYY-Www`` (week date)      ``2009-W01``   ❌
-           ``YYYYWww`` (week date)       ``2009W01``    ❌
-           ``YYYY-Www-D`` (week date)    ``2009-W01-1`` ❌
-           ``YYYYWwwD`` (week date)      ``2009-W01-1`` ❌
-           ``YYYY-DDD`` (ordinal date)   ``1981-095``   ❌
-           ``YYYYDDD`` (ordinal date)    ``1981095``    ❌ 
-           ============================= ============== ==================
-        
-        Time Formats
-        ^^^^^^^^^^^^
-        
-        Times are optional and are separated from the date by the letter ``T``.
-        
-        Consistent with `RFC 3339`__, ``ciso860`` also allows either a space character, or a lower-case ``t``, to be used instead of a ``T``.
-        
-        __ https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats
-        
-        The following time formats are supported:
-        
-        .. table::
-           :widths: auto
-        
-           =================================== =================== ==============  
-           Format                              Example             Supported          
-           =================================== =================== ============== 
-           ``hh``                              ``11``              ✅ 
-           ``hhmm``                            ``1130``            ✅ 
-           ``hh:mm``                           ``11:30``           ✅ 
-           ``hhmmss``                          ``113059``          ✅ 
-           ``hh:mm:ss``                        ``11:30:59``        ✅ 
-           ``hhmmss.ssssss``                   ``113059.123456``   ✅ 
-           ``hh:mm:ss.ssssss``                 ``11:30:59.123456`` ✅ 
-           ``hhmmss,ssssss``                   ``113059,123456``   ✅ 
-           ``hh:mm:ss,ssssss``                 ``11:30:59,123456`` ✅ 
-           Midnight (special case)             ``24:00:00``        ✅               
-           ``hh.hhh`` (fractional hours)       ``11.5``            ❌               
-           ``hh:mm.mmm`` (fractional minutes)  ``11:30.5``         ❌               
-           =================================== =================== ============== 
-        
-        **Note:** Python datetime objects only have microsecond precision (6 digits). Any additional precision will be truncated.
-        
-        Time Zone Information
-        ^^^^^^^^^^^^^^^^^^^^^
-        
-        Time zone information may be provided in one of the following formats:
-        
-        .. table::
-           :widths: auto
-        
-           ========== ========== =========== 
-           Format     Example    Supported          
-           ========== ========== =========== 
-           ``Z``      ``Z``      ✅
-           ``z``      ``z``      ✅
-           ``±hh``    ``+11``    ✅
-           ``±hhmm``  ``+1130``  ✅
-           ``±hh:mm`` ``+11:30`` ✅
-           ========== ========== ===========
-        
-        While the ISO 8601 specification allows the use of MINUS SIGN (U+2212) in the time zone separator, ``ciso8601`` only supports the use of the HYPHEN-MINUS (U+002D) character.
-        
-        Consistent with `RFC 3339`_, ``ciso860`` also allows a lower-case ``z`` to be used instead of a ``Z``.
-        
-        Strict RFC 3339 Parsing
-        -----------------------
-        
-        ``ciso8601`` parses ISO 8601 datetimes, which can be thought of as a superset of `RFC 3339`_ (`roughly`_). In cases where you might want strict RFC 3339 parsing, ``ciso8601`` offers a ``parse_rfc3339`` method, which behaves in a similar manner to ``parse_datetime``:
-        
-        .. _roughly: https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats
-        
-        ``parse_rfc3339(dt: String): datetime`` is a function that takes a string and either:
-        
-        * Returns a properly parsed Python datetime, **if and only if** the **entire** string conforms to RFC 3339.
-        * Raises a ``ValueError`` with a description of the reason why the string doesn't conform to RFC 3339.
-        
-        Ignoring Timezone Information While Parsing
-        -------------------------------------------
-        
-        It takes more time to parse timestamps with time zone information, especially if they're not in UTC. However, there are times when you don't care about time zone information, and wish to produce naive datetimes instead.
-        For example, if you are certain that your program will only parse timestamps from a single time zone, you might want to strip the time zone information and only output naive datetimes.
-        
-        In these limited cases, there is a second function provided.
-        ``parse_datetime_as_naive`` will ignore any time zone information it finds and, as a result, is faster for timestamps containing time zone information.
-        
-        .. code:: python
-        
-          In [1]: import ciso8601
-        
-          In [2]: ciso8601.parse_datetime_as_naive('2014-12-05T12:30:45.123456-05:30')
-          Out[2]: datetime.datetime(2014, 12, 5, 12, 30, 45, 123456)
-        
-        NOTE: ``parse_datetime_as_naive`` is only useful in the case where your timestamps have time zone information, but you want to ignore it. This is somewhat unusual.
-        If your timestamps don't have time zone information (i.e. are naive), simply use ``parse_datetime``. It is just as fast.
-        
-Platform: UNKNOWN
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Topic :: Software Development :: Libraries :: Python Modules


=====================================
ciso8601.egg-info/SOURCES.txt deleted
=====================================
@@ -1,12 +0,0 @@
-CHANGELOG.md
-LICENSE
-MANIFEST.in
-README.rst
-module.c
-setup.py
-ciso8601/__init__.pyi
-ciso8601/py.typed
-ciso8601.egg-info/PKG-INFO
-ciso8601.egg-info/SOURCES.txt
-ciso8601.egg-info/dependency_links.txt
-ciso8601.egg-info/top_level.txt
\ No newline at end of file


=====================================
ciso8601.egg-info/dependency_links.txt deleted
=====================================
@@ -1 +0,0 @@
-


=====================================
ciso8601.egg-info/top_level.txt deleted
=====================================
@@ -1 +0,0 @@
-ciso8601


=====================================
debian/watch
=====================================
@@ -1,3 +1,3 @@
 version=4
-opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
-https://pypi.debian.net/ciso8601/ciso8601-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+
+https://github.com/closeio/ciso8601/releases .*/archive/v?@ANY_VERSION@@ARCHIVE_EXT@


=====================================
generate_test_timestamps.py
=====================================
@@ -0,0 +1,228 @@
+import datetime
+import pytz
+
+from collections import namedtuple
+
+
+def __merge_dicts(*dict_args):
+    # Only needed for Python <3.5 support. In Python 3.5+, you can use the {**a, **b} syntax.
+    """
+    From: https://stackoverflow.com/a/26853961
+    Given any number of dicts, shallow copy and merge into a new dict,
+    precedence goes to key value pairs in latter dicts.
+    """
+    result = {}
+    for dictionary in dict_args:
+        result.update(dictionary)
+    return result
+
+
+NumberField = namedtuple('NumberField', ['min_width', 'max_width', 'min_value', 'max_value'])
+NUMBER_FIELDS = {
+    "year": NumberField(4, 4, 1, 9999),
+    "month": NumberField(2, 2, 1, 12),
+    "day": NumberField(2, 2, 1, 31),
+    "hour": NumberField(2, 2, 0, 24),  # 24 = special midnight value
+    "minute": NumberField(2, 2, 0, 59),
+    "second": NumberField(2, 2, 0, 60),  # 60 = Leap second
+    "microsecond": NumberField(1, None, 0, None),  # Can have unbounded characters
+    "tzhour": NumberField(2, 2, 0, 23),
+    "tzminute": NumberField(2, 2, 0, 59)
+}
+
+PADDED_NUMBER_FIELD_FORMATS = {
+    field_name: "{{{field_name}:0>{max_width}}}".format(field_name=field_name, max_width=field.max_width if field.max_width is not None else 1)
+    for field_name, field in NUMBER_FIELDS.items()
+}
+
+
+def __generate_valid_formats(year=2014, month=2, day=3, hour=1, minute=23, second=45, microsecond=123456, tzhour=4, tzminute=30):
+    # Given a set of values, generates the 400+ different combinations of those values within a valid ISO 8601 string.
+    # Returns a Python format string, the fields in the format string, and the corresponding parameters you could pass to the datetime constructor
+    # These can be used by generate_valid_timestamp_and_datetime and generate_invalid_timestamp_and_datetime to produce test cases
+
+    valid_basic_calendar_date_formats = [
+        ("{year}{month}{day}", set(["year", "month", "day"]), {"year": year, "month": month, "day": day})
+    ]
+
+    valid_extended_calendar_date_formats = [
+        ("{year}-{month}", set(["year", "month"]), {"year": year, "month": month, "day": 1}),
+        ("{year}-{month}-{day}", set(["year", "month", "day"]), {"year": year, "month": month, "day": day}),
+    ]
+
+    valid_date_and_time_separators = [
+        None,
+        'T',
+        't',
+        ' '
+    ]
+
+    valid_basic_time_formats = [
+        ("{hour}", set(["hour"]), {"hour": hour}),
+        ("{hour}{minute}", set(["hour", "minute"]), {"hour": hour, "minute": minute}),
+        ("{hour}{minute}{second}", set(["hour", "minute", "second"]), {"hour": hour, "minute": minute, "second": second})
+    ]
+
+    valid_extended_time_formats = [
+        ("{hour}", set(["hour"]), {"hour": hour}),
+        ("{hour}:{minute}", set(["hour", "minute"]), {"hour": hour, "minute": minute}),
+        ("{hour}:{minute}:{second}", set(["hour", "minute", "second"]), {"hour": hour, "minute": minute, "second": second}),
+    ]
+
+    valid_subseconds = [
+        ("", set(), {}),
+        (".{microsecond}", set(["microsecond"]), {"microsecond": microsecond}),  # TODO: Generate the trimmed 0's version?
+        (",{microsecond}", set(["microsecond"]), {"microsecond": microsecond}),
+    ]
+
+    valid_tz_info_formats = [
+        ("", set(), {}),
+        ("Z", set(), {"tzinfo": pytz.UTC}),
+        ("z", set(), {"tzinfo": pytz.UTC}),
+        ("-{tzhour}", set(["tzhour"]), {"tzinfo": pytz.FixedOffset(-1 * tzhour * 60)}),
+        ("+{tzhour}", set(["tzhour"]), {"tzinfo": pytz.FixedOffset(1 * tzhour * 60)}),
+        ("-{tzhour}{tzminute}", set(["tzhour", "tzminute"]), {"tzinfo": pytz.FixedOffset(-1 * ((tzhour * 60) + tzminute))}),
+        ("+{tzhour}{tzminute}", set(["tzhour", "tzminute"]), {"tzinfo": pytz.FixedOffset(1 * ((tzhour * 60) + tzminute))}),
+        ("-{tzhour}:{tzminute}", set(["tzhour", "tzminute"]), {"tzinfo": pytz.FixedOffset(-1 * ((tzhour * 60) + tzminute))}),
+        ("+{tzhour}:{tzminute}", set(["tzhour", "tzminute"]), {"tzinfo": pytz.FixedOffset(1 * ((tzhour * 60) + tzminute))})
+    ]
+
+    for valid_calendar_date_formats, valid_time_formats in [(valid_basic_calendar_date_formats, valid_basic_time_formats), (valid_extended_calendar_date_formats, valid_extended_time_formats)]:
+        for calendar_format, calendar_fields, calendar_params in valid_calendar_date_formats:
+            for date_and_time_separator in valid_date_and_time_separators:
+                if date_and_time_separator is None:
+                    full_format = calendar_format
+                    datetime_params = calendar_params
+                    yield (full_format, calendar_fields, datetime_params)
+                else:
+                    for time_format, time_fields, time_params in valid_time_formats:
+                        for subsecond_format, subsecond_fields, subsecond_params in valid_subseconds:
+                            for tz_info_format, tz_info_fields, tz_info_params in valid_tz_info_formats:
+                                if "second" in time_fields:
+                                    # Add subsecond
+                                    full_format = calendar_format + date_and_time_separator + time_format + subsecond_format + tz_info_format
+                                    fields = set().union(calendar_fields, time_fields, subsecond_fields, tz_info_fields)
+                                    datetime_params = __merge_dicts(calendar_params, time_params, subsecond_params, tz_info_params)
+                                elif subsecond_format == "":  # Arbitrary choice of subsecond format. We don't want duplicates, so we only yield for one of them.
+                                    full_format = calendar_format + date_and_time_separator + time_format + tz_info_format
+                                    fields = set().union(calendar_fields, time_fields, tz_info_fields)
+                                    datetime_params = __merge_dicts(calendar_params, time_params, tz_info_params)
+                                else:
+                                    # Ignore other subsecond formats
+                                    continue
+
+                                yield (full_format, fields, datetime_params)
+
+
+def __pad_params(**kwargs):
+    # Pads parameters to the required field widths.
+    return {key: PADDED_NUMBER_FIELD_FORMATS[key].format(**{key: value}) if key in PADDED_NUMBER_FIELD_FORMATS else value for key, value in kwargs.items()}
+
+
+def generate_valid_timestamp_and_datetime(year=2014, month=2, day=3, hour=1, minute=23, second=45, microsecond=123456, tzhour=4, tzminute=30):
+    # Given a set of values, generates the 400+ different combinations of those values within a valid ISO 8601 string, and the corresponding datetime
+    # This can be used to generate test cases of valid ISO 8601 timestamps.
+
+    # Note that this will produce many test cases that exercise the exact same code pathways (ie. offer no additional coverage).
+    # Given a knowledge of the code, this is excessive, but these serve as a good set of black box tests (ie. You could apply these to any ISO 8601 parse).
+
+    kwargs = {
+        "year": year,
+        "month": month,
+        "day": day,
+        "hour": hour,
+        "minute": minute,
+        "second": second,
+        "microsecond": microsecond,
+        "tzhour": tzhour,
+        "tzminute": tzminute
+    }
+    for timestamp_format, _fields, datetime_params in __generate_valid_formats(**kwargs):
+        # Pad each field to the appropriate width
+        padded_kwargs = __pad_params(**kwargs)
+        timestamp = timestamp_format.format(**padded_kwargs)
+        yield (timestamp, datetime.datetime(**datetime_params))
+
+
+def generate_invalid_timestamp(year=2014, month=2, day=3, hour=1, minute=23, second=45, microsecond=123456, tzhour=4, tzminute=30):
+    # At the very least, each field can be invalid in the following ways:
+    #   - Have too few characters
+    #   - Have too many characters
+    #   - Contain invalid characters
+    #   - Have a value that is too small
+    #   - Have a value that is too large
+    #
+    # This function takes each valid format (from `__generate_valid_formats()`), and mangles each field within the format to be invalid in each of the above ways.
+    # It also tests the case of trailing characters after each format.
+
+    # Note that this will produce many test cases that exercise the exact same code pathways (ie. offer no additional coverage).
+    # Given a knowledge of the code, this is excessive, but these serve as a good set of black box tests (ie. You could apply these to any ISO 8601 parse).
+
+    # This does not produce every invalid timestamp format though. For simplicity of the code, it does not cover the cases of:
+    #   - The fields having 0 characters (Many fields (like day, minute, second etc.) are optional. So unless the field follows a separator, it is valid to have 0 characters)
+    #   - Invalid day numbers for a given month (ex. "2014-02-31")
+    #   - Invalid separators (ex. "2014=04=01")
+    #   - Missing/Mismatched separators (ex. "2014-0101T0000:00")
+    #   - Hour = 24, but not Special midnight case  (ex. "24:00:01")
+    #   - Timestamps that bear no resemblance to ISO 8601
+    # These cases will need to be test separately
+
+    kwargs = {
+        "year": year,
+        "month": month,
+        "day": day,
+        "hour": hour,
+        "minute": minute,
+        "second": second,
+        "microsecond": microsecond,
+        "tzhour": tzhour,
+        "tzminute": tzminute
+    }
+
+    for timestamp_format, fields, _datetime_params in __generate_valid_formats(**kwargs):
+        for field_name in fields:
+            mangled_kwargs = __pad_params(**kwargs)
+            field = NUMBER_FIELDS.get(field_name, None)
+            if field is not None:
+                # Too few characters
+                for length in range(1, field.min_width):
+                    str_value = str(__pad_params(**{field_name: kwargs[field_name]})[field_name])[0:length]
+                    mangled_kwargs[field_name] = "{{:0>{length}}}".format(length=length).format(str_value)
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+
+                # Too many characters
+                if field.max_width is not None:
+                    mangled_kwargs[field_name] = "{{:0>{length}}}".format(length=field.max_width + 1).format(kwargs[field_name])
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+
+                # Too small of value
+                if (field.min_value - 1) >= 0:
+                    mangled_kwargs[field_name] = __pad_params(**{field_name: field.min_value - 1})[field_name]
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+
+                # Too large of value
+                if field.max_value is not None:
+                    mangled_kwargs[field_name] = __pad_params(**{field_name: field.max_value + 1})[field_name]
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+
+                # Invalid characters
+                max_invalid_characters = field.max_width if field.max_width is not None else 1
+                # ex. 2014 -> a, aa, aaa
+                for length in range(1, max_invalid_characters):
+                    mangled_kwargs[field_name] = "a" * length
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+                # ex. 2014 -> aaaa, 2aaa, 20aa, 201a
+                for length in range(0, max_invalid_characters):
+                    str_value = str(__pad_params(**{field_name: kwargs[field_name]})[field_name])[0:length]
+                    mangled_kwargs[field_name] = "{{:a<{length}}}".format(length=max_invalid_characters).format(str_value)
+                    timestamp = timestamp_format.format(**mangled_kwargs)
+                    yield timestamp
+
+        # Trailing characters
+        timestamp = timestamp_format.format(**__pad_params(**kwargs)) + "EXTRA"
+        yield timestamp


=====================================
setup.cfg deleted
=====================================
@@ -1,4 +0,0 @@
-[egg_info]
-tag_build = 
-tag_date = 0
-


=====================================
tests.py
=====================================
@@ -0,0 +1,446 @@
+# -*- coding: utf-8 -*-
+
+import ciso8601
+import datetime
+import sys
+
+from generate_test_timestamps import generate_valid_timestamp_and_datetime, generate_invalid_timestamp
+
+if sys.version_info.major == 2:
+    # We use unittest2 since it has a backport of the `unittest.TestCase.assertRaisesRegex` method,
+    # which is called `assertRaisesRegexp` in Python 2. This saves us the hassle of monkey-patching
+    # the class ourselves.
+    import unittest2 as unittest
+else:
+    import unittest
+
+
+class ValidTimestampTestCase(unittest.TestCase):
+    def test_auto_generated_valid_formats(self):
+        for (timestamp, expected_datetime) in generate_valid_timestamp_and_datetime():
+            try:
+                self.assertEqual(ciso8601.parse_datetime(timestamp), expected_datetime)
+            except Exception:
+                print("Had problems parsing: {timestamp}".format(timestamp=timestamp))
+                raise
+
+    def test_parse_as_naive_auto_generated_valid_formats(self):
+        for (timestamp, expected_datetime) in generate_valid_timestamp_and_datetime():
+            try:
+                self.assertEqual(ciso8601.parse_datetime_as_naive(timestamp), expected_datetime.replace(tzinfo=None))
+            except Exception:
+                print("Had problems parsing: {timestamp}".format(timestamp=timestamp))
+                raise
+
+    def test_excessive_subsecond_precision(self):
+        self.assertEqual(
+            ciso8601.parse_datetime('20140203T103527.234567891234'),
+            datetime.datetime(2014, 2, 3, 10, 35, 27, 234567)
+        )
+
+    def test_leap_year(self):
+        # There is nothing unusual about leap years in ISO 8601.
+        # We just want to make sure that they work in general.
+        for leap_year in (1600, 2000, 2016):
+            self.assertEqual(
+                ciso8601.parse_datetime('{}-02-29'.format(leap_year)),
+                datetime.datetime(leap_year, 2, 29, 0, 0, 0, 0)
+            )
+
+    def test_special_midnight(self):
+        self.assertEqual(
+            ciso8601.parse_datetime('2014-02-03T24:00:00'),
+            datetime.datetime(2014, 2, 4, 0, 0, 0)
+        )
+
+
+class InvalidTimestampTestCase(unittest.TestCase):
+    # Many invalid test cases are covered by `test_parse_auto_generated_invalid_formats`,
+    # But it doesn't cover all invalid cases, so we test those here.
+    # See `generate_test_timestamps.generate_invalid_timestamp` for details.
+
+    def test_parse_auto_generated_invalid_formats(self):
+        for timestamp in generate_invalid_timestamp():
+            try:
+                with self.assertRaises(ValueError, msg="Timestamp '{0}' was supposed to be invalid, but parsing it didn't raise ValueError.".format(timestamp)):
+                    ciso8601.parse_datetime(timestamp)
+            except Exception as exc:
+                print("Timestamp '{0}' was supposed to raise ValueError, but raised {1} instead".format(timestamp, type(exc).__name__))
+                raise
+
+    def test_non_ascii_characters(self):
+        if sys.version_info >= (3, 3):
+            self.assertRaisesRegex(
+                ValueError,
+                r"Invalid character while parsing date separator \('-'\) \('🐵', Index: 7\)",
+                ciso8601.parse_datetime,
+                '2019-01🐵01',
+            )
+            self.assertRaisesRegex(
+                ValueError,
+                r"Invalid character while parsing day \('🐵', Index: 8\)",
+                ciso8601.parse_datetime,
+                '2019-01-🐵',
+            )
+        else:
+            self.assertRaisesRegex(
+                ValueError,
+                r"Invalid character while parsing date separator \('-'\) \(Index: 7\)",
+                ciso8601.parse_datetime,
+                '2019-01🐵01',
+            )
+            self.assertRaisesRegex(
+                ValueError,
+                r"Invalid character while parsing day \(Index: 8\)",
+                ciso8601.parse_datetime,
+                '2019-01-🐵',
+            )
+
+    def test_invalid_calendar_separator(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing month",
+            ciso8601.parse_datetime,
+            '2018=01=01',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing date separator \('-'\) \('=', Index: 7\)",
+            ciso8601.parse_datetime,
+            '2018-01=01',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing date separator \('-'\) \('0', Index: 7\)",
+            ciso8601.parse_datetime,
+            '2018-0101',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing day \('-', Index: 6\)",
+            ciso8601.parse_datetime,
+            '201801-01',
+        )
+
+    def test_invalid_empty_but_required_fields(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing year. Expected 4 more characters",
+            ciso8601.parse_datetime,
+            '',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing month. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing day. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing hour. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-01T',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing minute. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing second. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing subsecond. Expected 1 more character",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing tz hour. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.00+',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing tz minute. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.00-00:',
+        )
+
+    def test_invalid_day_for_month(self):
+        for non_leap_year in (1700, 1800, 1900, 2014):
+            self.assertRaisesRegex(
+                ValueError,
+                r"day is out of range for month",
+                ciso8601.parse_datetime,
+                '{}-02-29'.format(non_leap_year)
+            )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"day is out of range for month",
+            ciso8601.parse_datetime,
+            '2014-01-32',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"day is out of range for month",
+            ciso8601.parse_datetime,
+            '2014-06-31',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"day is out of range for month",
+            ciso8601.parse_datetime,
+            '2014-06-00',
+        )
+
+    def test_invalid_yyyymm_format(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Unexpected end of string while parsing day. Expected 2 more characters",
+            ciso8601.parse_datetime,
+            '201406',
+        )
+
+    def test_invalid_date_and_time_separator(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing date and time separator \(ie. 'T' or ' '\) \('_', Index: 10\)",
+            ciso8601.parse_datetime,
+            '2018-01-01_00:00:00',
+        )
+
+    def test_invalid_hour_24(self):
+        # A value of hour = 24 is only valid in the special case of 24:00:00
+        self.assertRaisesRegex(
+            ValueError,
+            r"hour must be in 0..23",
+            ciso8601.parse_datetime,
+            '2014-02-03T24:35:27',
+        )
+
+    def test_invalid_time_separator(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing time separator \(':'\) \('=', Index: 16\)",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00=00'
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing time separator \(':'\) \('0', Index: 16\)",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:0000'
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing second \(':', Index: 15\)",
+            ciso8601.parse_datetime,
+            '2018-01-01T0000:00'
+        )
+
+    def test_invalid_tz_minute(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"tzminute must be in 0..59",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.00-00:99',
+        )
+
+    def test_invalid_tz_offsets_too_large(self):
+        # The Python interpreter crashes if you give the datetime constructor a TZ offset with an absolute value >= 1440
+        # TODO: Determine whether these are valid ISO 8601 values and therefore whether ciso8601 should support them.
+        self.assertRaisesRegex(
+            ValueError,
+            # Error message differs whether or not we are using pytz or datetime.timezone
+            r"^offset must be a timedelta strictly between" if sys.version_info.major >= 3 else r"\('absolute offset is too large', -5940\)",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.00-99',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"tzminute must be in 0..59",
+            ciso8601.parse_datetime,
+            '2018-01-01T00:00:00.00-23:60',
+        )
+
+    def test_mixed_basic_and_extended_formats(self):
+        """
+        Both dates and times have "basic" and "extended" formats.
+        But when you combine them into a datetime, the date and time components
+        must have the same format.
+        """
+        self.assertRaisesRegex(
+            ValueError,
+            r"Cannot combine \"extended\" date format with \"basic\" time format",
+            ciso8601.parse_datetime,
+            '2014-01-02T010203',
+        ),
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Cannot combine \"basic\" date format with \"extended\" time format",
+            ciso8601.parse_datetime,
+            '20140102T01:02:03',
+        )
+
+
+class Rfc3339TestCase(unittest.TestCase):
+    def test_valid_rfc3339_timestamps(self):
+        """
+        Validate that valid RFC 3339 datetimes are parseable by parse_rfc3339
+        and produce the same result as parse_datetime.
+        """
+        for string in [
+                '2018-01-02T03:04:05Z',
+                '2018-01-02t03:04:05z',
+                '2018-01-02 03:04:05z',
+                '2018-01-02T03:04:05+00:00',
+                '2018-01-02T03:04:05-00:00',
+                '2018-01-02T03:04:05.12345Z',
+                '2018-01-02T03:04:05+01:23',
+                '2018-01-02T03:04:05-12:34',
+                '2018-01-02T03:04:05-12:34',
+        ]:
+            self.assertEqual(ciso8601.parse_datetime(string),
+                             ciso8601.parse_rfc3339(string))
+
+    def test_invalid_rfc3339_timestamps(self):
+        """
+        Validate that datetime strings that are valid ISO 8601 but invalid RFC
+        3339 trigger a ValueError when passed to RFC 3339, and that this
+        ValueError explicitly mentions RFC 3339.
+        """
+        for timestamp in [
+                "2018-01-02",  # Missing mandatory time
+                "2018-01-02T03",  # Missing mandatory minute and second
+                "2018-01-02T03Z",  # Missing mandatory minute and second
+                "2018-01-02T03:04",  # Missing mandatory minute and second
+                "2018-01-02T03:04Z",  # Missing mandatory minute and second
+                "2018-01-02T03:04:01+04",  # Missing mandatory offset minute
+                "2018-01-02T03:04:05",  # Missing mandatory offset
+                "2018-01-02T03:04:05.12345",  # Missing mandatory offset
+                "2018-01-02T24:00:00Z",  # 24:00:00 is not valid in RFC 3339
+                '20180102T03:04:05-12:34',  # Missing mandatory date separators
+                '2018-01-02T030405-12:34',  # Missing mandatory time separators
+                '2018-01-02T03:04:05-1234',  # Missing mandatory offset separator
+                '2018-01-02T03:04:05,12345Z'  # Invalid comma fractional second separator
+        ]:
+            with self.assertRaisesRegex(ValueError, r"RFC 3339", msg="Timestamp '{0}' was supposed to be invalid, but parsing it didn't raise ValueError.".format(timestamp)):
+                ciso8601.parse_rfc3339(timestamp)
+
+
+class GithubIssueRegressionTestCase(unittest.TestCase):
+    # These are test cases that were provided in GitHub issues submitted to ciso8601.
+    # They are kept here as regression tests.
+    # They might not have any additional value above-and-beyond what is already tested in the normal unit tests.
+
+    def test_issue_5(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing minute \(':', Index: 14\)",
+            ciso8601.parse_datetime,
+            '2014-02-03T10::27',
+        )
+
+    def test_issue_6(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing second \('.', Index: 17\)",
+            ciso8601.parse_datetime,
+            '2014-02-03 04:05:.123456',
+        )
+
+    def test_issue_8(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"hour must be in 0..23",
+            ciso8601.parse_datetime,
+            '2001-01-01T24:01:01',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"month must be in 1..12",
+            ciso8601.parse_datetime,
+            '07722968',
+        )
+
+    def test_issue_13(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"month must be in 1..12",
+            ciso8601.parse_datetime,
+            '2014-13-01',
+        )
+
+    def test_issue_22(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"day is out of range for month",
+            ciso8601.parse_datetime,
+            '2016-11-31T12:34:34.521059',
+        )
+
+    def test_issue_35(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid character while parsing date separator \('-'\) \('1', Index: 7\)",
+            ciso8601.parse_datetime,
+            '2017-0012-27T13:35:19+0200',
+        )
+
+    def test_issue_42(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"day is out of range for month",
+            ciso8601.parse_datetime,
+            '20140200',
+        )
+
+    def test_issue_71(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Cannot combine \"basic\" date format with \"extended\" time format",
+            ciso8601.parse_datetime,
+            '20010203T04:05:06Z',
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Cannot combine \"basic\" date format with \"extended\" time format",
+            ciso8601.parse_datetime,
+            '20010203T04:05',
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()


=====================================
tox.ini
=====================================
@@ -0,0 +1,11 @@
+[tox]
+envlist = py27,py34,py35,py36,py37,py38
+
+[testenv]
+setenv=
+    STRICT_WARNINGS=1
+deps=
+    pytz
+    nose
+    unittest2
+commands=nosetests



View it on GitLab: https://salsa.debian.org/med-team/python-ciso8601/-/compare/469bede427e6d7591f3b73ae52a9feb8a1403bf7...b2881fe5db42e4cab376f148006204cec06d4abd

-- 
View it on GitLab: https://salsa.debian.org/med-team/python-ciso8601/-/compare/469bede427e6d7591f3b73ae52a9feb8a1403bf7...b2881fe5db42e4cab376f148006204cec06d4abd
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20200419/bb90dabc/attachment-0001.html>


More information about the debian-med-commit mailing list