[med-svn] [Git][python-team/packages/python-ciso8601][upstream] New upstream version 2.3.1

Lance Lin (@linqigang) gitlab at salsa.debian.org
Wed Nov 8 12:44:51 GMT 2023



Lance Lin pushed to branch upstream at Debian Python Team / packages / python-ciso8601


Commits:
d099081d by Lance Lin at 2023-11-07T21:25:29+07:00
New upstream version 2.3.1
- - - - -


26 changed files:

- .circleci/config.yml
- .clang-format
- + .github/pull_request_template.md
- + .github/workflows/build-wheels.yml
- CHANGELOG.md
- CONTRIBUTING.md
- MANIFEST.in
- README.rst
- + RELEASING.md
- benchmarking/Dockerfile
- benchmarking/README.rst
- benchmarking/format_results.py
- benchmarking/perform_comparison.py
- benchmarking/requirements.txt
- benchmarking/run_benchmarks.sh
- benchmarking/tox.ini
- generate_test_timestamps.py
- + isocalendar.c
- + isocalendar.h
- module.c
- setup.py
- tests/test_timezone.py
- tests/tests.py
- timezone.c
- tox.ini
- + why_ciso8601.md


Changes:

=====================================
.circleci/config.yml
=====================================
@@ -3,17 +3,30 @@ version: 2.1
 workflows:
   workflow:
     jobs:
+      - test_python_34
       - test:
           matrix:
             parameters:
