<!-- Generated with "Markdown All in One" extension for Visual Studio Code -->

- [Unreleased](#unreleased)
- [2.x.x](#2xx)
  - [Version 2.3.3](#version-233)
  - [Version 2.3.2](#version-232)
  - [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)
  - [Version 2.1.1](#version-211)
  - [Version 2.1.0](#version-210)
  - [Version 2.0.1](#version-201)
  - [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)
      - [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)

<!-- /TOC -->

# Unreleased

* Nil.

# 2.x.x

## Version 2.3.3

* Added Python 3.14 support
* Added PyPy 3.11 support
* Fixed memory leak in `FixedOffset_fromutc` (#172).
* Migrated some CI testing to GHA

## Version 2.3.2

* Add support for aarch64 wheels. Thank you @bbayles!
* Add wheels for PyPy 3.10
* Added Python 3.13 support
* Better error message when attempting to parse a BCE year (#156). Thanks @javiabellan

## 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
* Switched to using a C implementation of `timezone` objects.
    * Much faster parse times for timestamps with timezone information
        * ~2.5x faster on Python 2.7, ~10% faster on Python 3.9
    * Thanks to [`pendulum`](https://github.com/sdispater/pendulum) and @sdispater for the code.
    * Python 2.7 users no longer need to install `pytz` dependency :smiley:
* Added caching of tzinfo objects
  * Parsing is ~1.1x faster for subsequent timestamps that have the same time zone offset.
  * Caching can be disabled at compile time by setting the `CISO8601_CACHING_ENABLED=0` environment variable
* Fixed a memory leak in the case where an invalid timestamp had a non-UTC timezone and extra characters

## Version 2.1.3

* Fixed a problem where non-ASCII characters would give bad error messages (#84). Thanks @olliemath.

## Version 2.1.2

* Fixed a problem where `ciso8601.__version__` was not working (#80). Thanks @ianhoffman.
* Added Python 3.8 support (#83)
* Added benchmarking scripts (#55)

## Version 2.1.1

* Fixed a problem where builds on Windows were not working (#76). Thanks @alexandrul and @gillesdouaire, and sorry.

## Version 2.1.0

* 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).
    * 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

* Fixed some memory leaks introduced in 2.0.0 (#51)

## Version 2.0.0

Version 2.0.0 was a major rewrite of `ciso8601`.

Version 1.x.x had a problem with error handling in the case of invalid timestamps.

In 1.x.x, parse_datetime:

* All valid datetime strings within the supported subset of ISO 8601 would result in the correct Python datetime (this was good)
* Some invalid timestamps will return `None` and others might get truncated and return an incorrect Python datetime (this was bad)

A developer with a given timestamp string, could not predict a priori what `ciso8601` is going to return without looking at the code.
Fundamentally, this is the problem that version 2 addressed.

Fundamentally, `parse_datetime(dt: String): datetime` was rewritten so that it takes a string and either:

* Returns a properly parsed Python datetime, **if and only if** that **entire** string conforms to the supported subset of ISO 8601
* Raises an `ValueError` with a description of the reason why the string doesn't conform to the supported subset of ISO 8601

### Breaking changes

1. Version 2 now raises `ValueError` when a timestamp does not conform to the supported subset of ISO 8601
    * This includes trailing characters in the timestamp
    * No longer accepts single character "day" values
    * See migration guide below for more examples
2. `parse_datetime_unaware` was renamed to `parse_datetime_as_naive` (See "Migration Guide" below for reasons)

### Other Changes

* Attempting to parse a timestamp with time zone information without having pytz installed raises `ImportError` (Only affects Python 2.7). Fixes #19
* Added support for the special case of midnight (24:00:00) that is valid in ISO 8601. Fixes #41
* Fixed bug where "20140200" would not fail, but produce 2014-02-01. Fixes #42

### v1.x.x -> 2.0.0 Migration guide

#### ValueError instead of None

Places where you were checking for a return of `None` from ciso8601:

```python
    timestamp = "2018-01-01T00:00:00+05:00"
    dt = parse_datetime(timestamp)
    if dt is None:
        raise ValueError(f"Could not parse {timestamp}")
```

You should change to now expect `ValueError` to be thrown:

```python
    timestamp = "2018-01-01T00:00:00+05:00"
    dt = parse_datetime(timestamp)
```

#### Tightened ISO 8601 conformance

The rules with respect to what ciso8601 will consider a conforming ISO 8601 string have been tightened.

Now a timestamp will parse **if and only if** the timestamp is 100% conforming to the supported subset of the ISO 8601 specification.


```python
    # trailing separator
    "2014-"
    "2014-01-"
    "2014-01-01T"
    "2014-01-01T00:"
    "2014-01-01T00:00:"
    "2014-01-01T00:00:00-"
    "2014-01-01T00:00:00-00:"

    # Mix of no-separator and separator
    "201401-02"
    "2014-0102"
    "2014-01-02T00:0000"
    "2014-01-02T0000:00"

    "2014-01-02T01:23:45Zabcdefghij" # Trailing characters

    "2014-01-1" # Single digit day
    "2014-01-01T00:00:00-0:04" # Single digit tzhour
    "2014-01-01T00:00:00-00:4" # Single digit tzminute
```

These should have been considered bugs in ciso8601 1.x.x, but it may be the case that your code was relying on the previously lax parsing rules.

#### `parse_datetime_unaware` has been renamed

`parse_datetime_unaware` existed for the case where your input timestamp had time zone information, but you wanted to ignore the time zone information and therefore could save some cycles by not creating the underlying `tzinfo` object.

It has been renamed to `parse_datetime_as_naive` for 2 reasons:

1. Developers were assuming that `parse_datetime_unaware` was the function to use for parsing naive timestamps, when really it is for parsing timestamps with time zone information as naive datetimes. `parse_datetime` handles parsing both timestamps with and without time zone information and should be used for all parsing, unless you actually need this use case. See additional description in [the README](https://github.com/closeio/ciso8601/tree/raise-valueerror-on-invalid-dates#ignoring-timezone-information-while-parsing) for a more detailed description of this use case.
2. Python [refers to datetimes without time zone information](https://docs.python.org/3/library/datetime.html) as `naive`, not `unaware`

Before switching all instances of `parse_datetime_unaware`, make sure to ask yourself whether you actually intended to use `parse_datetime_unaware`.

* If you meant to parse naive timestamps as naive datetimes, use `parse_datetime` instead.
* If you actually meant to parse **timestamps with time zone information** as **naive** datetimes, use `parse_datetime_as_naive` instead.

|                                    | Input with TZ Info | Input without TZ Info |
| ---------------------------------- | ------------------ | --------------------- |
| `parse_datetime()` output          | tz aware datetime  | tz naive datetime     |
| `parse_datetime_as_naive()` output | tz naive datetime  | tz naive datetime     |