-              python_version: ["2.7", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9"]
+              python_version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
       - test_pypy:
           matrix:
             parameters:
-              python_version: ["2.7", "3.7"]
+              python_version: ["2.7", "3.7", "3.8", "3.9", "3.10"]
       - lint-rst
+      - clang-format
 
 jobs:
+  # `cimg/python` doesn't support Python 3.4,
+  # but old `circleci/python` is still around!
+  test_python_34:
+    steps:
+      - checkout
+      - run:
+          name: Test
+          command: python setup.py test
+    docker:
+      - image: circleci/python:3.4
+
   test:
     parameters:
       python_version:
@@ -24,7 +37,7 @@ jobs:
           name: Test
           command: python setup.py test
     docker:
-      - image: circleci/python:<<parameters.python_version>>
+      - image: cimg/python:<<parameters.python_version>>
 
   test_pypy:
     parameters:
@@ -54,4 +67,28 @@ jobs:
             . venv/bin/activate
             rst-lint --encoding=utf-8 README.rst
     docker:
-      - image: circleci/python:3.9
+      - image: cimg/python:3.12
+
+  clang-format:
+    working_directory: ~/code
+    steps:
+      - checkout
+      - run:
+          name: Install lint tools
+          command: |
+            sudo apt-get update -y
+            sudo apt-get install -y clang-format
+      - run:
+          name: Lint
+          command: |
+            SOURCE_FILES=`find ./ -name \*.c -type f -or -name \*.h -type f`
+            for SOURCE_FILE in $SOURCE_FILES
+            do
+              export FORMATTING_ISSUE_COUNT=`clang-format -output-replacements-xml $SOURCE_FILE | grep offset | wc -l`
+              if [ "$FORMATTING_ISSUE_COUNT" -gt "0" ]; then
+                echo "Source file $SOURCE_FILE contains formatting issues. Please use clang-format tool to resolve found issues."
+                exit 1
+              fi
+            done
+    docker:
+      - image: cimg/python:3.12


=====================================
.clang-format
=====================================
@@ -4,6 +4,7 @@ BasedOnStyle: Google
 AlwaysBreakAfterReturnType: All
 AllowShortIfStatementsOnASingleLine: false
 AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: Consecutive
 BreakBeforeBraces: Stroustrup
 ColumnLimit: 79
 DerivePointerAlignment: false
@@ -12,6 +13,7 @@ Language: Cpp
 PointerAlignment: Right
 ReflowComments: true
 SpaceBeforeParens: ControlStatements
+SpacesBeforeTrailingComments: 2
 SpacesInParentheses: false
 TabWidth: 4
 UseTab: Never


=====================================
.github/pull_request_template.md
=====================================
@@ -0,0 +1,34 @@
+### What are you trying to accomplish?
+<!--
+Link to an issue or provide enough context so that someone new can understand the 'why' behind this change.
+-->
+
+...
+
+### What approach did you choose and why?
+<!--
+There are many ways to solve a problem. How did you approach this problem and why?
+-->
+
+...
+
+### What should reviewers focus on?
+<!--
+Outline anything you'd like reviewers to pay extra attention to. List open questions for discussion.
+-->
+
+...
+
+### The impact of these changes
+<!--
+Are there any specific impacts from this change that you'd like to call out?
+-->
+
+...
+
+### Testing
+<!--
+Are there any test results or screenshots that would be useful to include? How can a reviewer try out your change?
+-->
+
+...


=====================================
.github/workflows/build-wheels.yml
=====================================
@@ -0,0 +1,169 @@
+name: Build Wheels
+
+on:
+  workflow_dispatch:
+    inputs:
+      requested_release_tag:
+        description: 'The tag to use for this release (e.g., `v2.3.1`)'
+        required: true
+
+jobs:
+  sanity_check:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout at v3
+
+      - uses: actions/setup-python at v4
+        name: Install Python
+        with:
+          python-version: '3.12'
+
+      - run: |
+          pip install packaging
+
+      - name: Normalize the release version
+        run: |
+          echo "release_version=`echo '${{ github.event.inputs.requested_release_tag }}' | sed 's/^v//'`" >> $GITHUB_ENV
+
+      - name: Normalize the release tag
+        run: |
+          echo "release_tag=v${release_version}" >> $GITHUB_ENV
+
+      - name: Get the VERSION from setup.py
+        run: |
+          echo "ciso8601_version=`grep -Po 'VERSION = "\K[^"]*' setup.py`" >> $GITHUB_ENV
+
+      - name: Get the latest version from PyPI
+        run: |
+          curl https://pypi.org/pypi/ciso8601/json | python -c 'import json, sys; contents=sys.stdin.read(); parsed = json.loads(contents); print("pypi_version=" + parsed["info"]["version"])' >> $GITHUB_ENV
+
+      - name: Log all the things
+        run: |
+          echo 'Requested release tag `${{ github.event.inputs.requested_release_tag }}`'
+          echo 'Release version       `${{ env.release_version }}`'
+          echo 'Release tag           `${{ env.release_tag }}`'
+          echo 'VERSION in setup.py   `${{ env.ciso8601_version }}`'
+          echo 'Version in PyPI       `${{ env.pypi_version }}`'
+
+      - name: Verify that the version string we produced looks like a version string
+        run: |
+          echo "${{ env.release_version }}" | sed '/^[0-9]\+\.[0-9]\+\.[0-9]\+$/!{q1}'
+
+      - name: Verify that the version tag we produced looks like a version tag
+        run: |
+          echo "${{ env.release_tag }}" | sed '/^v[0-9]\+\.[0-9]\+\.[0-9]\+$/!{q1}'
+
+      - name: Verify that the release version matches the VERSION in setup.py
+        run: |
+          [[ ${{ env.release_version }} == ${{ env.ciso8601_version }} ]]
+
+      - name: Verify that the `release_version` is larger/newer than the existing release in PyPI
+        run: |
+          python -c 'import sys; from packaging import version; code = 0 if version.parse("${{ env.pypi_version }}") < version.parse("${{ env.release_version }}") else 1; sys.exit(code)'
+
+      - name: Verify that the `release_version` is present in the CHANGELOG
+        # TODO: Use something like `changelog-cli` to extract the correct version number
+        run: |
+          grep ${{ env.release_version }} CHANGELOG.md
+
+      - name: Serialize normalized release values
+        run: |
+          echo -e "release_version=${{ env.release_version }}\nrelease_tag=${{ env.release_tag }}" > release_values.txt
+
+      - name: Share normalized release values
+        uses: actions/upload-artifact at v3
+        with:
+          name: release_values
+          path: release_values.txt
+
+  build_wheels:
+    name: Build wheel on ${{ matrix.os }}
+    needs: [sanity_check]
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+      - uses: actions/checkout at v3
+
+      - name: Build wheels
+        uses: closeio/cibuildwheel at v2.16.2
+        env:
+          CIBW_SKIP: "pp*-macosx* *-win32 *-manylinux_i686"
+          CIBW_ARCHS_MACOS: x86_64 arm64 universal2
+
+      - uses: actions/upload-artifact at v3
+        with:
+          path: ./wheelhouse/*.whl
+
+  build_sdist:
+    name: Build source distribution
+    needs: [sanity_check]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout at v3
+
+      - uses: actions/setup-python at v4
+        name: Install Python
+        with:
+          python-version: '3.12'
+
+      - name: Get build tool
+        run: pip install --upgrade build
+
+      - name: Build sdist
+        run: python -m build
+
+      - uses: actions/upload-artifact at v3
+        with:
+          path: dist/*.tar.gz
+
+  # create_or_update_draft_release:
+  #   # TODO: Figure out how to do this in an idempotent way
+  #   name: Create or update draft release in GitHub
+  #   needs: [build_wheels, build_sdist, sanity_check]
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - name: Get normalized release values
+  #       uses: actions/download-artifact at v2
+  #       with:
+  #         name: release_values
+
+  #     - name: Load normalized release values
+  #       run: |
+  #         xargs -a release_values.txt -l -I{} bash -c 'echo {} >> $GITHUB_ENV'
+
+  #     - uses: actions/create-release at v1
+  #       env:
+  #         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions
+  #       with:
+  #         tag_name: ${{ env.release_tag }}
+  #         commitish: master # Default branch
+  #         release_name: ${{ env.release_tag }}
+  #         body: "" # TODO: Pull this in using `changelog-cli`
+  #         draft: true
+
+  # upload_to_pypi:
+  #   name: Upload wheels to PyPI
+  #   needs: [build_wheels, build_sdist, sanity_check]
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/download-artifact at v2
+  #       with:
+  #         name: artifact
+  #         path: dist
+
+  #     - uses: closeio/gh-action-pypi-publish at v1.4.2
+  #       with:
+  #         user: __token__
+  #         password: ${{ secrets.PYPI_PASSWORD }}
+  #         # repository_url: https://test.pypi.org/legacy/ # Test PyPI
+
+  # create_release:
+  #   name: Create release in GitHub
+  #   needs: [upload_to_pypi]
+  #   runs-on: ubuntu-latest
+  #   steps:  # TODO
+  #     - run: |
+  #         echo "We're doing a release!? ${{ github.event }}"


=====================================
CHANGELOG.md
=====================================
@@ -1,8 +1,9 @@
-<!-- Generated with "Markdown T​O​C" extension for Visual Studio Code -->
-<!-- TOC anchorMode:github.com -->
+<!-- Generated with "Markdown All in One" extension for Visual Studio Code -->
 
 - [Unreleased](#unreleased)
 - [2.x.x](#2xx)
+  - [Version 2.3.1](#version-231)
+  - [Version 2.3.0](#version-230)
   - [Version 2.2.0](#version-220)
   - [Version 2.1.3](#version-213)
   - [Version 2.1.2](#version-212)
@@ -12,7 +13,7 @@
   - [Version 2.0.0](#version-200)
     - [Breaking changes](#breaking-changes)
     - [Other Changes](#other-changes)
-    - [v1.x.x -> 2.0.0 Migration guide](#v1xx---200-migration-guide)
+    - [v1.x.x -\> 2.0.0 Migration guide](#v1xx---200-migration-guide)
       - [ValueError instead of None](#valueerror-instead-of-none)
       - [Tightened ISO 8601 conformance](#tightened-iso-8601-conformance)
       - [`parse_datetime_unaware` has been renamed](#parse_datetime_unaware-has-been-renamed)
@@ -25,6 +26,21 @@
 
 # 2.x.x
 
+## Version 2.3.1
+
+* Added Python 3.12 wheels
+
+## Version 2.3.0
+
+* Added Python 3.11 support
+* Fix the build for PyPy2 ([#116](https://github.com/closeio/ciso8601/pull/116))
+* Added missing `fromutc` implementation for `FixedOffset` (#113). Thanks @davidkraljic
+* Removed improper ability to call `FixedOffset`'s `dst`, `tzname` and `utcoffset` without arguments
+* Fixed: `datetime.tzname` returns a `str` in Python 2.7, not a `unicode`
+* Change `METH_VARARGS` to `METH_O`, enhancing performance. ([#130](https://github.com/closeio/ciso8601/pull/130))
+* Added support for ISO week dates, ([#139](https://github.com/closeio/ciso8601/pull/139))
+* Added support for ordinal dates, ([#140](https://github.com/closeio/ciso8601/pull/140))
+
 ## Version 2.2.0
 
 * Added Python 3.9 support
@@ -57,7 +73,7 @@
 * Added [Mypy](http://mypy-lang.org/)/[PEP 484](https://www.python.org/dev/peps/pep-0484/) typing information (#68, Thanks @NickG123).
 * Added a new function: `parse_rfc3339`, which strictly parses RFC 3339 (#70).
 * No longer accept mixed "basic" and "extended" format timestamps (#73).
-    * ex. `20140203T23:35:27` and `2014-02-03T233527` are not valid in ISO 8601, but were not raising `ValueError`.
+    * e.g., `20140203T23:35:27` and `2014-02-03T233527` are not valid in ISO 8601, but were not raising `ValueError`.
     * Attempting to parse such timestamps now raises `ValueError`
 
 ## Version 2.0.1


=====================================
CONTRIBUTING.md
=====================================
@@ -8,7 +8,7 @@ The following is a set of guidelines for contributing to ciso8601, which are hos
 
 [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)
+[Design Philosophy](#design-philosophy)
 
 [How Can I Contribute?](#how-can-i-contribute)
   * [Reporting Bugs](#reporting-bugs)
@@ -29,7 +29,7 @@ Sure. First [search the existing issues](https://github.com/closeio/ciso8601/iss
 
 ## 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. 
+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.
 
@@ -73,7 +73,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit
 
 #### 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. 
+* **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.
 
@@ -107,7 +107,7 @@ See [this guide](https://opensource.guide/how-to-contribute/#opening-a-pull-requ
 
 #### C Coding Style
 
-ciso8601 tries to adhere to the [Python PEP 7](https://www.python.org/dev/peps/pep-0007/) style guide. 
+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.
 
@@ -115,7 +115,7 @@ You can use [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html) to make
 
 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).   
+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
 
@@ -165,4 +165,3 @@ rst-lint --encoding=utf-8 README.rst
 
 * Follow the [C Code](#c-coding-style) style guide.
 * Document new code and functionality [See "Documentation"](#documentation)
-


=====================================
MANIFEST.in
=====================================
@@ -1,4 +1,5 @@
 include LICENSE
 include README.rst
 include CHANGELOG.md
+include isocalendar.h
 include timezone.h


=====================================
README.rst
=====================================
@@ -14,15 +14,11 @@ 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 cPython 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9.
-
-**Note:** ciso8601 doesn't support the entirety of the ISO 8601 spec, `only a popular subset`_.
+Tested with cPython 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12.
 
 .. _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
@@ -32,7 +28,7 @@ Tested with cPython 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9.
 .. contents:: Contents
 
 
-Quick Start
+Quick start
 -----------
 
 .. code:: bash
@@ -57,7 +53,14 @@ See `CHANGELOG`_ for the Migration Guide.
 
 .. _CHANGELOG: https://github.com/closeio/ciso8601/blob/master/CHANGELOG.md
 
-Error Handling
+When should I not use ``ciso8601``?
+-----------------------------------
+
+``ciso8601`` is not necessarily the best solution for every use case (especially since Python 3.11). See `Should I use ciso8601?`_
+
+.. _`Should I use ciso8601?`: https://github.com/closeio/ciso8601/blob/master/why_ciso8601.md
+
+Error handling
 --------------
 
 Starting in v2.0.0, ``ciso8601`` offers strong guarantees when it comes to parsing strings.
@@ -72,125 +75,140 @@ If time zone information is provided, an aware datetime object will be returned.
 Benchmark
 ---------
 
-Parsing a timestamp with no time zone information (ex. ``2014-01-09T21:48:00``):
+Parsing a timestamp with no time zone information (e.g., ``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.
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |             Module             |Python 3.12|Python 3.11|Python 3.10|Python 3.9|Relative slowdown (versus ciso8601, latest Python)|…|Python 3.8|Python 3.7|          Python 2.7           |
+    +================================+===========+===========+===========+==========+==================================================+=+==========+==========+===============================+
+    |ciso8601                        |98 nsec    |90 nsec    |122 nsec   |122 nsec  |N/A                                               |…|118 nsec  |124 nsec  |134 nsec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |backports.datetime_fromisoformat|N/A        |N/A        |112 nsec   |108 nsec  |0.9x                                              |…|106 nsec  |118 nsec  |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |datetime (builtin)              |129 nsec   |132 nsec   |N/A        |N/A       |1.3x                                              |…|N/A       |N/A       |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |pendulum                        |N/A        |180 nsec   |187 nsec   |186 nsec  |2.0x                                              |…|196 nsec  |200 nsec  |8.52 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |udatetime                       |695 nsec   |662 nsec   |674 nsec   |692 nsec  |7.1x                                              |…|724 nsec  |713 nsec  |586 nsec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |str2date                        |6.86 usec  |5.78 usec  |6.59 usec  |6.4 usec  |70.0x                                             |…|6.66 usec |6.96 usec |❌                             |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |iso8601utils                    |N/A        |N/A        |N/A        |8.59 usec |70.5x                                             |…|8.6 usec  |9.59 usec |11.2 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |iso8601                         |10 usec    |8.24 usec  |8.96 usec  |9.21 usec |102.2x                                            |…|9.14 usec |9.63 usec |25.7 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |isodate                         |11.1 usec  |8.76 usec  |10.2 usec  |9.76 usec |113.6x                                            |…|9.92 usec |11 usec   |44.1 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |PySO8601                        |17.2 usec  |13.6 usec  |16 usec    |15.8 usec |175.3x                                            |…|16.1 usec |17.1 usec |17.7 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |aniso8601                       |22.2 usec  |17.8 usec  |23.2 usec  |23.1 usec |227.0x                                            |…|24.3 usec |27.2 usec |30.7 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |zulu                            |23.3 usec  |19 usec    |22 usec    |21.3 usec |237.9x                                            |…|21.6 usec |22.7 usec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |maya                            |N/A        |36.1 usec  |42.5 usec  |42.7 usec |401.6x                                            |…|41.3 usec |44.2 usec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |python-dateutil                 |57.6 usec  |51.4 usec  |63.3 usec  |62.6 usec |587.7x                                            |…|63.7 usec |67.3 usec |119 usec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |arrow                           |62 usec    |54 usec    |65.5 usec  |65.7 usec |633.0x                                            |…|66.6 usec |70.2 usec |78.8 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |metomi-isodatetime              |1.29 msec  |1.33 msec  |1.76 msec  |1.77 msec |13201.1x                                          |…|1.79 msec |1.91 msec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |moment                          |1.81 msec  |1.65 msec  |1.75 msec  |1.79 msec |18474.8x                                          |…|1.78 msec |1.84 msec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+
+ciso8601 takes 98 nsec, which is **1.3x faster than datetime (builtin)**, the next fastest Python 3.12 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``):
+Parsing a timestamp with time zone information (e.g., ``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.
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |             Module             |Python 3.12|Python 3.11|Python 3.10|Python 3.9|Relative slowdown (versus ciso8601, latest Python)|…|Python 3.8|Python 3.7|          Python 2.7           |
+    +================================+===========+===========+===========+==========+==================================================+=+==========+==========+===============================+
+    |ciso8601                        |95 nsec    |96.8 nsec  |128 nsec   |123 nsec  |N/A                                               |…|125 nsec  |125 nsec  |140 nsec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |backports.datetime_fromisoformat|N/A        |N/A        |147 nsec   |149 nsec  |1.1x                                              |…|138 nsec  |149 nsec  |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |datetime (builtin)              |198 nsec   |207 nsec   |N/A        |N/A       |2.1x                                              |…|N/A       |N/A       |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |pendulum                        |N/A        |225 nsec   |214 nsec   |211 nsec  |2.3x                                              |…|219 nsec  |224 nsec  |13.5 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |udatetime                       |799 nsec   |803 nsec   |805 nsec   |830 nsec  |8.4x                                              |…|827 nsec  |805 nsec  |768 nsec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |str2date                        |7.73 usec  |6.75 usec  |7.78 usec  |7.8 usec  |81.4x                                             |…|7.74 usec |8.13 usec |❌                             |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |iso8601                         |13.7 usec  |11.3 usec  |12.7 usec  |12.5 usec |143.8x                                            |…|12.4 usec |12.6 usec |31.1 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |isodate                         |13.7 usec  |11.3 usec  |12.9 usec  |12.7 usec |144.0x                                            |…|12.7 usec |13.9 usec |46.7 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |iso8601utils                    |N/A        |N/A        |N/A        |21.4 usec |174.9x                                            |…|22.1 usec |23.4 usec |28.3 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |PySO8601                        |25.1 usec  |20.4 usec  |23.2 usec  |23.8 usec |263.8x                                            |…|23.5 usec |24.8 usec |25.3 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |zulu                            |26.3 usec  |21.4 usec  |25.7 usec  |24 usec   |277.2x                                            |…|24.5 usec |25.3 usec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |aniso8601                       |27.7 usec  |23.7 usec  |30.3 usec  |30 usec   |291.3x                                            |…|31.6 usec |33.8 usec |39.2 usec                      |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |maya                            |N/A        |36 usec    |41.3 usec  |41.8 usec |372.0x                                            |…|42.4 usec |42.7 usec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |python-dateutil                 |70.7 usec  |65.1 usec  |77.9 usec  |80.2 usec |744.0x                                            |…|79.4 usec |83.6 usec |100 usec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |arrow                           |73 usec    |62.8 usec  |74.5 usec  |73.9 usec |768.6x                                            |…|75.1 usec |80 usec   |148 usec                       |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |metomi-isodatetime              |1.22 msec  |1.25 msec  |1.72 msec  |1.72 msec |12876.3x                                          |…|1.76 msec |1.83 msec |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+    |moment                          |❌         |❌         |❌         |❌        |2305822.8x                                        |…|❌        |❌        |N/A                            |
+    +--------------------------------+-----------+-----------+-----------+----------+--------------------------------------------------+-+----------+----------+-------------------------------+
+
+ciso8601 takes 95 nsec, which is **2.1x faster than datetime (builtin)**, the next fastest Python 3.12 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:
+Tested on Linux 5.15.49-linuxkit using the following modules:
 
 .. code:: python
 
-  aniso8601==8.0.0
-  arrow==0.15.2
-  ciso8601==2.1.2
-  iso8601==0.1.12
+  aniso8601==9.0.1
+  arrow==1.3.0 (on Python 3.8, 3.9, 3.10, 3.11, 3.12), arrow==1.2.3 (on Python 3.7), arrow==0.17.0 (on Python 2.7)
+  backports.datetime_fromisoformat==2.0.1
+  ciso8601==2.3.0
+  iso8601==2.1.0 (on Python 3.8, 3.9, 3.10, 3.11, 3.12), iso8601==0.1.16 (on Python 2.7)
   iso8601utils==0.1.2
-  isodate==0.6.0
+  isodate==0.6.1
   maya==0.6.1
-  moment==0.8.2
-  pendulum==2.0.5
+  metomi-isodatetime==1!3.1.0
+  moment==0.12.1
+  pendulum==2.1.2
   PySO8601==0.2.0
-  python-dateutil==2.8.0
+  python-dateutil==2.8.2
   str2date==0.905
-  udatetime==0.0.16
-  zulu==1.1.1
+  udatetime==0.0.17
+  zulu==2.0.0
 
 .. </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
 
-Supported Subset of ISO 8601
+Supported subset of ISO 8601
 ----------------------------
 
-``ciso8601`` only supports the most common subset of ISO 8601.
+.. |datetime.fromisoformat| replace:: ``datetime.fromisoformat``
+.. _datetime.fromisoformat: https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
+
+``ciso8601`` only supports a subset of ISO 8601, but supports a superset of what is supported by Python itself (|datetime.fromisoformat|_), and supports the entirety of the `RFC 3339`_ specification.
 
-Date Formats
+Date formats
 ^^^^^^^^^^^^
 
 The following date formats are supported:
@@ -201,17 +219,18 @@ The following date formats are supported:
    ============================= ============== ==================
    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``   ❌
+   ``YYYY-MM-DD`` (extended)     ``2018-04-29`` ✅
+   ``YYYY-MM`` (extended)        ``2018-04``    ✅
+   ``YYYYMMDD`` (basic)          ``20180429``   ✅
+   ``YYYY-Www-D`` (week date)    ``2009-W01-1`` ✅
+   ``YYYY-Www`` (week date)      ``2009-W01``   ✅
+   ``YYYYWwwD`` (week date)      ``2009W011``   ✅
+   ``YYYYWww`` (week date)       ``2009W01``    ✅
+   ``YYYY-DDD`` (ordinal date)   ``1981-095``   ✅
+   ``YYYYDDD`` (ordinal date)    ``1981095``    ✅
    ============================= ============== ==================
 
-Week dates or ordinal dates are not currently supported.
+Uncommon ISO 8601 date formats are not supported:
 
 .. table::
    :widths: auto
@@ -219,20 +238,19 @@ Week dates or ordinal dates are not currently supported.
    ============================= ============== ==================
    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``    ❌
+   ``--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``   ❌
    ============================= ============== ==================
 
-Time Formats
+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``.
+Consistent with `RFC 3339`__, ``ciso8601`` 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
 
@@ -260,7 +278,7 @@ The following time formats are supported:
 
 **Note:** Python datetime objects only have microsecond precision (6 digits). Any additional precision will be truncated.
 
-Time Zone Information
+Time zone information
 ^^^^^^^^^^^^^^^^^^^^^
 
 Time zone information may be provided in one of the following formats:
@@ -280,9 +298,9 @@ Time zone information may be provided in one of the following formats:
 
 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``.
+Consistent with `RFC 3339`_, ``ciso8601`` also allows a lower-case ``z`` to be used instead of a ``Z``.
 
-Strict RFC 3339 Parsing
+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``:
@@ -294,8 +312,8 @@ Strict RFC 3339 Parsing
 * 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
--------------------------------------------
+Ignoring time zone 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.


=====================================
RELEASING.md
=====================================
@@ -0,0 +1,22 @@
+# Releasing ciso8601 <!-- omit in toc -->
+
+The document will describe the process of releasing a new version of `ciso8601`
+
+<!-- Generated with "Markdown All in One" extension for Visual Studio Code -->
+- [Prerequisites](#prerequisites)
+- [Create the release in GitHub](#create-the-release-in-github)
+
+## Prerequisites
+
+* Confirm that [`VERSION`](setup.py) has been changed
+* Confirm that [`CHANGELOG`](CHANGELOG.md) includes an entry for the version
+* Confirm that these changes have been merged into the `master`.
+## Create the release in GitHub
+
+1. Go to https://github.com/closeio/ciso8601/releases/new and draft a new release at the tag you created for the release.
+2. Each release is tagged with a Git tag. In the `Choose a Tag` field type a new tag. The tag must follow the format `v<version>` (i.e., the version with a `v` in front) (e.g., `v2.2.0`).
+3. In the `Release Title` field, type the same tag name (e.g., `v2.2.0`)
+4. In the `Describe this release` field copy-paste the `CHANGELOG.md` notes for this release.
+5. Click `Publish Release`
+
+This will trigger a [GitHub Action](.github/workflows/build-wheels.yml) that listens for new tags that follow our format, builds the wheels for the release, and publishes them to PyPI.
\ No newline at end of file


=====================================
benchmarking/Dockerfile
=====================================
@@ -5,28 +5,32 @@ RUN apt-get update && \
     add-apt-repository ppa:deadsnakes/ppa && \
     apt-get update
 
+# Install the other dependencies
+RUN apt-get install -y git curl gcc build-essential
+
+# Install tzdata non-iteractively
+# https://stackoverflow.com/questions/44331836/apt-get-install-tzdata-noninteractive/44333806#44333806
+RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata
+
 # Install the Python versions
-RUN apt install -y python python-dev && \
-    apt install -y python3.5 python3.5-dev python3.5-venv && \
-    apt install -y python3.6 python3.6-dev python3.6-venv && \
+RUN apt install -y python2 python2-dev && \
     apt install -y python3.7 python3.7-dev python3.7-venv && \
     apt install -y python3.8 python3.8-dev python3.8-venv && \
-    apt install -y python3.9 python3.9-dev python3.9-venv
-
-# Install the other dependencies
-RUN apt-get install -y git curl gcc build-essential
+    apt install -y python3.9 python3.9-dev python3.9-venv && \
+    apt install -y python3.10 python3.10-dev python3.10-venv && \
+    apt install -y python3.11 python3.11-dev python3.11-venv && \
+    apt install -y python3.12 python3.12-dev python3.12-venv
 
-# Make Python 3.9 the default `python`
-RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.9 10
+# Make Python 3.12 the default `python`
+RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.12 10
 
 # Get pip
-RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
-    python get-pip.py
+RUN python -m ensurepip --upgrade
 
 ADD requirements.txt requirements.txt
 
 # Install benchmarking dependencies
-RUN pip install -r requirements.txt
+RUN python -m pip install -r requirements.txt
 
 # Work around https://bugs.launchpad.net/ubuntu/+source/tzdata/+bug/1899343, which messes with `moment`
 RUN echo "Etc/UTC" | tee /etc/timezone && \


=====================================
benchmarking/README.rst
=====================================
@@ -7,9 +7,7 @@ Benchmarking ciso8601
 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
+``ciso8601``'s goal is to be the world's fastest ISO 8601 datetime parser for Python.
 
 In order to see how we compare, we run benchmarks against each other known ISO 8601 parser.
 


=====================================
benchmarking/format_results.py
=====================================
@@ -5,6 +5,7 @@ import platform
 import re
 
 from collections import defaultdict, UserDict
+from packaging import version as version_parse
 
 import pytablewriter
 
@@ -22,7 +23,7 @@ class Result:
         if self.exception:
             return f"Raised  ``{self.exception}`` Exception"
         elif not self.matched_expected:
-            return f"**Incorrect Result** (``{self.parsed_value}``)"
+            return "❌"
         else:
             return self.formatted_timing()
 
@@ -32,10 +33,10 @@ class ModuleResults(UserDict):
         non_exception_results = [(_python_version, result) for _python_version, result in self.data.items() if result.exception is None]
         return sorted(non_exception_results, key=lambda kvp: kvp[0], reverse=True)[0][1]
 
-FILENAME_REGEX_RAW = r"benchmark_timings_python(\d)(\d).csv"
+FILENAME_REGEX_RAW = r"benchmark_timings_python(\d)(\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_RAW = r"module_versions_python(\d)(\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}
@@ -64,9 +65,11 @@ def format_used_module_versions(module_versions_used):
         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()]))
+            results.append(", ".join([f"{module}=={version} (on Python {', '.join(version_sort(py_versions))})" for version, py_versions in versions.items()]))
     return results
 
+def version_sort(versions):
+    return [str(v) for v in sorted([version_parse.parse(v) for v in versions])]
 
 def relative_slowdown(subject, comparison):
     most_modern_common_version = next(iter(sorted(set(subject.keys()).intersection(set(comparison)), reverse=True)), None)
@@ -92,8 +95,12 @@ def load_benchmarking_results(results_directory):
             with open(csv_file, "r") as fin:
                 reader = csv.reader(fin, delimiter=",", quotechar='"')
                 major, minor, timestamp = next(reader)
+                major = int(major)
+                minor = int(minor)
                 timestamps.add(timestamp)
                 for module, _setup, stmt, parse_result, count, time_taken, matched, exception in reader:
+                    if module == "hardcoded":
+                        continue
                     timing = float(time_taken) / int(count) if exception == "" else None
                     exception = exception if exception != "" else None
                     results[module][(major, minor)] = Result(
@@ -114,22 +121,28 @@ def load_benchmarking_results(results_directory):
     python_versions_by_modernity = sorted(python_versions, reverse=True)
     return results, python_versions_by_modernity, calling_code
 
+SPACER_COLUMN = ["…"]
 
 def write_benchmarking_results(results_directory, output_file, baseline_module, include_call):
     results, python_versions_by_modernity, calling_code = load_benchmarking_results(results_directory)
     modules_by_modern_speed = [module for module, results in sorted([*results.items()], key=lambda kvp: kvp[1].most_modern_result().timing)]
 
+    # GitHub in desktop browsers displays 830 pixels in a table width before adding a scroll bar.
+    # Experimentally, this means we can show the results from the 4 latest versions of Python and our important slowdown summary before it cuts off
+    # We add a spacer column before continuing with older versions of Python so that the slowdown summary isn't lost in the noise.
+    modern_versions_before_slowdown_summary = 4
+
     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 {baseline_module}, latest Python)"]
-    writer.type_hint_list = [pytablewriter.String] * len(writer.header_list)
+    formatted_python_versions = [f"Python {major}.{minor}" for major, minor in python_versions_by_modernity]
+    writer.headers = ["Module"] + (["Call"] if include_call else []) + formatted_python_versions[0:modern_versions_before_slowdown_summary] + [f"Relative slowdown (versus {baseline_module}, latest Python)"] + SPACER_COLUMN + formatted_python_versions[modern_versions_before_slowdown_summary:]
+    writer.type_hints = [pytablewriter.String] * len(writer.headers)
 
     calling_codes = [calling_code[module] for module in modules_by_modern_speed]
     performance_results = [[results[module].get(python_version, NOT_APPLICABLE) for python_version in python_versions_by_modernity] for module in modules_by_modern_speed]
     relative_slowdowns = [relative_slowdown(results[module], results[baseline_module]) if module != baseline_module 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)
+        [module] + ([calling_code[module]] if include_call else []) + performance_by_version[0:modern_versions_before_slowdown_summary] + [relative_slowdown] + SPACER_COLUMN + performance_by_version[modern_versions_before_slowdown_summary:] 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:
@@ -137,15 +150,16 @@ def write_benchmarking_results(results_directory, output_file, baseline_module,
         writer.write_table()
         fout.write("\n")
 
-        if len(modules_by_modern_speed) > 1:
+        latest_python_version = python_versions_by_modernity[0]
+        modules_supporting_latest_python = [module for module in modules_by_modern_speed if latest_python_version in results[module]]
+        if len(modules_supporting_latest_python) > 1:
             baseline_module_timing = results[baseline_module].most_modern_result().formatted_timing()
 
-            fastest_module, next_fastest_module = modules_by_modern_speed[0:2]
+            fastest_module, next_fastest_module = modules_supporting_latest_python[0:2]
             if fastest_module == baseline_module:
-                fout.write(f"{baseline_module} takes {baseline_module_timing}, which is **{relative_slowdown(results[next_fastest_module], results[baseline_module])} faster than {next_fastest_module}**, the next fastest ISO 8601 parser in this comparison.\n")
+                fout.write(f"{baseline_module} takes {baseline_module_timing}, which is **{relative_slowdown(results[next_fastest_module], results[baseline_module])} faster than {next_fastest_module}**, the next fastest {formatted_python_versions[0]} parser in this comparison.\n")
             else:
-                fout.write(f"{baseline_module} takes {baseline_module_timing}, which is **{relative_slowdown(results[baseline_module], results[fastest_module])} slower than {fastest_module}**, the fastest ISO 8601 parser in this comparison.\n")
-
+                fout.write(f"{baseline_module} takes {baseline_module_timing}, which is **{relative_slowdown(results[baseline_module], results[fastest_module])} slower than {fastest_module}**, the fastest {formatted_python_versions[0]} parser in this comparison.\n")
 
 def load_module_version_info(results_directory):
     module_versions_used = defaultdict(dict)


=====================================
benchmarking/perform_comparison.py
=====================================
@@ -4,10 +4,12 @@ import os
 import sys
 import timeit
 
-from datetime import datetime
+from datetime import datetime, timedelta
 
 import pytz
 
+if (sys.version_info.major, sys.version_info.minor) >= (3, 5):
+    from metomi.isodatetime.data import TimePoint
 
 try:
     from importlib.metadata import version as get_module_version
@@ -17,32 +19,58 @@ except ImportError:
 ISO_8601_MODULES = {
     "aniso8601": ("import aniso8601", "aniso8601.parse_datetime('{timestamp}')"),
     "ciso8601": ("import ciso8601", "ciso8601.parse_datetime('{timestamp}')"),
+    "hardcoded": ("import ciso8601", "ciso8601._hard_coded_benchmark_timestamp()"),
     "python-dateutil": ("import dateutil.parser", "dateutil.parser.parse('{timestamp}')"),
     "iso8601": ("import iso8601", "iso8601.parse_date('{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" and (sys.version_info.major, sys.version_info.minor) < (3, 9):
+if (sys.version_info.major, sys.version_info.minor) >= (3, 11):
+    # Python 3.11 added full ISO 8601 parsing
+    ISO_8601_MODULES["datetime (builtin)"] = ("from datetime import datetime", "datetime.fromisoformat('{timestamp}')")
+
+if sys.version_info.major >= 3 and (sys.version_info.major, sys.version_info.minor) < (3, 11):
+    # backports.datetime_fromisoformat brings the Python 3.11 logic into older Python 3 versions
+    ISO_8601_MODULES["backports.datetime_fromisoformat"] = ("from backports.datetime_fromisoformat import datetime_fromisoformat", "datetime_fromisoformat('{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, sys.version_info.minor) >= (3, 5):
+    # metomi-isodatetime doesn't support Python < 3.5
+    ISO_8601_MODULES["metomi-isodatetime"] = ("import metomi.isodatetime.parsers as parse", "parse.TimePointParser().parse('{timestamp}')")
+
 if (sys.version_info.major, sys.version_info.minor) >= (3, 6):
     # zulu v2.0.0+ no longer supports Python < 3.6
     ISO_8601_MODULES["zulu"] = ("import zulu", "zulu.parse('{timestamp}')")
 
-if (sys.version_info.major, sys.version_info.minor) != (3, 6):
+if (sys.version_info.major, sys.version_info.minor) != (3, 6) and (sys.version_info.major, sys.version_info.minor) <= (3, 9):
     # iso8601utils installs enum34, which messes with tox in Python 3.6
     # https://stackoverflow.com/q/43124775
+    # https://github.com/silverfernsys/iso8601utils/pull/5
+    # iso8601utils uses `from collections import Iterable` which no longer works in Python 3.10
+    # https://github.com/silverfernsys/iso8601utils/issues/6
     ISO_8601_MODULES["iso8601utils"] = ("from iso8601utils import parsers", "parsers.datetime('{timestamp}')")
 
 if (sys.version_info.major, sys.version_info.minor) != (3, 4):
-    # arrow no longer supports Python 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
+
+if sys.version_info.major >= 3 and (sys.version_info.major, sys.version_info.minor) < (3, 12):
+    # `maya` uses a version of `regex` which no longer supports Python 2
+    # `maya` uses `pendulum`, which doesn't yet support Python 3.12
+    ISO_8601_MODULES["maya"] = ("import maya", "maya.parse('{timestamp}').datetime()")
+
+if (sys.version_info.major, sys.version_info.minor) < (3, 12):
+    # `pendulum` doesn't yet support Python 3.12
+    ISO_8601_MODULES["pendulum"] = ("from pendulum.parsing import parse_iso8601", "parse_iso8601('{timestamp}')")
+
+if (sys.version_info.major, sys.version_info.minor) >= (3, 5):
+    # `moment` is built on `times`, which is built on `arrow`, which no longer supports Python 3.4
+    # `moment` uses a version of `regex` which no longer supports Python 2
     ISO_8601_MODULES["moment"] = ("import moment", "moment.date('{timestamp}').date")
 
 class Result:
@@ -68,11 +96,29 @@ class Result:
             self.exception
         ]
 
+def metomi_compare(timepoint, dt):
+    # Really (s)crappy comparison function
+    # Ignores subsecond accuracy.
+    # https://github.com/metomi/isodatetime/issues/196
+    offset = timedelta(hours=timepoint.time_zone.hours, minutes=timepoint.time_zone.minutes)
+    return timepoint.year == dt.year and \
+        timepoint.month_of_year == dt.month and \
+        timepoint.day_of_month == dt.day and \
+        timepoint.hour_of_day == dt.hour and \
+        timepoint.minute_of_hour == dt.minute and \
+        timepoint.second_of_minute == dt.second and \
+        offset == dt.tzinfo.utcoffset(dt)
+
 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
+
+    # Special handling for metomi-isodatetime
+    if (sys.version_info.major, sys.version_info.minor) >= (3, 5) and isinstance(dt1, TimePoint):
+        return metomi_compare(dt1, dt2)
+
     return dt1 == dt2
 
 def auto_range_counts(filepath):
@@ -105,7 +151,15 @@ def write_module_versions(filepath):
         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 module == "datetime (builtin)" or module == "hardcoded":
+                continue
+            # Unfortunately, `backports.datetime_fromisoformat` has the distribution name `backports-datetime-fromisoformat` in PyPI
+            # This messes with Python 3.8 and 3.9's get_module_version, so we special case it.
+            if module == "backports.datetime_fromisoformat":
+                module_version = get_module_version("backports-datetime-fromisoformat")
+            else:
+                module_version = get_module_version(module)
+            module_version_writer.writerow([module, module_version])
 
 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,


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


=====================================
benchmarking/run_benchmarks.sh
=====================================
@@ -1,5 +1,5 @@
-tox '2014-01-09T21:48:00'
-tox '2014-01-09T21:48:00-05:30'
+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


=====================================
benchmarking/tox.ini
=====================================
@@ -1,8 +1,11 @@
 [tox]
-envlist = py39,py38,py37,py36,py35,py34,py27
+requires =
+    tox>=4
+envlist = py312,py311,py310,py39,py38,py37
 setupdir=..
 
 [testenv]
+package = sdist
 setenv =
     CISO8601_CACHING_ENABLED = 1
 deps=
@@ -13,21 +16,29 @@ deps=
     aniso8601
     ; `arrow` no longer supports Python 3.4
     arrow; python_version != '3.4'
+    backports.datetime_fromisoformat; python_version > '3' and python_version < '3.11'
     iso8601
-    ; `iso8601utils` installs `enum34`, which messes with tox in Python 3.6
-    ; https://stackoverflow.com/q/43124775
-    iso8601utils; python_version !=  '3.6'
+    # iso8601utils installs enum34, which messes with tox in Python 3.6
+    # https://stackoverflow.com/q/43124775
+    # https://github.com/silverfernsys/iso8601utils/pull/5
+    # iso8601utils uses `from collections import Iterable` which no longer works in Python 3.10
+    # https://github.com/silverfernsys/iso8601utils/issues/6
+    iso8601utils; python_version != '3.6' and python_version != '3.10'
     isodate
-    maya
+    ; `maya` uses a version of `regex` which no longer supports Python 2
+    ; `maya` uses `pendulum`, which doesn't yet support Python 3.12
+    maya; python_version > '3' and python_version < '3.12'
+    metomi-isodatetime; python_version >= '3.5'
     ; `moment` is built on `times`, which is built on `arrow`, which no longer supports Python 3.4
-    moment; python_version != '3.4'
-    pendulum
+    ; `moment` uses a version of `regex` which no longer supports Python 2
+    moment; python_version >= '3.5'
+    ; `pendulum` doesn't yet support Python 3.12
+    pendulum; python_version < '3.12'
     pyso8601
     python-dateutil
     str2date
     ; `udatetime` doesn't support Windows
-    ; `udatetime` doesn't compile on Python 3.9 (https://github.com/freach/udatetime/issues/32)
-    udatetime; os_name != 'nt' and python_version < '3.9'
+    udatetime; os_name != 'nt'
     ; `zulu` v2.0.0+ no longer supports Python < 3.6
     zulu; python_version >= '3.6'
     pytz


=====================================
generate_test_timestamps.py
=====================================
@@ -1,5 +1,6 @@
 import datetime
 import pytz
+import sys
 
 from collections import namedtuple
 
@@ -22,6 +23,9 @@ NUMBER_FIELDS = {
     "year": NumberField(4, 4, 1, 9999),
     "month": NumberField(2, 2, 1, 12),
     "day": NumberField(2, 2, 1, 31),
+    "ordinal_day": NumberField(3, 3, 1, 365), # Intentionally missing leap year case
+    "iso_week":  NumberField(2, 2, 1, 53),
+    "iso_day":  NumberField(1, 1, 1, 7),
     "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
@@ -39,7 +43,7 @@ PADDED_NUMBER_FIELD_FORMATS = {
 }
 
 
-def __generate_valid_formats(year=2014, month=2, day=3, hour=1, minute=23, second=45, microsecond=123456, tzhour=4, tzminute=30):
+def __generate_valid_formats(year=2014, month=2, day=3, iso_week=6, iso_day=1, ordinal_day=34, 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
@@ -53,6 +57,24 @@ def __generate_valid_formats(year=2014, month=2, day=3, hour=1, minute=23, secon
         ("{year}-{month}-{day}", set(["year", "month", "day"]), {"year": year, "month": month, "day": day}),
     ]
 
+    valid_basic_week_date_formats = [
+        ("{year}W{iso_week}", set(["year", "iso_week"]), {"year": year, "iso_week": iso_week, "iso_day": 1}),
+        ("{year}W{iso_week}{iso_day}", set(["year", "iso_week", "iso_day"]), {"year": year, "iso_week": iso_week, "iso_day": iso_day})
+    ]
+
+    valid_extended_week_date_formats = [
+        ("{year}-W{iso_week}", set(["year", "iso_week"]), {"year": year, "iso_week": iso_week, "iso_day": 1}),
+        ("{year}-W{iso_week}-{iso_day}", set(["year", "iso_week", "iso_day"]), {"year": year, "iso_week": iso_week, "iso_day": iso_day})
+    ]
+
+    valid_basic_ordinal_date_formats = [
+        ("{year}{ordinal_day}", set(["year", "ordinal_day"]), {"year": year, "ordinal_day": ordinal_day}),
+    ]
+
+    valid_extended_ordinal_date_formats = [
+        ("{year}-{ordinal_day}", set(["year", "ordinal_day"]), {"year": year, "ordinal_day": ordinal_day}),
+    ]
+
     valid_date_and_time_separators = [None, "T", "t", " "]
 
     valid_basic_time_formats = [
@@ -85,8 +107,35 @@ def __generate_valid_formats(year=2014, month=2, day=3, hour=1, minute=23, secon
         ("+{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)]:
+    format_date_time_combinations = [
+        (valid_basic_calendar_date_formats, valid_basic_time_formats),
+        (valid_extended_calendar_date_formats, valid_extended_time_formats),
+        (valid_basic_ordinal_date_formats, valid_basic_time_formats),
+        (valid_extended_ordinal_date_formats, valid_extended_time_formats),
+    ]
+
+    if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
+        # We rely on datetime.datetime.fromisocalendar
+        # to generate the expected values, but that was added in Python 3.8
+        format_date_time_combinations += [
+            (valid_basic_week_date_formats, valid_basic_time_formats),
+            (valid_extended_week_date_formats, valid_extended_time_formats)
+        ]
+
+    for valid_calendar_date_formats, valid_time_formats in format_date_time_combinations:
         for calendar_format, calendar_fields, calendar_params in valid_calendar_date_formats:
+
+            if "iso_week" in calendar_fields:
+                dt = datetime.datetime.fromisocalendar(calendar_params["year"], calendar_params["iso_week"], calendar_params["iso_day"])
+                calendar_params = __merge_dicts(calendar_params, { "month": dt.month, "day": dt.day })
+                del(calendar_params["iso_week"])
+                del(calendar_params["iso_day"])
+
+            if "ordinal_day" in calendar_fields:
+                dt = datetime.datetime(calendar_params["year"], 1, 1) + (datetime.timedelta(days=(calendar_params["ordinal_day"] - 1)))
+                calendar_params = __merge_dicts(calendar_params, { "month": dt.month, "day": dt.day })
+                del(calendar_params["ordinal_day"])
+
             for date_and_time_separator in valid_date_and_time_separators:
                 if date_and_time_separator is None:
                     full_format = calendar_format
@@ -117,17 +166,20 @@ def __pad_params(**kwargs):
     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):
+def generate_valid_timestamp_and_datetime(year=2014, month=2, day=3, iso_week=6, iso_day=1, ordinal_day=34, 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).
+    # Note that this will produce many test cases that exercise the exact same code pathways (i.e., offer no additional coverage).
+    # Given a knowledge of the code, this is excessive, but these serve as a good set of black box tests (i.e., You could apply these to any ISO 8601 parse).
 
     kwargs = {
         "year": year,
         "month": month,
         "day": day,
+        "iso_week": iso_week,
+        "iso_day": iso_day,
+        "ordinal_day": ordinal_day,
         "hour": hour,
         "minute": minute,
         "second": second,
@@ -142,7 +194,7 @@ def generate_valid_timestamp_and_datetime(year=2014, month=2, day=3, hour=1, min
         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):
+def generate_invalid_timestamp(year=2014, month=2, day=3, iso_week=6, iso_day=1, ordinal_day=34, 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
@@ -153,13 +205,14 @@ def generate_invalid_timestamp(year=2014, month=2, day=3, hour=1, minute=23, sec
     # 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).
+    # Note that this will produce many test cases that exercise the exact same code pathways (i.e., offer no additional coverage).
+    # Given a knowledge of the code, this is excessive, but these serve as a good set of black box tests (i.e., 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")
+    #   - Ordinal dates in leap years
     #   - 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
@@ -169,6 +222,9 @@ def generate_invalid_timestamp(year=2014, month=2, day=3, hour=1, minute=23, sec
         "year": year,
         "month": month,
         "day": day,
+        "iso_week": iso_week,
+        "iso_day": iso_day,
+        "ordinal_day": ordinal_day,
         "hour": hour,
         "minute": minute,
         "second": second,
@@ -184,28 +240,48 @@ def generate_invalid_timestamp(year=2014, month=2, day=3, hour=1, minute=23, sec
             if field is not None:
                 # Too few characters
                 for length in range(1, field.min_width):
+                    if timestamp_format.startswith("{year}W{iso_week}{iso_day}") and field_name == "iso_week":
+                        # If you reduce the iso_week field to 1 character, then the iso_day will make it into
+                        # a valid "{year}W{iso_week}" timestamp
+                        continue
+                    if timestamp_format.startswith("{year}{month}{day}") and (field_name == "month" or field_name == "day"):
+                        # If you reduce the month or day field to 1 character, then it will make it into
+                        # a valid "{year}{ordinal_day}" timestamp
+                        continue
+                    if timestamp_format.startswith("{year}{month}{day}") and field_name == "year" and length == 3:
+                        # If you reduce the year field to 3 characters, then it will make it into
+                        # a valid "{year}{ordinal_day}" timestamp
+                        continue
+                    if timestamp_format.startswith("{year}-{ordinal_day}") and field_name == "ordinal_day" and length == 2:
+                        # If you reduce the ordinal_day field to 2 characters, then it will make it into
+                        # a valid "{year}-{month}" timestamp
+                        continue
                     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
+                    yield (timestamp, "{0} has too few characters".format(field_name))
 
                 # Too many characters
                 if field.max_width is not None:
+                    if timestamp_format.startswith("{year}-{month}") and field_name == "month":
+                        # If you extend the month field to 3 characters, then it will make it into
+                        # a valid "{year}{ordinal_day}" timestamp
+                        continue
                     mangled_kwargs[field_name] = "{{:0>{length}}}".format(length=field.max_width + 1).format(kwargs[field_name])
                     timestamp = timestamp_format.format(**mangled_kwargs)
-                    yield timestamp
+                    yield (timestamp, "{0} has too many characters".format(field_name))
 
                 # 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
+                    yield (timestamp, "{0} has too small value".format(field_name))
 
                 # 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
+                    yield (timestamp, "{0} has too large value".format(field_name))
 
                 # Invalid characters
                 max_invalid_characters = field.max_width if field.max_width is not None else 1
@@ -213,14 +289,14 @@ def generate_invalid_timestamp(year=2014, month=2, day=3, hour=1, minute=23, sec
                 for length in range(1, max_invalid_characters):
                     mangled_kwargs[field_name] = "a" * length
                     timestamp = timestamp_format.format(**mangled_kwargs)
-                    yield timestamp
+                    yield (timestamp, "{0} has invalid characters".format(field_name))
                 # 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
+                    yield (timestamp, "{0} has invalid characters".format(field_name))
 
         # Trailing characters
         timestamp = timestamp_format.format(**__pad_params(**kwargs)) + "EXTRA"
-        yield timestamp
+        yield (timestamp, "{0} has extra characters".format(field_name))


=====================================
isocalendar.c
=====================================
@@ -0,0 +1,327 @@
+/* This file was originally taken from cPython's code base
+ * (`Modules/_datetimemodule.c`) at commit
+ * 27d8dc2c9d3de886a884f79f0621d4586c0e0f7a
+ *
+ * Below is a copy of the Python 3.11 code license
+ * (from https://docs.python.org/3/license.html):
+ *
+ * PSF LICENSE AGREEMENT FOR PYTHON 3.11.0
+ *
+ * 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"),
+ *    and the Individual or Organization ("Licensee") accessing and otherwise
+ *    using Python 3.11.0 software in source or binary form and its associated
+ *    documentation.
+ *
+ * 2. Subject to the terms and conditions of this License Agreement, PSF hereby
+ *    grants Licensee a nonexclusive, royalty-free, world-wide license to
+ *    reproduce, analyze, test, perform and/or display publicly, prepare
+ *    derivative works, distribute, and otherwise use Python 3.11.0 alone or in
+ *    any derivative version, provided, however, that PSF's License Agreement
+ *    and PSF's notice of copyright, i.e., "Copyright © 2001-2022 Python
+ *    Software Foundation; All Rights Reserved" are retained in Python 3.11.0
+ *    alone or in any derivative version prepared by Licensee.
+ *
+ * 3. In the event Licensee prepares a derivative work that is based on or
+ *    incorporates Python 3.11.0 or any part thereof, and wants to make the
+ *    derivative work available to others as provided herein, then Licensee
+ *    hereby agrees to include in any such work a brief summary of the changes
+ *    made to Python 3.11.0.
+ *
+ * 4. PSF is making Python 3.11.0 available to Licensee on an "AS IS" basis.
+ *    PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED.  BY WAY
+ *    OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY
+ *    REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY
+ *    PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 3.11.0 WILL NOT INFRINGE ANY
+ *    THIRD PARTY RIGHTS.
+ *
+ * 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.11.0
+ *    FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT
+ *    OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.11.0, OR ANY
+ *    DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+ *
+ * 6. This License Agreement will automatically terminate upon a material
+ *    breach of its terms and conditions.
+ *
+ * 7. Nothing in this License Agreement shall be deemed to create any
+ *    relationship of agency, partnership, or joint venture between PSF and
+ *    Licensee.  This License Agreement does not grant permission to use PSF
+ *    trademarks or trade name in a trademark sense to endorse or promote
+ *    products or services of Licensee, or any third party.
+ *
+ * 8. By copying, installing or otherwise using Python 3.11.0, Licensee agrees
+ *    to be bound by the terms and conditions of this License Agreement.
+ */
+
+#include "isocalendar.h"
+
+#include "Python.h"
+
+/* ---------------------------------------------------------------------------
+ * General calendrical helper functions
+ */
+
+/* For each month ordinal in 1..12, the number of days in that month,
+ * and the number of days before that month in the same year.  These
+ * are correct for non-leap years only.
+ */
+static const int _days_in_month[] = {
+    0, /* unused; this vector uses 1-based indexing */
+    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
+};
+
+static const int _days_before_month[] = {
+    0, /* unused; this vector uses 1-based indexing */
+    0,  31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
+    365  // Useful for month + 1 accesses for December
+};
+
+/* year -> 1 if leap year, else 0. */
+static int
+is_leap(int year)
+{
+    /* Cast year to unsigned.  The result is the same either way, but
+     * C can generate faster code for unsigned mod than for signed
+     * mod (especially for % 4 -- a good compiler should just grab
+     * the last 2 bits when the LHS is unsigned).
+     */
+    const unsigned int ayear = (unsigned int)year;
+    return ayear % 4 == 0 && (ayear % 100 != 0 || ayear % 400 == 0);
+}
+
+/* year, month -> number of days in that month in that year */
+static int
+days_in_month(int year, int month)
+{
+    assert(month >= 1);
+    assert(month <= 12);
+    if (month == 2 && is_leap(year))
+        return 29;
+    else
+        return _days_in_month[month];
+}
+
+/* year, month -> number of days in year preceding first day of month */
+static int
+days_before_month(int year, int month)
+{
+    int days;
+
+    assert(month >= 1);
+    assert(month <= 12);
+    days = _days_before_month[month];
+    if (month > 2 && is_leap(year))
+        ++days;
+    return days;
+}
+
+/* year -> number of days before January 1st of year.  Remember that we
+ * start with year 1, so days_before_year(1) == 0.
+ */
+static int
+days_before_year(int year)
+{
+    int y = year - 1;
+    /* This is incorrect if year <= 0; we really want the floor
+     * here.  But so long as MINYEAR is 1, the smallest year this
+     * can see is 1.
+     */
+    assert(year >= 1);
+    return y * 365 + y / 4 - y / 100 + y / 400;
+}
+
+/* Number of days in 4, 100, and 400 year cycles.  That these have
+ * the correct values is asserted in the module init function.
+ */
+#define DI4Y   1461   /* days_before_year(5); days in 4 years */
+#define DI100Y 36524  /* days_before_year(101); days in 100 years */
+#define DI400Y 146097 /* days_before_year(401); days in 400 years  */
+
+/* ordinal -> year, month, day, considering 01-Jan-0001 as day 1. */
+static void
+ord_to_ymd(int ordinal, int *year, int *month, int *day)
+{
+    int n, n1, n4, n100, n400, leapyear, preceding;
+
+    /* ordinal is a 1-based index, starting at 1-Jan-1.  The pattern of
+     * leap years repeats exactly every 400 years.  The basic strategy is
+     * to find the closest 400-year boundary at or before ordinal, then
+     * work with the offset from that boundary to ordinal.  Life is much
+     * clearer if we subtract 1 from ordinal first -- then the values
+     * of ordinal at 400-year boundaries are exactly those divisible
+     * by DI400Y:
+     *
+     *    D  M   Y            n              n-1
+     *    -- --- ----        ----------     ----------------
+     *    31 Dec -400        -DI400Y       -DI400Y -1
+     *     1 Jan -399         -DI400Y +1   -DI400Y      400-year boundary
+     *    ...
+     *    30 Dec  000        -1             -2
+     *    31 Dec  000         0             -1
+     *     1 Jan  001         1              0          400-year boundary
+     *     2 Jan  001         2              1
+     *     3 Jan  001         3              2
+     *    ...
+     *    31 Dec  400         DI400Y        DI400Y -1
+     *     1 Jan  401         DI400Y +1     DI400Y      400-year boundary
+     */
+    assert(ordinal >= 1);
+    --ordinal;
+    n400 = ordinal / DI400Y;
+    n = ordinal % DI400Y;
+    *year = n400 * 400 + 1;
+
+    /* Now n is the (non-negative) offset, in days, from January 1 of
+     * year, to the desired date.  Now compute how many 100-year cycles
+     * precede n.
+     * Note that it's possible for n100 to equal 4!  In that case 4 full
+     * 100-year cycles precede the desired day, which implies the
+     * desired day is December 31 at the end of a 400-year cycle.
+     */
+    n100 = n / DI100Y;
+    n = n % DI100Y;
+
+    /* Now compute how many 4-year cycles precede it. */
+    n4 = n / DI4Y;
+    n = n % DI4Y;
+
+    /* And now how many single years.  Again n1 can be 4, and again
+     * meaning that the desired day is December 31 at the end of the
+     * 4-year cycle.
+     */
+    n1 = n / 365;
+    n = n % 365;
+
+    *year += n100 * 100 + n4 * 4 + n1;
+    if (n1 == 4 || n100 == 4) {
+        assert(n == 0);
+        *year -= 1;
+        *month = 12;
+        *day = 31;
+        return;
+    }
+
+    /* Now the year is correct, and n is the offset from January 1.  We
+     * find the month via an estimate that's either exact or one too
+     * large.
+     */
+    leapyear = n1 == 3 && (n4 != 24 || n100 == 3);
+    assert(leapyear == is_leap(*year));
+    *month = (n + 50) >> 5;
+    preceding = (_days_before_month[*month] + (*month > 2 && leapyear));
+    if (preceding > n) {
+        /* estimate is too large */
+        *month -= 1;
+        preceding -= days_in_month(*year, *month);
+    }
+    n -= preceding;
+    assert(0 <= n);
+    assert(n < days_in_month(*year, *month));
+
+    *day = n + 1;
+}
+
+/* year, month, day -> ordinal, considering 01-Jan-0001 as day 1. */
+static int
+ymd_to_ord(int year, int month, int day)
+{
+    return days_before_year(year) + days_before_month(year, month) + day;
+}
+
+/* Day of week, where Monday==0, ..., Sunday==6.  1/1/1 was a Monday. */
+static int
+weekday(int year, int month, int day)
+{
+    return (ymd_to_ord(year, month, day) + 6) % 7;
+}
+
+/* Ordinal of the Monday starting week 1 of the ISO year.  Week 1 is the
+ * first calendar week containing a Thursday.
+ */
+static int
+iso_week1_monday(int year)
+{
+    int first_day = ymd_to_ord(year, 1, 1); /* ord of 1/1 */
+    /* 0 if 1/1 is a Monday, 1 if a Tue, etc. */
+    int first_weekday = (first_day + 6) % 7;
+    /* ordinal of closest Monday at or before 1/1 */
+    int week1_monday = first_day - first_weekday;
+
+    if (first_weekday > 3) /* if 1/1 was Fri, Sat, Sun */
+        week1_monday += 7;
+    return week1_monday;
+}
+
+int
+iso_to_ymd(const int iso_year, const int iso_week, const int iso_day,
+           int *year, int *month, int *day)
+{
+    if (iso_week <= 0 || iso_week >= 53) {
+        int out_of_range = 1;
+        if (iso_week == 53) {
+            // ISO years have 53 weeks in it on years starting with a Thursday
+            // and on leap years starting on Wednesday
+            int first_weekday = weekday(iso_year, 1, 1);
+            if (first_weekday == 3 ||
+                (first_weekday == 2 && is_leap(iso_year))) {
+                out_of_range = 0;
+            }
+        }
+
+        if (out_of_range) {
+            return -2;
+        }
+    }
+
+    if (iso_day <= 0 || iso_day >= 8) {
+        return -3;
+    }
+
+    // Convert (Y, W, D) to (Y, M, D) in-place
+    int day_1 = iso_week1_monday(iso_year);
+
+    int day_offset = (iso_week - 1) * 7 + iso_day - 1;
+
+    ord_to_ymd(day_1 + day_offset, year, month, day);
+    return 0;
+}
+
+int
+ordinal_to_ymd(const int iso_year, int ordinal_day, int *year, int *month,
+               int *day)
+{
+    if (ordinal_day < 1) {
+        return -1;
+    }
+
+    /* January */
+    if (ordinal_day <= _days_before_month[2]) {
+        *year = iso_year;
+        *month = 1;
+        *day = ordinal_day - _days_before_month[1];
+        return 0;
+    }
+
+    /* February */
+    if (ordinal_day <= (_days_before_month[3] + (is_leap(iso_year) ? 1 : 0))) {
+        *year = iso_year;
+        *month = 2;
+        *day = ordinal_day - _days_before_month[2];
+        return 0;
+    }
+
+    if (is_leap(iso_year)) {
+        ordinal_day -= 1;
+    }
+
+    /* March - December */
+    for (int i = 3; i <= 12; i++) {
+        if (ordinal_day <= _days_before_month[i + 1]) {
+            *year = iso_year;
+            *month = i;
+            *day = ordinal_day - _days_before_month[i];
+            return 0;
+        }
+    }
+
+    return -2;
+}


=====================================
isocalendar.h
=====================================
@@ -0,0 +1,12 @@
+#ifndef ISO_CALENDER_H
+#define ISO_CALENDER_H
+
+int
+iso_to_ymd(const int iso_year, const int iso_week, const int iso_day,
+           int *year, int *month, int *day);
+
+int
+ordinal_to_ymd(const int iso_year, const int ordinal_day, int *year,
+               int *month, int *day);
+
+#endif


=====================================
module.c
=====================================
@@ -1,6 +1,8 @@
 #include <Python.h>
 #include <ctype.h>
 #include <datetime.h>
+
+#include "isocalendar.h"
 #include "timezone.h"
 
 #define STRINGIZE(x)            #x
@@ -12,22 +14,27 @@
     ((PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 6) || PY_MAJOR_VERSION > 3)
 #define PY_VERSION_AT_LEAST_37 \
     ((PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 7) || PY_MAJOR_VERSION > 3)
-
-// PyPy compatibility for cPython 3.7's Timezone API was added to PyPy 7.3.6
-// https://foss.heptapod.net/pypy/pypy/-/merge_requests/826
+#define PY_VERSION_AT_LEAST_38 \
+    ((PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 8) || PY_MAJOR_VERSION > 3)
+
+/* PyPy compatibility for cPython 3.7's Timezone API was added to PyPy 7.3.6
+ * https://foss.heptapod.net/pypy/pypy/-/merge_requests/826
+ * But was then reverted in 7.3.7 for PyPy 3.7:
+ * https://foss.heptapod.net/pypy/pypy/-/commit/eeeafcf905afa0f26049ac29dc00f5b295171f99
+ * It is still present in 7.3.7 for PyPy 3.8+
+ */
 #ifdef PYPY_VERSION
-    #define SUPPORTS_37_TIMEZONE_API \
-            (PYPY_VERSION_NUM >= 0x07030600)
+#define SUPPORTS_37_TIMEZONE_API \
+    (PYPY_VERSION_NUM >= 0x07030600) && PY_VERSION_AT_LEAST_38
 #else
-    #define SUPPORTS_37_TIMEZONE_API \
-            PY_VERSION_AT_LEAST_37
+#define SUPPORTS_37_TIMEZONE_API PY_VERSION_AT_LEAST_37
 #endif
 
 static PyObject *utc;
 
 #if CISO8601_CACHING_ENABLED
 /* 2879 = (1439 * 2) + 1, number of offsets from UTC possible in
- * Python (ie. [-1439, 1439]).
+ * Python (i.e., [-1439, 1439]).
  *
  * 0 - 1438 = Negative offsets [-1439..-1]
  * 1439 = Zero offset
@@ -36,75 +43,77 @@ static PyObject *utc;
 static PyObject *tz_cache[2879] = {NULL};
 #endif
 
-#define PARSE_INTEGER(field, length, field_name)                       \
-    for (i = 0; i < length; i++) {                                     \
-        if (*c >= '0' && *c <= '9') {                                  \
-            field = 10 * field + *c++ - '0';                           \
-        }                                                              \
-        else {                                                         \
-            return format_unexpected_character_exception(              \
-                field_name, c, (c - str) / sizeof(char), length - i);  \
-        }                                                              \
+#define PARSE_INTEGER(field, length, field_name)                      \
+    for (i = 0; i < length; i++) {                                    \
+        if (*c >= '0' && *c <= '9') {                                 \
+            field = 10 * field + *c++ - '0';                          \
+        }                                                             \
+        else {                                                        \
+            return format_unexpected_character_exception(             \
+                field_name, c, (c - str) / sizeof(char), length - i); \
+        }                                                             \
     }
 
-#define PARSE_FRACTIONAL_SECOND()                              \
-    for (i = 0; i < 6; i++) {                                  \
-        if (*c >= '0' && *c <= '9') {                          \
-            usecond = 10 * usecond + *c++ - '0';               \
-        }                                                      \
-        else if (i == 0) {                                     \
-            /* We need at least one digit. */                  \
-            /* Trailing '.' or ',' is not allowed */           \
-            return format_unexpected_character_exception(      \
-                "subsecond", c, (c - str) / sizeof(char), 1);  \
-        }                                                      \
-        else                                                   \
-            break;                                             \
-    }                                                          \
-                                                               \
-    /* Omit excessive digits */                                \
-    while (*c >= '0' && *c <= '9') c++;                        \
-                                                               \
-    /* If we break early, fully expand the usecond */          \
+#define PARSE_FRACTIONAL_SECOND()                             \
+    for (i = 0; i < 6; i++) {                                 \
+        if (*c >= '0' && *c <= '9') {                         \
+            usecond = 10 * usecond + *c++ - '0';              \
+        }                                                     \
+        else if (i == 0) {                                    \
+            /* We need at least one digit. */                 \
+            /* Trailing '.' or ',' is not allowed */          \
+            return format_unexpected_character_exception(     \
+                "subsecond", c, (c - str) / sizeof(char), 1); \
+        }                                                     \
+        else                                                  \
+            break;                                            \
+    }                                                         \
+                                                              \
+    /* Omit excessive digits */                               \
+    while (*c >= '0' && *c <= '9') c++;                       \
+                                                              \
+    /* If we break early, fully expand the usecond */         \
     while (i++ < 6) usecond *= 10;
 
 #if PY_VERSION_AT_LEAST_33
-    #define PARSE_SEPARATOR(separator, field_name)                                \
-        if (separator) {                                                          \
-            c++;                                                                  \
-        }                                                                         \
-        else {                                                                    \
-            PyObject *unicode_str = PyUnicode_FromString(c);                      \
-            PyObject *unicode_char = PyUnicode_Substring(unicode_str, 0, 1);      \
-            PyErr_Format(PyExc_ValueError,                                        \
-                        "Invalid character while parsing %s ('%U', Index: %lu)",  \
-                        field_name, unicode_char, (c - str) / sizeof(char));      \
-            Py_DECREF(unicode_str);                                               \
-            Py_DECREF(unicode_char);                                              \
-            return NULL;                                                          \
-        }
+#define PARSE_SEPARATOR(separator, field_name)                                \
+    if (separator) {                                                          \
+        c++;                                                                  \
+    }                                                                         \
+    else {                                                                    \
+        PyObject *unicode_str = PyUnicode_FromString(c);                      \
+        PyObject *unicode_char = PyUnicode_Substring(unicode_str, 0, 1);      \
+        PyErr_Format(PyExc_ValueError,                                        \
+                     "Invalid character while parsing %s ('%U', Index: %lu)", \
+                     field_name, unicode_char, (c - str) / sizeof(char));     \
+        Py_DECREF(unicode_str);                                               \
+        Py_DECREF(unicode_char);                                              \
+        return NULL;                                                          \
+    }
 #else
-    #define PARSE_SEPARATOR(separator, field_name)                                \
-        if (separator) {                                                          \
-            c++;                                                                  \
-        }                                                                         \
-        else {                                                                    \
-            if (isascii((int) *c)) {                                              \
-                PyErr_Format(PyExc_ValueError,                                    \
-                        "Invalid character while parsing %s ('%c', Index: %lu)",  \
-                        field_name, *c, (c - str) / sizeof(char));                \
-            }                                                                     \
-            else {                                                                \
-                PyErr_Format(PyExc_ValueError,                                    \
-                        "Invalid character while parsing %s (Index: %lu)",        \
-                        field_name, (c - str) / sizeof(char));                    \
-            }                                                                     \
-            return NULL;                                                          \
-        }
+#define PARSE_SEPARATOR(separator, field_name)                              \
+    if (separator) {                                                        \
+        c++;                                                                \
+    }                                                                       \
+    else {                                                                  \
+        if (isascii((int)*c)) {                                             \
+            PyErr_Format(                                                   \
+                PyExc_ValueError,                                           \
+                "Invalid character while parsing %s ('%c', Index: %lu)",    \
+                field_name, *c, (c - str) / sizeof(char));                  \
+        }                                                                   \
+        else {                                                              \
+            PyErr_Format(PyExc_ValueError,                                  \
+                         "Invalid character while parsing %s (Index: %lu)", \
+                         field_name, (c - str) / sizeof(char));             \
+        }                                                                   \
+        return NULL;                                                        \
+    }
 #endif
 
 static void *
-format_unexpected_character_exception(char *field_name, char *c, size_t index,
+format_unexpected_character_exception(char *field_name, const char *c,
+                                      size_t index,
                                       int expected_character_count)
 {
     if (*c == '\0') {
@@ -116,48 +125,55 @@ format_unexpected_character_exception(char *field_name, char *c, size_t index,
             (expected_character_count != 1) ? "s" : "");
     }
     else {
-        #if PY_VERSION_AT_LEAST_33
-            PyObject *unicode_str = PyUnicode_FromString(c);
-            PyObject *unicode_char = PyUnicode_Substring(unicode_str, 0, 1);
+#if PY_VERSION_AT_LEAST_33
+        PyObject *unicode_str = PyUnicode_FromString(c);
+        PyObject *unicode_char = PyUnicode_Substring(unicode_str, 0, 1);
+        PyErr_Format(PyExc_ValueError,
+                     "Invalid character while parsing %s ('%U', Index: %zu)",
+                     field_name, unicode_char, index);
+        Py_DECREF(unicode_str);
+        Py_DECREF(unicode_char);
+#else
+        if (isascii((int)*c)) {
+            PyErr_Format(
+                PyExc_ValueError,
+                "Invalid character while parsing %s ('%c', Index: %zu)",
+                field_name, *c, index);
+        }
+        else {
             PyErr_Format(PyExc_ValueError,
-                        "Invalid character while parsing %s ('%U', Index: %zu)",
-                        field_name, unicode_char, index);
-            Py_DECREF(unicode_str);
-            Py_DECREF(unicode_char);
-        #else
-            if (isascii((int) *c)) {
-                PyErr_Format(PyExc_ValueError,
-                        "Invalid character while parsing %s ('%c', Index: %zu)",
-                        field_name, *c, index);
-            }
-            else {
-                PyErr_Format(PyExc_ValueError,
-                        "Invalid character while parsing %s (Index: %zu)",
-                        field_name, index);
-            }
-        #endif
+                         "Invalid character while parsing %s (Index: %zu)",
+                         field_name, index);
+        }
+#endif
     }
     return NULL;
 }
 
 #define IS_CALENDAR_DATE_SEPARATOR (*c == '-')
+#define IS_ISOCALENDAR_SEPARATOR   (*c == 'W')
 #define IS_DATE_AND_TIME_SEPARATOR (*c == 'T' || *c == ' ' || *c == 't')
-#define IS_TIME_SEPARATOR (*c == ':')
+#define IS_TIME_SEPARATOR          (*c == ':')
 #define IS_TIME_ZONE_SEPARATOR \
     (*c == 'Z' || *c == '-' || *c == '+' || *c == 'z')
 #define IS_FRACTIONAL_SEPARATOR (*c == '.' || (*c == ',' && !rfc3339_only))
 
 static PyObject *
-_parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
+_parse(PyObject *self, PyObject *dtstr, int parse_any_tzinfo, int rfc3339_only)
 {
     PyObject *obj;
     PyObject *tzinfo = Py_None;
+#if PY_VERSION_AT_LEAST_33
+    Py_ssize_t len;
+#endif
 
     int i;
-    char *str = NULL;
-    char *c;
+    const char *str;
+    const char *c;
     int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0,
         usecond = 0;
+    int iso_week = 0, iso_day = 0;
+    int ordinal_day = 0;
     int time_is_midnight = 0;
     int tzhour = 0, tzminute = 0, tzsign = 0;
 #if CISO8601_CACHING_ENABLED
@@ -167,9 +183,20 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
     PyObject *temp;
     int extended_date_format = 0;
 
-    if (!PyArg_ParseTuple(args, "s", &str))
+#if PY_MAJOR_VERSION >= 3
+    if (!PyUnicode_Check(dtstr)) {
+#else
+    if (!PyString_Check(dtstr)) {
+#endif
+        PyErr_SetString(PyExc_TypeError, "argument must be str");
         return NULL;
-    c = str;
+    }
+
+#if PY_VERSION_AT_LEAST_33
+    str = c = PyUnicode_AsUTF8AndSize(dtstr, &len);
+#else
+    str = c = PyString_AsString(dtstr);
+#endif
 
     /* Year */
     PARSE_INTEGER(year, 4, "year")
@@ -189,26 +216,73 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
     }
 #endif
 
-    if (IS_CALENDAR_DATE_SEPARATOR) { /* Separated Month and Day (ie. MM-DD) */
+    if (IS_CALENDAR_DATE_SEPARATOR) {
         c++;
         extended_date_format = 1;
 
-        /* Month */
-        PARSE_INTEGER(month, 2, "month")
+        if (IS_ISOCALENDAR_SEPARATOR) { /* Separated ISO Calendar week and day
+                                           (i.e., Www-D) */
+            c++;
+
+            if (rfc3339_only) {
+                PyErr_SetString(PyExc_ValueError,
+                                "Datetime string not in RFC 3339 format.");
+                return NULL;
+            }
+
+            PARSE_INTEGER(iso_week, 2, "iso_week")
 
-        if (*c != '\0' && !IS_DATE_AND_TIME_SEPARATOR) { /* Optional Day */
-            PARSE_SEPARATOR(IS_CALENDAR_DATE_SEPARATOR, "date separator ('-')")
+            if (*c != '\0' && !IS_DATE_AND_TIME_SEPARATOR) { /* Optional Day */
+                PARSE_SEPARATOR(IS_CALENDAR_DATE_SEPARATOR,
+                                "date separator ('-')")
+                PARSE_INTEGER(iso_day, 1, "iso_day")
+            }
+            else {
+                iso_day = 1;
+            }
 
-            /* Day */
-            PARSE_INTEGER(day, 2, "day")
-        }
-        else if (rfc3339_only) {
-            PyErr_SetString(PyExc_ValueError,
-                            "Datetime string not in RFC 3339 format.");
-            return NULL;
+            int rv = iso_to_ymd(year, iso_week, iso_day, &year, &month, &day);
+            if (rv) {
+                PyErr_Format(PyExc_ValueError, "Invalid ISO Calendar date");
+                return NULL;
+            }
         }
-        else {
-            day = 1;
+        else { /* Separated month and may (i.e., MM-DD) or
+                  ordinal date (i.e., DDD) */
+            /* For sake of simplicity, we'll assume that it is a month
+             * If we find out later that it's an ordinal day, then we'll adjust
+             */
+            PARSE_INTEGER(month, 2, "month")
+
+            if (*c != '\0' && !IS_DATE_AND_TIME_SEPARATOR) {
+                if (IS_CALENDAR_DATE_SEPARATOR) { /* Optional day */
+                    c++;
+                    PARSE_INTEGER(day, 2, "day")
+                }
+                else { /* Ordinal day */
+                    PARSE_INTEGER(ordinal_day, 1, "ordinal day")
+                    ordinal_day = (month * 10) + ordinal_day;
+
+                    int rv =
+                        ordinal_to_ymd(year, ordinal_day, &year, &month, &day);
+                    if (rv) {
+                        PyErr_Format(
+                            PyExc_ValueError,
+                            "Invalid ordinal day: %d is %s for year %d",
+                            ordinal_day, rv == -1 ? "too small" : "too large",
+                            year);
+                        return NULL;
+                    }
+                }
+            }
+            else if (rfc3339_only) {
+                PyErr_SetString(PyExc_ValueError,
+                                "Datetime string not in RFC 3339 format.");
+                return NULL;
+            }
+            else {
+                day = 1;
+            }
         }
     }
     else if (rfc3339_only) {
@@ -216,13 +290,55 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
                         "Datetime string not in RFC 3339 format.");
         return NULL;
     }
-    else { /* Non-separated Month and Day (ie. MMDD) */
-        /* Month */
-        PARSE_INTEGER(month, 2, "month")
-        /* Note that YYMM is not a valid timestamp. If the calendar date is not
-         * separated, a day is required (ie. YYMMDD)
-         */
-        PARSE_INTEGER(day, 2, "day")
+    else {
+        if (IS_ISOCALENDAR_SEPARATOR) { /* Non-separated ISO Calendar week and
+                                           day (i.e., WwwD) */
+            c++;
+
+            PARSE_INTEGER(iso_week, 2, "iso_week")
+
+            if (*c != '\0' && !IS_DATE_AND_TIME_SEPARATOR) { /* Optional Day */
+                PARSE_INTEGER(iso_day, 1, "iso_day")
+            }
+            else {
+                iso_day = 1;
+            }
+
+            int rv = iso_to_ymd(year, iso_week, iso_day, &year, &month, &day);
+            if (rv) {
+                PyErr_Format(PyExc_ValueError, "Invalid ISO Calendar date");
+                return NULL;
+            }
+        }
+        else { /* Non-separated Month and Day (i.e., MMDD) or
+                  ordinal date (i.e., DDD)*/
+            /* For sake of simplicity, we'll assume that it is a month
+             * If we find out later that it's an ordinal day, then we'll adjust
+             */
+            PARSE_INTEGER(month, 2, "month")
+
+            PARSE_INTEGER(ordinal_day, 1, "ordinal day")
+
+            if (*c == '\0' || IS_DATE_AND_TIME_SEPARATOR) { /* Ordinal day */
+                ordinal_day = (month * 10) + ordinal_day;
+                int rv =
+                    ordinal_to_ymd(year, ordinal_day, &year, &month, &day);
+                if (rv) {
+                    PyErr_Format(PyExc_ValueError,
+                                 "Invalid ordinal day: %d is %s for year %d",
+                                 ordinal_day,
+                                 rv == -1 ? "too small" : "too large", year);
+                    return NULL;
+                }
+            }
+            else { /* Day */
+                /* Note that YYYYMM is not a valid timestamp. If the calendar
+                 * date is not separated, a day is required (i.e., YYMMDD)
+                 */
+                PARSE_INTEGER(day, 1, "day")
+                day = (ordinal_day * 10) + day;
+            }
+        }
     }
 
 #if !PY_VERSION_AT_LEAST_36
@@ -287,7 +403,7 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
     if (*c != '\0') {
         /* Date and time separator */
         PARSE_SEPARATOR(IS_DATE_AND_TIME_SEPARATOR,
-                        "date and time separator (ie. 'T' or ' ')")
+                        "date and time separator (i.e., 'T', 't', or ' ')")
 
         /* Hour */
         PARSE_INTEGER(hour, 2, "hour")
@@ -295,7 +411,8 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
         if (*c != '\0' &&
             !IS_TIME_ZONE_SEPARATOR) { /* Optional minute and second */
 
-            if (IS_TIME_SEPARATOR) { /* Separated Minute and Second (ie. mm:ss)
+            if (IS_TIME_SEPARATOR) { /* Separated Minute and Second
+                                      *  (i.e., mm:ss)
                                       */
                 c++;
 
@@ -337,7 +454,7 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
                                 "mandatory in RFC 3339.");
                 return NULL;
             }
-            else { /* Non-separated Minute and Second (ie. mmss) */
+            else { /* Non-separated Minute and Second (i.e., mmss) */
                 /* Minute */
                 PARSE_INTEGER(minute, 2, "minute")
                 if (*c != '\0' &&
@@ -478,13 +595,13 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
                     if ((tzinfo = tz_cache[tz_index]) == NULL) {
                         tzinfo = new_fixed_offset(60 * tzminute);
 
-                        if (tzinfo == NULL) /* ie. PyErr_Occurred() */
+                        if (tzinfo == NULL) /* i.e., PyErr_Occurred() */
                             return NULL;
                         tz_cache[tz_index] = tzinfo;
                     }
 #else
                     tzinfo = new_fixed_offset(60 * tzminute);
-                    if (tzinfo == NULL) /* ie. PyErr_Occurred() */
+                    if (tzinfo == NULL) /* i.e., PyErr_Occurred() */
                         return NULL;
 #endif
                 }
@@ -533,30 +650,40 @@ _parse(PyObject *self, PyObject *args, int parse_any_tzinfo, int rfc3339_only)
 }
 
 static PyObject *
-parse_datetime_as_naive(PyObject *self, PyObject *args)
+parse_datetime_as_naive(PyObject *self, PyObject *dtstr)
+{
+    return _parse(self, dtstr, 0, 0);
+}
+
+static PyObject *
+parse_datetime(PyObject *self, PyObject *dtstr)
 {
-    return _parse(self, args, 0, 0);
+    return _parse(self, dtstr, 1, 0);
 }
 
 static PyObject *
-parse_datetime(PyObject *self, PyObject *args)
+parse_rfc3339(PyObject *self, PyObject *dtstr)
 {
-    return _parse(self, args, 1, 0);
+    return _parse(self, dtstr, 1, 1);
 }
 
 static PyObject *
-parse_rfc3339(PyObject *self, PyObject *args)
+_hard_coded_benchmark_timestamp(PyObject *self, PyObject *ignored)
 {
-    return _parse(self, args, 1, 1);
+    return PyDateTimeAPI->DateTime_FromDateAndTime(
+        2014, 1, 9, 21, 48, 0, 0, Py_None, PyDateTimeAPI->DateTimeType);
 }
 
 static PyMethodDef CISO8601Methods[] = {
-    {"parse_datetime", parse_datetime, METH_VARARGS,
+    {"parse_datetime", (PyCFunction)parse_datetime, METH_O,
      "Parse a ISO8601 date time string."},
-    {"parse_datetime_as_naive", parse_datetime_as_naive, METH_VARARGS,
+    {"parse_datetime_as_naive", parse_datetime_as_naive, METH_O,
      "Parse a ISO8601 date time string, ignoring the time zone component."},
-    {"parse_rfc3339", parse_rfc3339, METH_VARARGS,
+    {"parse_rfc3339", parse_rfc3339, METH_O,
      "Parse an RFC 3339 date time string."},
+    {"_hard_coded_benchmark_timestamp", _hard_coded_benchmark_timestamp,
+     METH_NOARGS,
+     "Return a datetime using hardcoded values (for benchmarking purposes)"},
     {NULL, NULL, 0, NULL}};
 
 #if PY_MAJOR_VERSION >= 3


=====================================
setup.py
=====================================
@@ -31,7 +31,7 @@ if os.environ.get("STRICT_WARNINGS", "0") == "1":
         os.environ["_CL_"] = ""
     os.environ["_CL_"] += " /WX"
 
-VERSION = "2.2.0"
+VERSION = "2.3.1"
 CISO8601_CACHING_ENABLED = int(os.environ.get('CISO8601_CACHING_ENABLED', '1') == '1')
 
 setup(
@@ -44,7 +44,7 @@ setup(
     ext_modules=[
         Extension(
             "ciso8601",
-            sources=["module.c", "timezone.c"],
+            sources=["module.c", "timezone.c", "isocalendar.c"],
             define_macros=[
                 ("CISO8601_VERSION", VERSION),
                 ("CISO8601_CACHING_ENABLED", CISO8601_CACHING_ENABLED),
@@ -56,7 +56,6 @@ setup(
     test_suite="tests",
     tests_require=[
         "pytz",
-        "unittest2 ; python_version < '3'",
     ],
     classifiers=[
         "Intended Audience :: Developers",
@@ -72,6 +71,9 @@ setup(
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Topic :: Software Development :: Libraries :: Python Modules",
     ],
 )


=====================================
tests/test_timezone.py
=====================================
@@ -1,19 +1,15 @@
 # -*- coding: utf-8 -*-
 
 import sys
+import unittest
 
 from datetime import datetime, timedelta
 
 from ciso8601 import FixedOffset
 
 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
-
+    # We use add `unittest.TestCase.assertRaisesRegex` method, which is called `assertRaisesRegexp` in Python 2.
+    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
 
 class TimezoneTestCase(unittest.TestCase):
     def test_utcoffset(self):
@@ -25,12 +21,13 @@ class TimezoneTestCase(unittest.TestCase):
                 built_in_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=tz)
                 our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(minutes * 60))
                 self.assertEqual(built_in_dt.utcoffset(), our_dt.utcoffset(), "`utcoffset` output did not match for offset: {minutes}".format(minutes=minutes))
+                self.assertEqual(built_in_dt.tzinfo.utcoffset(built_in_dt), our_dt.tzinfo.utcoffset(our_dt), "`tzinfo.utcoffset` output did not match for offset: {minutes}".format(minutes=minutes))
         else:
-            self.assertEqual(FixedOffset(0).utcoffset(), timedelta(minutes=0))
-            self.assertEqual(FixedOffset(+0).utcoffset(), timedelta(minutes=0))
-            self.assertEqual(FixedOffset(-0).utcoffset(), timedelta(minutes=0))
-            self.assertEqual(FixedOffset(-4980).utcoffset(), timedelta(hours=-1, minutes=-23))
-            self.assertEqual(FixedOffset(+45240).utcoffset(), timedelta(hours=12, minutes=34))
+            for seconds in [0, +0, -0, -4980, +45240]:
+                offset = FixedOffset(seconds)
+                our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=offset)
+                self.assertEqual(our_dt.utcoffset(), timedelta(seconds=seconds))
+                self.assertEqual(offset.utcoffset(our_dt), timedelta(seconds=seconds))
 
     def test_dst(self):
         if sys.version_info >= (3, 2):
@@ -41,12 +38,13 @@ class TimezoneTestCase(unittest.TestCase):
                 built_in_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=tz)
                 our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(minutes * 60))
                 self.assertEqual(built_in_dt.dst(), our_dt.dst(), "`dst` output did not match for offset: {minutes}".format(minutes=minutes))
+                self.assertEqual(built_in_dt.tzinfo.dst(built_in_dt), our_dt.tzinfo.dst(our_dt), "`tzinfo.dst` output did not match for offset: {minutes}".format(minutes=minutes))
         else:
-            self.assertIsNone(FixedOffset(0).dst(), "UTC")
-            self.assertIsNone(FixedOffset(+0).dst(), "UTC")
-            self.assertIsNone(FixedOffset(-0).dst(), "UTC")
-            self.assertIsNone(FixedOffset(-4980).dst(), "UTC-01:23")
-            self.assertIsNone(FixedOffset(+45240).dst(), "UTC+12:34")
+            for seconds in [0, +0, -0, -4980, +45240]:
+                offset = FixedOffset(seconds)
+                our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=offset)
+                self.assertIsNone(our_dt.dst())
+                self.assertIsNone(offset.dst(our_dt))
 
     def test_tzname(self):
         if sys.version_info >= (3, 2):
@@ -57,12 +55,49 @@ class TimezoneTestCase(unittest.TestCase):
                 built_in_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=tz)
                 our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(minutes * 60))
                 self.assertEqual(built_in_dt.tzname(), our_dt.tzname(), "`tzname` output did not match for offset: {minutes}".format(minutes=minutes))
+                self.assertEqual(built_in_dt.tzinfo.tzname(built_in_dt), our_dt.tzinfo.tzname(our_dt), "`tzinfo.tzname` output did not match for offset: {minutes}".format(minutes=minutes))
         else:
-            self.assertEqual(FixedOffset(0).tzname(), "UTC+00:00")
-            self.assertEqual(FixedOffset(+0).tzname(), "UTC+00:00")
-            self.assertEqual(FixedOffset(-0).tzname(), "UTC+00:00")
-            self.assertEqual(FixedOffset(-4980).tzname(), "UTC-01:23")
-            self.assertEqual(FixedOffset(+45240).tzname(), "UTC+12:34")
+            for seconds, expected_tzname in [(0, "UTC+00:00"), (+0, "UTC+00:00"), (-0, "UTC+00:00"), (-4980, "UTC-01:23"), (+45240, "UTC+12:34")]:
+                offset = FixedOffset(seconds)
+                our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=offset)
+                self.assertEqual(our_dt.tzname(), expected_tzname)
+                self.assertEqual(offset.tzname(our_dt), expected_tzname)
+
+    def test_fromutc(self):
+        # https://github.com/closeio/ciso8601/issues/108
+        our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        expected_dt = datetime(2014, 2, 3, 11, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        self.assertEqual(expected_dt, our_dt.tzinfo.fromutc(our_dt))
+
+        if sys.version_info >= (3, 2):
+            from datetime import timezone
+            td = timedelta(minutes=60)
+            tz = timezone(td)
+            built_in_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=tz)
+            built_in_result = built_in_dt.tzinfo.fromutc(built_in_dt)
+            self.assertEqual(expected_dt, built_in_result)
+
+    def test_fromutc_straddling_a_day_boundary(self):
+        our_dt = datetime(2020, 2, 29, 23, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        expected_dt = datetime(2020, 3, 1, 0, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        self.assertEqual(expected_dt, our_dt.tzinfo.fromutc(our_dt))
+
+    def test_fromutc_fails_if_given_non_datetime(self):
+        our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        with self.assertRaises(TypeError, msg="fromutc: argument must be a datetime"):
+            our_dt.tzinfo.fromutc(123)
+
+    def test_fromutc_fails_if_tzinfo_is_none(self):
+        our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        other_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=None)
+        with self.assertRaises(ValueError, msg="fromutc: dt.tzinfo is not self"):
+            our_dt.tzinfo.fromutc(other_dt)
+
+    def test_fromutc_fails_if_tzinfo_is_some_other_offset(self):
+        our_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(60 * 60))
+        other_dt = datetime(2014, 2, 3, 10, 35, 27, 234567, tzinfo=FixedOffset(120 * 60))
+        with self.assertRaises(ValueError, msg="fromutc: dt.tzinfo is not self"):
+            our_dt.tzinfo.fromutc(other_dt)
 
 if __name__ == '__main__':
     unittest.main()


=====================================
tests/tests.py
=====================================
@@ -6,17 +6,14 @@ import pickle
 import platform
 import re
 import sys
+import unittest
 
-from ciso8601 import FixedOffset, parse_datetime, parse_datetime_as_naive, parse_rfc3339
+from ciso8601 import _hard_coded_benchmark_timestamp, FixedOffset, parse_datetime, parse_datetime_as_naive, parse_rfc3339
 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
+    # We use add `unittest.TestCase.assertRaisesRegex` method, which is called `assertRaisesRegexp` in Python 2.
+    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
 
 
 class ValidTimestampTestCase(unittest.TestCase):
@@ -59,36 +56,75 @@ class ValidTimestampTestCase(unittest.TestCase):
 
     def test_returns_built_in_utc_if_available(self):
         # Python 3.7 added a built-in UTC object at the C level (`PyDateTime_TimeZone_UTC`)
-        # PyPy added support for it in 7.3.6
+        # PyPy added support for it in 7.3.6, but only for PyPy 3.8+
 
         timestamp = '2018-01-01T00:00:00.00Z'
-        if (platform.python_implementation() == 'CPython' and sys.version_info >= (3, 7)) or \
-           (platform.python_implementation() == 'PyPy' and sys.pypy_version_info >= (7, 3, 6)):
+        if sys.version_info >= (3, 7) and \
+            (platform.python_implementation() == 'CPython'
+             or (platform.python_implementation() == 'PyPy' and sys.version_info >= (3, 8) and sys.pypy_version_info >= (7, 3, 6))):
             self.assertIs(parse_datetime(timestamp).tzinfo, datetime.timezone.utc)
         else:
             self.assertIsInstance(parse_datetime(timestamp).tzinfo, FixedOffset)
 
+    def test_ordinal(self):
+        self.assertEqual(
+            parse_datetime("2014-001"),
+            datetime.datetime(2014, 1, 1, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2014-031"),
+            datetime.datetime(2014, 1, 31, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2014-032"),
+            datetime.datetime(2014, 2, 1, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2014-059"),
+            datetime.datetime(2014, 2, 28, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2014-060"),
+            datetime.datetime(2014, 3, 1, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2016-060"), # Leap year
+            datetime.datetime(2016, 2, 29, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2014-365"),
+            datetime.datetime(2014, 12, 31, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2016-365"), # Leap year
+            datetime.datetime(2016, 12, 30, 0, 0, 0),
+        )
+        self.assertEqual(
+            parse_datetime("2016-366"), # Leap year
+            datetime.datetime(2016, 12, 31, 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():
+        for timestamp, reason 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)):
+                with self.assertRaises(ValueError, msg="Timestamp '{0}' was supposed to be invalid ({1}), but parsing it didn't raise ValueError.".format(timestamp, reason)):
                     parse_datetime(timestamp)
             except Exception as exc:
-                print("Timestamp '{0}' was supposed to raise ValueError, but raised {1} instead".format(timestamp, type(exc).__name__))
+                print("Timestamp '{0}' was supposed to raise ValueError ({1}), but raised {2} instead".format(timestamp, reason, 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\)",
+                r"Invalid character while parsing date and time separator \(i.e., 'T', 't', or ' '\) \('🐵', Index: 10\)",
                 parse_datetime,
-                "2019-01🐵01",
+                "2019-01-01🐵01:02:03Z",
             )
             self.assertRaisesRegex(
                 ValueError,
@@ -99,7 +135,7 @@ class InvalidTimestampTestCase(unittest.TestCase):
         else:
             self.assertRaisesRegex(
                 ValueError,
-                r"Invalid character while parsing date separator \('-'\) \(Index: 7\)",
+                r"Invalid character while parsing ordinal day \(Index: 7\)",
                 parse_datetime,
                 "2019-01🐵01",
             )
@@ -120,21 +156,21 @@ class InvalidTimestampTestCase(unittest.TestCase):
 
         self.assertRaisesRegex(
             ValueError,
-            r"Invalid character while parsing date separator \('-'\) \('=', Index: 7\)",
+            r"Invalid character while parsing ordinal day \('=', Index: 7\)",
             parse_datetime,
             "2018-01=01",
         )
 
         self.assertRaisesRegex(
             ValueError,
-            r"Invalid character while parsing date separator \('-'\) \('0', Index: 7\)",
+            r"Invalid character while parsing date and time separator \(i.e., 'T', 't', or ' '\) \('2', Index: 8\)",
             parse_datetime,
-            "2018-0101",
+            "2018-0102",
         )
 
         self.assertRaisesRegex(
             ValueError,
-            r"Invalid character while parsing day \('-', Index: 6\)",
+            r"Invalid character while parsing ordinal day \('-', Index: 6\)",
             parse_datetime,
             "201801-01",
         )
@@ -263,10 +299,53 @@ class InvalidTimestampTestCase(unittest.TestCase):
                 "2014-06-00",
             )
 
+    def test_invalid_ordinal(self):
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 0 is too small",
+            parse_datetime,
+            "2014-000",
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 0 is too small",
+            parse_datetime,
+            "2014000",
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 366 is too large for year 2014",
+            parse_datetime,
+            "2014-366", # Not a leap year
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 366 is too large for year 2014",
+            parse_datetime,
+            "2014366", # Not a leap year
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 999 is too large for year 2014",
+            parse_datetime,
+            "2014-999",
+        )
+
+        self.assertRaisesRegex(
+            ValueError,
+            r"Invalid ordinal day: 999 is too large for year 2014",
+            parse_datetime,
+            "2014999",
+        )
+
     def test_invalid_yyyymm_format(self):
         self.assertRaisesRegex(
             ValueError,
-            r"Unexpected end of string while parsing day. Expected 2 more characters",
+            r"Unexpected end of string while parsing ordinal day. Expected 1 more character",
             parse_datetime,
             "201406",
         )
@@ -274,7 +353,7 @@ class InvalidTimestampTestCase(unittest.TestCase):
     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\)",
+            r"Invalid character while parsing date and time separator \(i.e., 'T', 't', or ' '\) \('_', Index: 10\)",
             parse_datetime,
             "2018-01-01_00:00:00",
         )
@@ -505,7 +584,7 @@ class GithubIssueRegressionTestCase(unittest.TestCase):
     def test_issue_35(self):
         self.assertRaisesRegex(
             ValueError,
-            r"Invalid character while parsing date separator \('-'\) \('1', Index: 7\)",
+            r"Invalid character while parsing date and time separator \(i.e., 'T', 't', or ' '\) \('2', Index: 8\)",
             parse_datetime,
             "2017-0012-27T13:35:19+0200",
         )
@@ -542,5 +621,12 @@ class GithubIssueRegressionTestCase(unittest.TestCase):
         )
 
 
+class HardCodedBenchmarkTimestampTestCase(unittest.TestCase):
+    def test_returns_expected_hardcoded_datetime(self):
+        self.assertEqual(
+            _hard_coded_benchmark_timestamp(),
+            datetime.datetime(2014, 1, 9, 21, 48, 0, 0),
+        )
+
 if __name__ == "__main__":
     unittest.main()


=====================================
timezone.c
=====================================
@@ -33,8 +33,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #include <datetime.h>
 #include <structmember.h>
 
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
+#define SECS_PER_MIN                 60
+#define SECS_PER_HOUR                (60 * SECS_PER_MIN)
 #define TWENTY_FOUR_HOURS_IN_SECONDS 86400
 
 #define PY_VERSION_AT_LEAST_36 \
@@ -46,14 +46,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 typedef struct {
     // Seconds offset from UTC.
     // Must be in range (-86400, 86400) seconds exclusive.
-    // ie. (-1440, 1440) minutes exclusive.
+    // i.e., (-1440, 1440) minutes exclusive.
     PyObject_HEAD int offset;
 } FixedOffset;
 
-/*
- * def __init__(self, offset):
- *     self.offset = offset
- */
 static int
 FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs)
 {
@@ -72,46 +68,52 @@ FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs)
     return 0;
 }
 
-/*
- * def utcoffset(self, dt):
- *     return timedelta(seconds=self.offset * 60)
- */
 static PyObject *
-FixedOffset_utcoffset(FixedOffset *self, PyObject *args)
+FixedOffset_utcoffset(FixedOffset *self, PyObject *dt)
 {
     return PyDelta_FromDSU(0, self->offset, 0);
 }
 
-/*
- * def dst(self, dt):
- *     return timedelta(seconds=self.offset * 60)
- */
 static PyObject *
-FixedOffset_dst(FixedOffset *self, PyObject *args)
+FixedOffset_dst(FixedOffset *self, PyObject *dt)
 {
     Py_RETURN_NONE;
 }
 
-/*
- * def tzname(self, dt):
- *     sign = '+'
- *     if self.offset < 0:
- *         sign = '-'
- *     return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60)
- */
 static PyObject *
-FixedOffset_tzname(FixedOffset *self, PyObject *args)
+FixedOffset_fromutc(FixedOffset *self, PyDateTime_DateTime *dt)
 {
+    if (!PyDateTime_Check(dt)) {
+        PyErr_SetString(PyExc_TypeError,
+                        "fromutc: argument must be a datetime");
+        return NULL;
+    }
+    if (!dt->hastzinfo || dt->tzinfo != (PyObject *)self) {
+        PyErr_SetString(PyExc_ValueError,
+                        "fromutc: dt.tzinfo "
+                        "is not self");
+        return NULL;
+    }
 
+    return PyNumber_Add((PyObject *)dt,
+                        FixedOffset_utcoffset(self, (PyObject *)self));
+}
+
+static PyObject *
+FixedOffset_tzname(FixedOffset *self, PyObject *dt)
+{
     int offset = self->offset;
 
-    if (offset == 0){
+    if (offset == 0) {
 #if PY_VERSION_AT_LEAST_36
         return PyUnicode_FromString("UTC");
-#else
+#elif PY_MAJOR_VERSION >= 3
         return PyUnicode_FromString("UTC+00:00");
+#else
+        return PyString_FromString("UTC+00:00");
 #endif
-    } else {
+    }
+    else {
         char result_tzname[10] = {0};
         char sign = '+';
 
@@ -120,26 +122,22 @@ FixedOffset_tzname(FixedOffset *self, PyObject *args)
             offset *= -1;
         }
         snprintf(result_tzname, 10, "UTC%c%02u:%02u", sign,
-             (offset / SECS_PER_HOUR) & 31,
-             offset / SECS_PER_MIN % SECS_PER_MIN);
+                 (offset / SECS_PER_HOUR) & 31,
+                 offset / SECS_PER_MIN % SECS_PER_MIN);
+#if PY_MAJOR_VERSION >= 3
         return PyUnicode_FromString(result_tzname);
+#else
+        return PyString_FromString(result_tzname);
+#endif
     }
 }
 
-/*
- * def __repr__(self):
- *     return self.tzname()
- */
 static PyObject *
 FixedOffset_repr(FixedOffset *self)
 {
     return FixedOffset_tzname(self, NULL);
 }
 
-/*
- * def __getinitargs__(self):
- *     return (self.offset,)
- */
 static PyObject *
 FixedOffset_getinitargs(FixedOffset *self)
 {
@@ -157,11 +155,20 @@ static PyMemberDef FixedOffset_members[] = {
  * Class methods
  */
 static PyMethodDef FixedOffset_methods[] = {
-    {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""},
-    {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""},
-    {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""},
-    {"__getinitargs__", (PyCFunction)FixedOffset_getinitargs, METH_VARARGS,
-     ""},
+    {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_O,
+     PyDoc_STR("Return fixed offset.")},
+
+    {"dst", (PyCFunction)FixedOffset_dst, METH_O, PyDoc_STR("Return None.")},
+
+    {"fromutc", (PyCFunction)FixedOffset_fromutc, METH_O,
+     PyDoc_STR("datetime in UTC -> datetime in local time.")},
+
+    {"tzname", (PyCFunction)FixedOffset_tzname, METH_O,
+     PyDoc_STR("Returns offset as 'UTC(+|-)HH:MM'")},
+
+    {"__getinitargs__", (PyCFunction)FixedOffset_getinitargs, METH_NOARGS,
+     PyDoc_STR("pickle support")},
+
     {NULL}};
 
 static PyTypeObject FixedOffset_type = {


=====================================
tox.ini
=====================================
@@ -1,7 +1,10 @@
 [tox]
-envlist = {py27,py34,py35,py36,py37,py38,py39}-caching_{enabled,disabled}
+requires =
+    tox>=4
+envlist = {py27,py34,py35,py36,py37,py38,py39,py310,py311,py312}-caching_{enabled,disabled}
 
 [testenv]
+package = sdist
 setenv =
     STRICT_WARNINGS = 1
     caching_enabled: CISO8601_CACHING_ENABLED = 1
@@ -9,5 +12,4 @@ setenv =
 deps =
     pytz
     nose
-    unittest2
 commands=nosetests


=====================================
why_ciso8601.md
=====================================
@@ -0,0 +1,58 @@
+# Should I use ciso8601? <!-- omit in toc -->
+
+`ciso8601`'s goal is to be the world's fastest ISO 8601 datetime parser for Python.
+However, `ciso8601` is not the right choice for all use cases.
+This document aims to describe some considerations to make when choosing a timestamp parsing library.
+
+- [Do you care about the performance of timestamp parsing?](#do-you-care-about-the-performance-of-timestamp-parsing)
+- [Do you need strict RFC 3339 parsing?](#do-you-need-strict-rfc-3339-parsing)
+- [Do you need to support Python \< 3.11?](#do-you-need-to-support-python--311)
+- [Do you need to support Python 2.7?](#do-you-need-to-support-python-27)
+
+### Flowchart <!-- omit in toc -->
+
+```mermaid
+graph TD;
+    A[Do you care about the performance of timestamp parsing?]
+    A--yes-->Y;
+    A--no-->C;
+
+    C[Do you need to support Python 2.7?];
+    C--yes-->Y
+    C--no-->E
+
+    E[Do you need strict RFC 3339 parsing?];
+    E--yes-->Y;
+    E--no-->H;
+
+    H[Do you need to support Python < 3.11?]
+    H--yes-->V;
+    H--no-->Z;
+
+    V[Use `backports.datetime_fromisoformat`]
+    Y[Use `ciso8601`]
+    Z[Use `datetime.fromisoformat`]
+```
+
+## Do you care about the performance of timestamp parsing?
+
+In most Python programs, performance is not a primary concern.
+Even for performance-sensitive programs, timestamp parsing performance is often a negligible portion of the time spent, and not a performance bottleneck.
+
+**Note:** Since Python 3.11+, the performance of cPython's `datetime.fromisoformat` is now very good. See [the benchmarks](https://github.com/closeio/ciso8601#benchmark).
+
+If you really, truly want to use the fastest parser, then `ciso8601` aims to be the fastest. See [the benchmarks](https://github.com/closeio/ciso8601#benchmark) to see how it compares to other options.
+
+## Do you need strict RFC 3339 parsing?
+
+RFC 3339 can be (roughly) thought of as a subset of ISO 8601. If you need strict timestamp parsing that will complain if the given timestamp isn't strictly RFC 3339 compliant, then [`ciso8601` has a `parse_rfc3339` method](https://github.com/closeio/ciso8601#strict-rfc-3339-parsing).
+
+## Do you need to support Python < 3.11?
+
+Since Python 3.11, `datetime.fromisoformat` supports parsing nearly any ISO 8601 timestamp, and the cPython implementation is [very performant](https://github.com/closeio/ciso8601#benchmark).
+
+If you need to support older versions of Python 3, consider [`backports.datetime_fromisoformat`](https://github.com/movermeyer/backports.datetime_fromisoformat).
+
+## Do you need to support Python 2.7?
+
+`ciso8601` still supports Python 2.7, and is [much faster](https://github.com/closeio/ciso8601#benchmark) than other options for this [deprecated version of Python](https://pythonclock.org/).



View it on GitLab: https://salsa.debian.org/python-team/packages/python-ciso8601/-/commit/d099081de47a4b1ac13ce6c8190d04c06eea64bf

-- 
View it on GitLab: https://salsa.debian.org/python-team/packages/python-ciso8601/-/commit/d099081de47a4b1ac13ce6c8190d04c06eea64bf
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/20231108/ede2c7f5/attachment-0001.htm>


More information about the debian-med-commit mailing list