diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 559d9498..00000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,119 +0,0 @@ -# This file is autogenerated by maturin v1.3.1 -# To update, run -# -# maturin generate-ci github -# -name: CI - -on: - push: - branches: - - main - tags: - - '*' - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - manylinux: auto - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - windows: - runs-on: windows-latest - strategy: - matrix: - target: [x64, x86] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - architecture: ${{ matrix.target }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - sdist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - name: Upload sdist - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - release: - name: Release - runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels - - name: Publish to PyPI - uses: PyO3/maturin-action@v1 - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - with: - command: upload - args: --non-interactive --skip-existing * diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..9f093f9e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Build + +on: + pull_request: + branches: + - "**" + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install "tox<5" tox-gh-actions "poetry<1.8" + - name: Test with tox + run: tox diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e830f590..568da037 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +0.0.4 (2023-11-30) +------------------ + +- 🐍 Revert to pure Python implementation, as Rust extension disadvantages + outweigh its advantages +- ☀️ Implement ``NaiveDateTime`` + 0.0.3 (2023-11-16) ------------------ diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 728b45a6..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,472 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "iana-time-zone" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indoc" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" - -[[package]] -name = "js-sys" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "parking_lot", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unindent" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" - -[[package]] -name = "wasm-bindgen" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" - -[[package]] -name = "whenever" -version = "0.0.2" -dependencies = [ - "bincode", - "chrono", - "pyo3", -] - -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index c5a73454..00000000 --- a/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "whenever" -version = "0.0.3" -edition = "2021" - -[lib] -name = "whenever" -crate-type = ["cdylib"] - -[dependencies] -bincode = "1.3.3" -chrono = { version = "0.4.31", features = ["serde"] } -pyo3 = "0.20.0" diff --git a/Makefile b/Makefile index 9c1bec04..9ac4b9aa 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,17 @@ -.PHONY: build-dev -build-dev: - maturin develop --extras test - - .PHONY: test -test: build-dev - pytest +test: + pytest -s .PHONY: mypy -mypy: build-dev - mypy py/ tests/ +mypy: + mypy src/ tests/ .PHONY: format format: - black py/ tests/ - isort py/ tests/ - cargo fmt + black src/ tests/ + isort src/ tests/ .PHONY: docs -docs: build-dev +docs: @touch docs/api.rst make -C docs/ html diff --git a/README.rst b/README.rst index 554a159e..1bffd07a 100644 --- a/README.rst +++ b/README.rst @@ -13,13 +13,18 @@ .. image:: https://img.shields.io/badge/mypy-strict-forestgreen?style=flat-square :target: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict -.. image:: https://img.shields.io/github/actions/workflow/status/ariebovenberg/whenever/CI.yml?branch=main&style=flat-square +.. image:: https://img.shields.io/badge/coverage-100%25-forestgreen?style=flat-square + :target: https://github.com/ariebovenberg/whenever + +.. image:: https://img.shields.io/github/actions/workflow/status/ariebovenberg/whenever/tests.yml?branch=main&style=flat-square :target: https://github.com/ariebovenberg/whenever .. image:: https://img.shields.io/readthedocs/whenever.svg?style=flat-square :target: http://whenever.readthedocs.io/ -**Typesafe datetimes powered by Rust's chrono library.** +**Simple, type-safe datetimes for Python** + +Five simple classes on top of the standard library to help you write bug-free code. *Currently a work in progress. Leave a ⭐️ if you're interested how this develops.* @@ -27,81 +32,167 @@ Why? ---- Most Python datetime libraries use a single class for -naive, timezoned, *and* offset datetimes, -making it all too easy to mistakenly (and *silently!*) mix them. +UTC/timezoned/offset/local/naive datetimes, +making it all too easy to mistakenly mix them. Your type checker and IDE are powerless to help you, -leaving you to discover these errors at runtime. - -**Whenever** takes a different approach, and provides dedicated datetime -types that reveal mistakes *before* you run your code. - -Types of datetime (and why they're important) ---------------------------------------------- - -🚧 **NOT YET FULLY IMPLEMENTED** 🚧 - -**Whenever** distinguishes these types: - -1. **Naive** datetime: A simple type that isn't aware of any timezone or - UTC offset. -2. **UTC**-only datetime: A fast and efficient type for when you - *only* want to deal with UTC. -3. **Offset** datetime: A datetime with a *fixed* offset from UTC. -4. **Zoned** datetime: A datetime within a timezone, often with a variable - UTC offset. Zoned datetimes may be ambiguous or non-existent. - -Below is a table of supported operations for each type: - -+-----------------------+-------+-----+--------+-------+ -| Operation | Naive | UTC | Offset | Zoned | -+=======================+=======+=====+========+=======+ -| comparison | ✅ | ✅ | ✅ | ❌ | -+-----------------------+-------+-----+--------+-------+ -| difference | ✅ | ✅ | ✅ | ❌ | -+-----------------------+-------+-----+--------+-------+ -| add/subtract duration | ✅ | ✅ | ✅ | ❌ | -+-----------------------+-------+-----+--------+-------+ -| to timestamp | ❌ | ✅ | ✅ | ⚠️ | -+-----------------------+-------+-----+--------+-------+ -| from timestamp | ❌ | ✅ | ✅ | ✅ | -+-----------------------+-------+-----+--------+-------+ -| now() | ❌ | ✅ | ✅ | ✅ | -+-----------------------+-------+-----+--------+-------+ -| to naive | n/a | ✅ | ✅ | ✅ | -+-----------------------+-------+-----+--------+-------+ -| to UTC | ❌ | n/a | ✅ | ⚠️ | -+-----------------------+-------+-----+--------+-------+ -| to offset | ❌ | ✅ | n/a | ⚠️ | -+-----------------------+-------+-----+--------+-------+ -| to zoned | ❌ | ✅ | ✅ | n/a | -+-----------------------+-------+-----+--------+-------+ - -⚠️ = returns 0, 1, or 2 results, which must explicitly be handled. - -❌ = Too ambiguous to provide a sensible result. +leading to errors at best, and silent bugs at worst. + +**Whenever** gives you five *distinct types* you can't mix up. +They're simple, fully typed, and avoid common pitfalls. +It builds on the good parts of the standard library, +and draws inspiration from battle-tested libraries in other languages. + +Best of all, **whenever** is *boring*. It doesn't do any fancy things. +It's just five dead-simple classes thinly wrapping the standard library. +There's no function over 10 lines long, and no dependencies. +The goal is to give you peace of mind. + Quickstart ---------- -**Most of the functionality it not yet implemented.** -Some basic UTC functionality is already available though: +🚧 WORK IN PROGRESS 🚧 + +Whenever provides these datetime types: .. code-block:: python - from whenever.utc import DateTime - from whenever import Some, Nothing + from whenever import ( + UTCDateTime, OffsetDateTime, ZonedDateTime, LocalDateTime, NaiveDateTime + ) + +and here's how you can use them: + ++-----------------------+-----+--------+-------+-------+-------+ +| Feature | Aware | Naive | ++ +-----+--------+-------+-------+ + +| | UTC | Offset | Zoned | Local | | ++=======================+=====+========+=======+=======+=======+ +| comparison | .. centered:: ✅ | ✅ | ++-----------------------+-----+--------+-------+-------+-------+ +| difference | .. centered:: ✅ | ✅ | ++-----------------------+-----+--------+-------+-------+-------+ +| add/subtract timedelta| ✅ | ❌ | ✅ | ✅ | ✅ | ++-----------------------+-----+--------+-------+-------+-------+ +| unambiguous | ✅ | ✅ | ❌ | ❌ | ✅ | ++-----------------------+-----+--------+-------+-------+-------+ +| to RFC3339/ISO8601 | ✅ | ✅ | ❌ | ❌ | ❌ | ++-----------------------+-----+--------+-------+-------+-------+ +| to/from timestamp | ✅ | ✅ | ✅ | ✅ | ❌ | ++-----------------------+-----+--------+-------+-------+-------+ +| now | ✅ | ✅ | ✅ | ✅ | ❌ | ++-----------------------+-----+--------+-------+-------+-------+ + +- **UTCDateTime** is always UTC: simple, fast, and unambiguous. + It's great if you're storing when something happened (or will happen) regardless of location. + + *Example use cases:* The "created" timestamp of a blog post + the scheduled start of a livestream. + + .. code-block:: python + + py311_release_livestream = UTCDateTime(2022, 10, 24, hour=17) + +- **OffsetDateTime** defines a local time using a UTC offset. + This is great if you're storing when something happened at a specific location. + It's less suitable for *future* events though, + because the UTC offset may change (e.g. due to daylight savings time). + For this reason, you also cannot add/subtract a ``timedelta`` + — the offset may have changed! + + *Example use case:* Time at which a local event occurred. + + .. code-block:: python + + from whenever import hours # alias for timedelta(hours=...) + + pycon23_started = OffsetDateTime(2023, 4, 21, hour=9, offset=hours(-6)) + +- **ZonedDateTime** accounts for timezones and their variable UTC offset. + When the clock is set backwards, times occurs twice: + a ``fold`` attribute is required to explicitly `resolve these ambiguities `_. + + *Example use case:* The time of an appointment at a specific location. + + .. code-block:: python + + from zoneinfo import ZoneInfo # timezones from the standard library 🎉 + london = ZoneInfo("Europe/London") + + # always at 11:00 in London, regardless of the offset + changing_the_guard = ZonedDateTime(2024, 12, 8, hour=11, tz=london, fold=0) + + # With `expect_unambiguous()` you can omit `fold`, + # but you'll get a ValueError in ambiguous cases. + changing_the_guard = ZonedDatetime.expect_unambiguous(2024, 12, 8, hour=11, tz=london) + +- **LocalDateTime** is a datetime in the system local timezone. + This type is great for representing a time on the current system. + + *Example use case:* An alarm clock app which runs on the user's system. + + .. code-block:: python + + print(f"Your timer will go off at {LocalDateTime.now() + hours(1)}.") + + +- **NaiveDateTime** isn't aware of timezones or UTC offset. + Use this if you need a datetime type detached from the complexities of the real world. + + *Example use case:* modeling time in a city simulation game. + + .. code-block:: python + + simulation_start = NaiveDateTime(1900, 1, 1, hour=0) + + +Why not...? +----------- + +The standard library +~~~~~~~~~~~~~~~~~~~~ + +While it has all the functionality you need, it has many pitfalls: + +- You can't be certain if ``datetime`` is naive or aware + without running the code. +- ``datetime`` inherits from ``date``, + `giving unexpected behaviour `_. +- adding/subtracting ``timedelta`` does not account for daylight savings time. +- naive datetimes implicitly function as local datetimes, + which is often not what you want. +- ``fold`` defaults to 0 for ambiguous datetimes, instead of forcing you to be explicit. +- Some outdated methods still exist (although they are deprecated), + such as ``datetime.utcnow()`` + +Pendulum +~~~~~~~~ + +Although fast and full-featured: + +- aware/naive datetimes cannot be distinguished at type-checking time. +- its types inherit from the standard library ``datetime``, + which brings along a lot of baggage and potential pitfalls. + +DateType +~~~~~~~~ + +DateType fixes most of the naive/aware issues at type-checking time, but: + +- it doesn't enforce correctness at runtime. +- it doesn't distinguish offset and zoned datetimes. +- it isn't able to *fully* type-check all `cases `_. - # Explicit types for functional/Rust-style error handling - d = DateTime.new(2020, 1, 1, 12, 0, 0).unwrap() +Heliclockter +~~~~~~~~~~~~ - match DateTime.parse("2020-08-15T12:08:30Z"): - case Some(d2) if d2 > d: - print('parsed a datetime after 2020-01-01T12:00:00Z') - case Nothing(): - print('failed to parse') +This library is a lot more explicit about the different types of datetimes, +however: - d.timestamp() # UNIX timestamp - d.to_py() # convert to Python's datetime.datetime +- it doesn't have a separate class for UTC and fixed-offset datetimes. +- its types inherit from the standard library ``datetime``, + which brings along a lot of baggage and potential pitfalls. +- No enorcement on resolving ambiguous datetimes. Versioning and compatibility policy @@ -130,6 +221,5 @@ An example of setting up things and running the tests: .. code-block:: bash - pip install maturin - maturin develop --extras test + poetry install pytest diff --git a/docs/api.rst b/docs/api.rst index f84ec59b..131773c8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,15 +6,6 @@ API reference Unless otherwise noted, all classes are immutable. -whenever.utc ------------- - -.. automodule:: whenever.utc - :members: DateTime - - -whenever --------- - -.. automodule:: whenever._common +.. automodule:: whenever :members: + :special-members: __eq__, __lt__, __str__, __add__, __sub__ diff --git a/docs/conf.py b/docs/conf.py index b19b9873..4908b13a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,22 +1,9 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# documentation build configuration file, created by -# sphinx-quickstart on Tue Jun 13 22:58:12 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - from __future__ import annotations # -- Project information ----------------------------------------------------- import importlib.metadata +import typing +typing.SPHINX_BUILD = True metadata = importlib.metadata.metadata("whenever") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..3a5cba05 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,795 @@ +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.11.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.3.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "hypothesis" +version = "6.91.0" +description = "A library for property-based testing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hypothesis-6.91.0-py3-none-any.whl", hash = "sha256:316e06d6f7d5f8ab87bcc7417fca750a2b082ed3ce902b979816b413276680b3"}, + {file = "hypothesis-6.91.0.tar.gz", hash = "sha256:a9f61a2bcfc342febcc1d04b80a99e789c57b700f91cbd43bbdb5d651af385cd"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.3)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["django (>=3.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark (>=0.10.1)"] +numpy = ["numpy (>=1.17.3)"] +pandas = ["pandas (>=1.1)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mypy-plugins" +version = "3.0.0" +description = "pytest plugin for writing tests for mypy plugins" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mypy-plugins-3.0.0.tar.gz", hash = "sha256:05a728c7cbc4f33610f97fe9266b2c3eb209e41c28935011b4fc9531662625f6"}, + {file = "pytest_mypy_plugins-3.0.0-py3-none-any.whl", hash = "sha256:a1e3f51b68898bc25713cc53718a28d9dc0cfd51d28a537ef18c7df3b123ed84"}, +] + +[package.dependencies] +decorator = "*" +Jinja2 = "*" +mypy = ">=1.3" +packaging = "*" +pytest = ">=7.0.0" +pyyaml = "*" +regex = "*" +tomlkit = ">=0.11" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "slotscheck" +version = "0.17.1" +description = "Ensure your __slots__ are working properly." +optional = false +python-versions = ">=3.8.1,<4" +files = [ + {file = "slotscheck-0.17.1-py3-none-any.whl", hash = "sha256:214f822016dcc7d3f3b254d5f9b8c731eabdc0e8dd29d0253618f65eb6deade2"}, + {file = "slotscheck-0.17.1.tar.gz", hash = "sha256:fdef47766530df994cf99dda484a15f212eaf6f2e2402026c39348f3f0e62827"}, +] + +[package.dependencies] +click = ">=8.0,<9.0" +tomli = {version = ">=0.2.6,<3.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<4.0" +content-hash = "15a844f2295a2826ed4827fdd1d1850f06a020d38751ac6a2b31830ce9b1fd8d" diff --git a/py/whenever/__init__.py b/py/whenever/__init__.py deleted file mode 100644 index c9db9f1c..00000000 --- a/py/whenever/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from abc import ABC -from typing import Generic, TypeVar - -from ._whenever import _common, utc - -__all__ = ["utc", "Option", "Some", "Nothing"] - -T = TypeVar("T") - - -class Option(Generic[T], ABC): - pass - - -Some = _common.Some -Nothing = _common.Nothing -Option.register(Some) -Option.register(Nothing) diff --git a/py/whenever/__init__.pyi b/py/whenever/__init__.pyi deleted file mode 100644 index 07cce3d4..00000000 --- a/py/whenever/__init__.pyi +++ /dev/null @@ -1,4 +0,0 @@ -from . import utc -from ._common import Nothing, Option, Some - -__all__ = ["utc", "Option", "Some", "Nothing"] diff --git a/py/whenever/_common.pyi b/py/whenever/_common.pyi deleted file mode 100644 index 7a18b3e8..00000000 --- a/py/whenever/_common.pyi +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Generic, TypeVar - -T = TypeVar("T") - -class Option(Generic[T]): - def unwrap(self) -> T: ... - -class Some(Option[T]): - __match_args__ = ("value",) - value: T - def __init__(self, value: T, /): ... - -class Nothing(Option[T]): - pass diff --git a/py/whenever/utc.pyi b/py/whenever/utc.pyi deleted file mode 100644 index ceb30b13..00000000 --- a/py/whenever/utc.pyi +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import datetime -from typing import Never - -from ._common import Option - -class DateTime: - def __init__( - self, _use_new_method_instead_of_constructor_: Never - ) -> None: ... - @staticmethod - def new( - year: int, - month: int, - day: int, - hour: int = 0, - min: int = 0, - sec: int = 0, - nano: int = 0, - /, - ) -> Option[DateTime]: ... - @staticmethod - def parse(s: str, /) -> Option[DateTime]: ... - def timestamp(self) -> float: ... - def timestamp_millis(self) -> float: ... - @staticmethod - def from_timestamp(i: int, /) -> Option[DateTime]: ... - @staticmethod - def from_timestamp_millis(i: int, /) -> Option[DateTime]: ... - @property - def year(self) -> int: ... - @property - def month(self) -> int: ... - @property - def day(self) -> int: ... - @property - def hour(self) -> int: ... - @property - def minute(self) -> int: ... - @property - def second(self) -> int: ... - @property - def nanosecond(self) -> int: ... - def __lt__(self, other: DateTime) -> bool: ... - def __le__(self, other: DateTime) -> bool: ... - def __gt__(self, other: DateTime) -> bool: ... - def __ge__(self, other: DateTime) -> bool: ... - def __hash__(self) -> int: ... - def to_py(self) -> datetime: ... diff --git a/pyproject.toml b/pyproject.toml index 4fa4837f..e28fcc88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,44 +1,44 @@ -[project] +[tool.poetry] name = "whenever" -description = "Typesafe datetimes powered by Rust's chrono library" -requires-python = ">=3.8" -authors = [ - {name = "Arie Bovenberg", email = "a.c.bovenberg@gmail.com"} -] +version = "0.0.4" +description = "Typesafe datetime for Python" +authors = ["Arie Bovenberg "] license = "MIT" classifiers = [ - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", "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", ] -dynamic = ["version"] +packages = [ + { include = "whenever", from = "src" }, +] +documentation = "https://whenever.readthedocs.io" +readme = "README.rst" +include = ["CHANGELOG.rst", "README.rst"] +repository = "https://github.com/ariebovenberg/whenever" +keywords = ["datetime"] -[project.urls] -Homepage = "https://whenever.rtfd.io" -Documentation = "https://whenever.rtfd.io" -Repository = "https://github.com/ariebovenberg/whenever.git" -Changelog = "https://github.com/ariebovenberg/whenever/blob/main/CHANGELOG.rst" +[tool.poetry.dependencies] +python = ">=3.8.1,<4.0" -[project.optional-dependencies] -test = [ - "pytest~=7.4", - "black~=23.3", - "isort~=5.12", - "pytest-mypy-plugins~=3.0", - "hypothesis~=6.0", - "pytest-benchmark~=4.0", -] +[tool.poetry.group.test.dependencies] +pytest = "^7.0.1" +pytest-cov = "^4.0.0" +pytest-benchmark = "^4.0.0" +pytest-mypy-plugins = "^3.0.0" +hypothesis = "^6.68.2" +[tool.poetry.group.typecheck.dependencies] +mypy = "^1.0.0" +pytest-mypy-plugins = "^3.0.0" -[tool.maturin] -python-source = "py" -module-name = "whenever._whenever" -features = ["pyo3/extension-module"] +[tool.poetry.group.linting.dependencies] +black = "^23.1" +flake8 = "^6.0.0" +isort = "^5.7.0" +slotscheck = "^0.17.0" [tool.black] line-length = 79 @@ -73,5 +73,5 @@ check_untyped_defs = true disable_error_code = ["no-untyped-def"] [build-system] -requires = ["maturin>=1.3,<2.0"] -build-backend = "maturin" +requires = ["poetry-core>=1.1.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/common.rs b/src/common.rs deleted file mode 100644 index 80b763de..00000000 --- a/src/common.rs +++ /dev/null @@ -1,89 +0,0 @@ -use pyo3::types::{PyAny, PyTuple, PyType}; -use pyo3::{intern, prelude::*}; - -#[pyclass(frozen, name = "Some", module = "whenever")] -pub struct PySome { - #[pyo3(get)] - pub value: PyObject, -} - -#[pymethods] -impl PySome { - #[new] - fn new(value: PyObject) -> Self { - PySome { value } - } - - fn unwrap(&self, py: Python) -> PyResult { - Ok(self.value.clone_ref(py)) - } - - pub fn __repr__(&self, py: Python) -> PyResult { - Ok(format!("Some({})", self.value.as_ref(py).repr()?,)) - } - - fn __eq__(&self, py: Python, rhs: &PySome) -> PyResult { - self.value.as_ref(py).eq(&rhs.value) - } - - fn __hash__(&self, py: Python) -> PyResult { - self.value.as_ref(py).hash() - } - - #[classmethod] - pub fn __class_getitem__(cls: &PyType, py: Python, _item: &PyAny) -> Py { - cls.into_py(py) - } - - #[classattr] - fn __match_args__(py: Python) -> &PyTuple { - PyTuple::new(py, vec![intern!(py, "value")]) - } -} - -#[pyclass(frozen, name = "Nothing", module = "whenever")] -pub struct PyNothing; - -#[pymethods] -impl PyNothing { - #[new] - fn new() -> Self { - // TODO: singleton? - PyNothing {} - } - - fn unwrap(&self) -> PyResult { - Err(pyo3::exceptions::PyValueError::new_err( - "called `unwrap` on a `Nothing` value", - )) - } - - fn __eq__(&self, _rhs: &PyNothing) -> bool { - true - } - - fn __hash__(&self) -> u64 { - 0 - } - - fn __bool__(&self) -> bool { - false - } - - pub fn __repr__(&self) -> &'static str { - "whenever.Nothing()" - } - - #[classmethod] - pub fn __class_getitem__(cls: &PyType, py: Python, _item: &PyAny) -> Py { - cls.into_py(py) - } -} - -pub fn submodule(py: Python) -> PyResult<&PyModule> { - let m = PyModule::new(py, "_common")?; - m.add("_NOTHING", (PyNothing {}).into_py(py))?; - m.add_class::()?; - m.add_class::()?; - Ok(m) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 124eef82..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -use pyo3::prelude::*; - -mod common; -mod utc; - -#[pymodule] -fn _whenever(py: Python, m: &PyModule) -> PyResult<()> { - let mod_utc = utc::submodule(py)?; - let mod_common = common::submodule(py)?; - - m.add_submodule(mod_utc)?; - m.add_submodule(mod_common)?; - - // See github.com/PyO3/pyo3/issues/759 - let sys_modules = py.import("sys")?.getattr("modules")?; - sys_modules.set_item("whenever.utc", mod_utc)?; - sys_modules.set_item("whenever._common", mod_common)?; - mod_utc.setattr("__name__", "whenever.utc")?; - mod_common.setattr("__name__", "whenever._common")?; - Ok(()) -} diff --git a/src/utc.rs b/src/utc.rs deleted file mode 100644 index 2c5b7d05..00000000 --- a/src/utc.rs +++ /dev/null @@ -1,215 +0,0 @@ -use chrono::{self, NaiveDateTime}; -use chrono::{Datelike, Timelike}; -use pyo3::prelude::*; -use pyo3::sync::GILOnceCell; -use pyo3::types::PyDateTime; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -use crate::common::{PyNothing, PySome}; - -/// Efficient UTC-only datetime -#[pyclass(frozen, module = "whenever.utc", weakref)] -struct DateTime { - inner: chrono::NaiveDateTime, -} - -#[pymethods] -impl DateTime { - #[new] - fn py_new() -> PyResult { - Err(pyo3::exceptions::PyTypeError::new_err( - "Cannot create a DateTime directly, use static methods like `new` instead", - )) - } - - /// Construct a DateTime from components - #[staticmethod] - #[pyo3(signature = (year, month, day, hour=0, min=0, sec=0, nano=0))] - fn new( - py: Python, - year: i32, - month: u32, - day: u32, - hour: u32, - min: u32, - sec: u32, - nano: u32, - ) -> PyObject { - match chrono::NaiveDate::from_ymd_opt(year, month, day) - .and_then(|d| d.and_hms_nano_opt(hour, min, sec, nano)) - { - Some(inner) => PySome { - value: DateTime { inner }.into_py(py), - } - .into_py(py), - None => (PyNothing {}).into_py(py), - } - } - - /// Parse a string in the format of ``YYYY-MM-DDTHH:MM:SS[.f]Z`` - #[staticmethod] - fn parse(py: Python, s: &str) -> PyObject { - match s.chars().last() { - Some('Z') => match s[..s.len() - 1].parse::() { - Ok(inner) => PySome { - value: DateTime { inner }.into_py(py), - } - .into_py(py), - _ => (PyNothing {}).into_py(py), - }, - _ => (PyNothing {}).into_py(py), - } - } - - /// Get the UNIX timestamp of this DateTime - fn timestamp(&self) -> i64 { - self.inner.timestamp() - } - - /// Construct a datetime from a UNIX timestamp - #[staticmethod] - fn from_timestamp(py: Python, timestamp: i64) -> PyObject { - match chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0) { - Some(inner) => PySome { - value: DateTime { inner }.into_py(py), - } - .into_py(py), - None => (PyNothing {}).into_py(py), - } - } - - /// Get the UNIX timestamp of this DateTime in milliseconds - fn timestamp_millis(&self) -> i64 { - self.inner.timestamp_millis() - } - - /// Construct a datetime from a UNIX timestamp in milliseconds - #[staticmethod] - fn from_timestamp_millis(py: Python, timestamp: i64) -> PyObject { - match chrono::NaiveDateTime::from_timestamp_opt( - timestamp / 1000, - ((timestamp % 1000) * 1_000_000) as u32, - ) { - Some(inner) => PySome { - value: DateTime { inner }.into_py(py), - } - .into_py(py), - None => (PyNothing {}).into_py(py), - } - } - - /// Convert this datetime to a Python :class:`datetime.datetime` object - fn to_py<'a>(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> { - PyDateTime::new( - py, - self.inner.year(), - self.inner.month() as u8, - self.inner.day() as u8, - self.inner.hour() as u8, - self.inner.minute() as u8, - self.inner.second() as u8, - self.inner.nanosecond() / 1_000, - None, - ) - } - - #[getter] - fn year(&self) -> i32 { - self.inner.year() - } - - #[getter] - fn month(&self) -> u32 { - self.inner.month() - } - - #[getter] - fn day(&self) -> u32 { - self.inner.day() - } - - #[getter] - fn hour(&self) -> u32 { - self.inner.hour() - } - - #[getter] - fn minute(&self) -> u32 { - self.inner.minute() - } - - #[getter] - fn second(&self) -> u32 { - self.inner.second() - } - - #[getter] - fn nanosecond(&self) -> u32 { - self.inner.nanosecond() - } - - fn __eq__(&self, rhs: &DateTime) -> bool { - self.inner == rhs.inner - } - - fn __lt__(&self, rhs: &DateTime) -> bool { - self.inner < rhs.inner - } - - fn __le__(&self, rhs: &DateTime) -> bool { - self.inner <= rhs.inner - } - - fn __gt__(&self, rhs: &DateTime) -> bool { - self.inner > rhs.inner - } - - fn __ge__(&self, rhs: &DateTime) -> bool { - self.inner >= rhs.inner - } - - fn __hash__(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.inner.hash(&mut hasher); - hasher.finish() - } - - fn __repr__(&self) -> String { - format!("whenever.utc.DateTime({:?}Z)", self.inner) - } - - fn __reduce__(&self, py: Python) -> PyResult<(PyObject, PyObject)> { - Ok(( - DATETIME_UNPICKLER.get(py).unwrap().clone(), - ( - self.inner.timestamp().to_object(py), - self.inner.timestamp_subsec_nanos().to_object(py), - ) - .to_object(py), - )) - } -} - -// Because the constructor cannot be called from Python, we have a custom unpickler. -// We give it a short name in Python because it contributes to the pickled size -#[pyfunction(name = "_ud")] -fn unpickle_datetime(secs: i64, nsecs: u32) -> DateTime { - DateTime { - inner: NaiveDateTime::from_timestamp_opt(secs, nsecs).unwrap(), - } -} - -static DATETIME_UNPICKLER: GILOnceCell = GILOnceCell::new(); - -pub fn submodule(py: Python<'_>) -> PyResult<&PyModule> { - let m = PyModule::new(py, "utc")?; - m.add_class::()?; - - let unpickle_func = wrap_pyfunction!(unpickle_datetime, m)?; - DATETIME_UNPICKLER.set(py, unpickle_func.into()).unwrap(); - unpickle_func.setattr("__module__", "whenever.utc")?; - m.add_function(unpickle_func)?; - - Ok(m) -} diff --git a/src/whenever/__init__.py b/src/whenever/__init__.py new file mode 100644 index 00000000..97208591 --- /dev/null +++ b/src/whenever/__init__.py @@ -0,0 +1,835 @@ +from __future__ import annotations + +import re +import sys +from datetime import date as _date +from datetime import datetime as _datetime +from datetime import time as _time +from datetime import timedelta, timezone, tzinfo +from operator import attrgetter +from typing import TYPE_CHECKING, ClassVar, overload + +try: + from typing import SPHINX_BUILD # type: ignore[attr-defined] +except ImportError: + SPHINX_BUILD = False + + +_UTC = timezone.utc + +__all__ = ["UTCDateTime", "NaiveDateTime"] + + +class UTCDateTime: + """A UTC-only datetime. Useful for representing location-indepentent + times en an unambiguous way. + + Example + ------- + + .. code-block:: python + + from whenever import UTCDateTime + py311_release_livestream = UTCDateTime(2022, 10, 24, hour=17) + """ + + __slots__ = ("_py_datetime", "__weakref__") + _py_datetime: _datetime + + def __init__( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + ) -> None: + self._py_datetime = _datetime( + year, month, day, hour, minute, second, microsecond, _UTC + ) + + @staticmethod + def now() -> UTCDateTime: + """Create a UTCDateTime from the current time + + Example + ------- + + .. code-block:: python + + now = UTCDateTime.now() + later = UTCDateTime.now() + assert later > now + + """ + self = _object_new(UTCDateTime) + self._py_datetime = _datetime_now(_UTC) + return self + + def __str__(self) -> str: + """Format a UTCDateTime as ``YYYY-MM-DDTHH:MM:SS[.ffffff]Z``. + This format is both RFC 3339 and ISO 8601 compliant. + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert str(d) == "2020-08-15T23:12:00Z" + + """ + return f"{self._py_datetime.isoformat()[:-6]}Z" + + @staticmethod + def fromstr(s: str, /) -> UTCDateTime: + """Create a UTCDateTime from ``YYYY-MM-DDTHH:MM:SS[.ffffff]Z``, + raising :class:`ValueError` if the string does not match this + exact format. The inverse of :meth:`__str__`. + + Example + ------- + + .. code-block:: python + + d = UTCDateTime.fromstr("2020-08-15T23:12:00Z") + assert d == UTCDateTime(2020, 8, 15, hour=23, minute=12) + + assert UTCDateTime.fromstr(str(d)) == d + + UTCDateTime.fromstr("2020-08-15T23:12") # raises ValueError + + """ + if not _match_utc_str(s): + raise ValueError("Invalid string") + self = _object_new(UTCDateTime) + self._py_datetime = _utc_fromisoformat(s) + return self + + if TYPE_CHECKING or SPHINX_BUILD: + + def timestamp(self) -> float: + """The UNIX timestamp. Inverse of :meth:`fromtimestamp`. + + Example + ------- + + .. code-block:: python + + assert UTCDateTime(1970, 1, 1).timestamp() == 0 + + ts = 1_123_000_000 + assert UTCDateTime.fromtimestamp(ts).timestamp() == ts + """ + ... + + else: + timestamp = property(attrgetter("_py_datetime.timestamp")) + + @staticmethod + def fromtimestamp(i: float, /) -> UTCDateTime: + """Create a UTCDateTime from a UNIX timestamp. + The inverse of :meth:`timestamp`. + + Example + ------- + + .. code-block:: python + + assert UTCDateTime.fromtimestamp(0) == UTCDateTime(1970, 1, 1) + d = UTCDateTime.fromtimestamp(1_123_000_000.45) + assert d == UTCDateTime(2004, 8, 2, 16, 26, 40, 450_000) + + assert UTCDateTime.fromtimestamp(d.timestamp()) == d + """ + self = _object_new(UTCDateTime) + self._py_datetime = _datetime.fromtimestamp(i, _UTC) + return self + + def to_py(self) -> _datetime: + """Get the underlying :class:`~datetime.datetime` object""" + return self._py_datetime + + @classmethod + def from_py(cls, d: _datetime, /) -> UTCDateTime: + """Create an instance from a :class:`~datetime.datetime` object. + It must be timezone-aware and have the exact :attr:`~datetime.UTC` + timezone. + + Inverse of :meth:`to_py`. + + Example + ------- + + .. code-block:: python + + from datetime import datetime, UTC + d = datetime(2020, 8, 15, hour=23, tzinfo=UTC) + + UTCDateTime.from_py(d) == UTCDateTime(2020, 8, 15, hour=23) + + UTCDateTime.from_py(datetime(2020, 8, 15, hour=23)) # ValueError + """ + if d.tzinfo is not _UTC: + raise ValueError( + "Can only create UTCDateTime from UTC datetime, " + f"got datetime with tzinfo={d.tzinfo!r}" + ) + self = _object_new(UTCDateTime) + self._py_datetime = d + return self + + def tz(self) -> tzinfo: + """The timezone, always :attr:`~datetime.UTC`""" + return _UTC + + if TYPE_CHECKING: + # We could have used typing.Unpack, but that's only available + # in Python 3.11+ or with typing_extensions. + def replace( + self, + *, + year: int | None = None, + month: int | None = None, + day: int | None = None, + hour: int | None = None, + minute: int | None = None, + second: int | None = None, + microsecond: int | None = None, + ) -> UTCDateTime: + ... + + else: + + def replace(self, /, **kwargs) -> UTCDateTime: + """Create a new UTCDateTime with the given fields replaced + + Example + ------- + + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, 23, 12) + assert d.replace(year=2021) == UTCDateTime(2021, 8, 15, 23, 12) + """ + if not _no_tzinfo_or_fold(kwargs): + raise TypeError("tzinfo and fold are not allowed arguments") + new = _object_new(UTCDateTime) + new._py_datetime = self._py_datetime.replace(**kwargs) + return new + + if TYPE_CHECKING or SPHINX_BUILD: + + @property + def year(self) -> int: + """The year""" + ... + + @property + def month(self) -> int: + """The month""" + ... + + @property + def day(self) -> int: + """The day""" + ... + + @property + def hour(self) -> int: + """The hour""" + ... + + @property + def minute(self) -> int: + """The minute""" + ... + + @property + def second(self) -> int: + """The second""" + ... + + @property + def microsecond(self) -> int: + """The microsecond""" + ... + + def weekday(self) -> int: + """The day of the week as an integer (Monday=0, Sunday=6) + + Example + ------- + + .. code-block:: python + + assert UTCDateTime(2022, 10, 25).weekday() == 1 + """ + ... + + def date(self) -> _date: + """The :class:`~datetime.date` part of the datetime + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d.date() == date(2020, 8, 15) + + """ + ... + + def time(self) -> _time: + """The :class:`~datetime.time` part of the datetime + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d.time() == time(23, 12) + """ + ... + + def __hash__(self) -> int: + ... + + else: + # Defining properties this way is faster than declaring a `def`, + # but the type checker doesn't like it. + year = property(attrgetter("_py_datetime.year")) + month = property(attrgetter("_py_datetime.month")) + day = property(attrgetter("_py_datetime.day")) + hour = property(attrgetter("_py_datetime.hour")) + minute = property(attrgetter("_py_datetime.minute")) + second = property(attrgetter("_py_datetime.second")) + microsecond = property(attrgetter("_py_datetime.microsecond")) + weekday = property(attrgetter("_py_datetime.weekday")) + date = property(attrgetter("_py_datetime.date")) + time = property(attrgetter("_py_datetime.time")) + __hash__ = property(attrgetter("_py_datetime.__hash__")) + + resolution: ClassVar[timedelta] = _datetime.resolution + """Alias for :attr:`datetime.datetime.resolution`""" + min: ClassVar[UTCDateTime] + """Small possible value""" + max: ClassVar[UTCDateTime] + """Biggest possible value""" + + # This ensures mypy's --strict-equalty works + if not TYPE_CHECKING: # pragma: no branch + + def __eq__(self, other: UTCDateTime) -> bool: + """Compare two objects + + Example + ------- + .. code-block:: python + + assert UTCDateTime(2020, 8, 15, 23) == UTCDateTime(2020, 8, 15, 23) + + """ # noqa: E501 + if not isinstance(other, UTCDateTime): + return NotImplemented + return self._py_datetime == other._py_datetime + + def __lt__(self, other: UTCDateTime) -> bool: + """Compare two objects + + Example + ------- + .. code-block:: python + + assert UTCDateTime(2020, 8, 15, hour=23) < UTCDateTime(2020, 8, 16) + + """ + if not isinstance(other, UTCDateTime): + return NotImplemented + return self._py_datetime < other._py_datetime + + def __le__(self, other: UTCDateTime) -> bool: + if not isinstance(other, UTCDateTime): + return NotImplemented + return self._py_datetime <= other._py_datetime + + def __gt__(self, other: UTCDateTime) -> bool: + if not isinstance(other, UTCDateTime): + return NotImplemented + return self._py_datetime > other._py_datetime + + def __ge__(self, other: UTCDateTime) -> bool: + if not isinstance(other, UTCDateTime): + return NotImplemented + return self._py_datetime >= other._py_datetime + + def __add__(self, other: timedelta) -> UTCDateTime: + """Add a timedelta to this datetime + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d + timedelta(days=1, seconds=5) == UTCDateTime( + 2020, 8, 16, hour=23, minute=12, second=5 + ) + """ + if not isinstance(other, timedelta): + return NotImplemented + new = _object_new(UTCDateTime) + new._py_datetime = self._py_datetime + other + return new + + if TYPE_CHECKING: + + @overload + def __sub__(self, other: UTCDateTime) -> timedelta: + ... + + @overload + def __sub__(self, other: timedelta) -> UTCDateTime: + ... + + def __sub__( + self, other: UTCDateTime | timedelta + ) -> UTCDateTime | timedelta: + ... + + else: + + def __sub__( + self, other: timedelta | UTCDateTime + ) -> UTCDateTime | timedelta: + """Subtract another datetime or timedelta + + Example + ------- + + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d - timedelta(days=1, seconds=5) == UTCDateTime( + 2020, 8, 14, hour=23, minute=11, second=55 + ) + + assert d - UTCDateTime(2020, 8, 14) > timedelta(days=1) + """ + if isinstance(other, UTCDateTime): + return self._py_datetime - other._py_datetime + elif isinstance(other, timedelta): + new = _object_new(UTCDateTime) + new._py_datetime = self._py_datetime - other + return new + return NotImplemented + + def __repr__(self) -> str: + return f"whenever.UTCDateTime({self})" + + # a custom pickle implementation with a smaller payload + def __reduce__(self) -> tuple[object, ...]: + return ( + UTCDateTime, + self._py_datetime.timetuple()[:6] + (self.microsecond,), + ) + + +class NaiveDateTime: + """A naive datetime. Useful when you need date and time, but without + any of the real-world complexities of timeszones.""" + + def __init__( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + ) -> None: + self._py_datetime = _datetime( + year, month, day, hour, minute, second, microsecond + ) + + if TYPE_CHECKING or SPHINX_BUILD: + + def __str__(self) -> str: + """Format a NaiveDateTime as ``YYYY-MM-DDTHH:MM:SS[.ffffff]``. + Inverse of :meth:`fromstr`. + + This format is ISO 8601 compliant, but not RFC 3339 compliant, + as this requires a UTC offset + + Example + ------- + + .. code-block:: python + + d = NaiveDateTime(2020, 8, 15, hour=23, minute=12) + assert str(d) == "2020-08-15T23:12:00" + + assert NaiveDateTime.fromstr(str(d)) == d + + """ + ... + + else: + __str__ = property(attrgetter("_py_datetime.isoformat")) + + @staticmethod + def fromstr(s: str, /) -> NaiveDateTime: + """Create an instance from ``YYYY-MM-DDTHH:MM:SS[.ffffff]``, + raising :class:`ValueError` if the string does not match this + exact format. The inverse of :meth:`__str__`. + + Example + ------- + + .. code-block:: python + + d = NaiveDateTime.fromstr("2020-08-15T23:12:00") + assert d == NaiveDateTime(2020, 8, 15, hour=23, minute=12) + + assert NaiveDateTime.fromstr(str(d)) == d + NaiveDateTime.fromstr("2020-08-15T23:12") # raises ValueError + + """ + if not _match_naive_str(s): + raise ValueError("Invalid string") + self = _object_new(NaiveDateTime) + self._py_datetime = _naive_fromisoformat(s) + return self + + def to_py(self) -> _datetime: + """Get the underlying :class:`~datetime.datetime` object. + Inverse of :meth:`from_py`. + + Example + ------- + .. code-block:: python + + d = NaiveDateTime(2020, 8, 15, hour=23, minute=12) + assert d.to_py() == datetime(2020, 8, 15, hour=23, minute=12) + + assert NaiveDateTime.from_py(d.to_py()) == d + """ + return self._py_datetime + + @classmethod + def from_py(cls, d: _datetime, /) -> NaiveDateTime: + """Create an instance from a :class:`~datetime.datetime` object. + It must be timezone-naive. Inverse of :meth:`to_py`. + + Example + ------- + .. code-block:: python + + from datetime import datetime + d = datetime(2020, 8, 15, hour=23) + + NaiveDateTime.from_py(d) == NaiveDateTime(2020, 8, 15, hour=23) + + # ValueError + NaiveDateTime.from_py(datetime(2020, 8, 15, hour=23, tzinfo=UTC)) + """ + if d.tzinfo is not None: + raise ValueError( + "Can only create NaiveDateTime from a naive datetime, " + f"got datetime with tzinfo={d.tzinfo!r}" + ) + self = _object_new(NaiveDateTime) + self._py_datetime = d + return self + + def tz(self) -> tzinfo | None: + """The timezone, always :obj:`None`""" + return None + + if TYPE_CHECKING: + # We could have used typing.Unpack, but that's only available + # in Python 3.11+ or with typing_extensions. + def replace( + self, + *, + year: int | None = None, + month: int | None = None, + day: int | None = None, + hour: int | None = None, + minute: int | None = None, + second: int | None = None, + microsecond: int | None = None, + ) -> NaiveDateTime: + ... + + else: + + def replace(self, /, **kwargs) -> NaiveDateTime: + """Create a new datetime with the given fields replaced + + Example + ------- + + .. code-block:: python + + d = NaiveDateTime(2020, 8, 15, 23, 12) + assert d.replace(year=2021) == NaiveDateTime(2021, 8, 15, 23, 12) + """ + if not _no_tzinfo_or_fold(kwargs): + raise TypeError("tzinfo and fold are not allowed arguments") + new = _object_new(NaiveDateTime) + new._py_datetime = self._py_datetime.replace(**kwargs) + return new + + if TYPE_CHECKING or SPHINX_BUILD: + + @property + def year(self) -> int: + """The year""" + ... + + @property + def month(self) -> int: + """The month""" + ... + + @property + def day(self) -> int: + """The day""" + ... + + @property + def hour(self) -> int: + """The hour""" + ... + + @property + def minute(self) -> int: + """The minute""" + ... + + @property + def second(self) -> int: + """The second""" + ... + + @property + def microsecond(self) -> int: + """The microsecond""" + ... + + def weekday(self) -> int: + """The day of the week as an integer (Monday=0, Sunday=6) + + Example + ------- + + .. code-block:: python + + assert UTCDateTime(2022, 10, 25).weekday() == 1 + """ + ... + + def date(self) -> _date: + """The :class:`~datetime.date` part of the datetime + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d.date() == date(2020, 8, 15) + + """ + ... + + def time(self) -> _time: + """The :class:`~datetime.time` part of the datetime + + Example + ------- + .. code-block:: python + + d = UTCDateTime(2020, 8, 15, hour=23, minute=12) + assert d.time() == time(23, 12) + """ + ... + + def __hash__(self) -> int: + ... + + else: + # Defining properties this way is faster than declaring a `def`, + # but the type checker doesn't like it. + year = property(attrgetter("_py_datetime.year")) + month = property(attrgetter("_py_datetime.month")) + day = property(attrgetter("_py_datetime.day")) + hour = property(attrgetter("_py_datetime.hour")) + minute = property(attrgetter("_py_datetime.minute")) + second = property(attrgetter("_py_datetime.second")) + microsecond = property(attrgetter("_py_datetime.microsecond")) + weekday = property(attrgetter("_py_datetime.weekday")) + date = property(attrgetter("_py_datetime.date")) + time = property(attrgetter("_py_datetime.time")) + __hash__ = property(attrgetter("_py_datetime.__hash__")) + + resolution: ClassVar[timedelta] = _datetime.resolution + """Alias for :attr:`datetime.datetime.resolution`""" + min: ClassVar[NaiveDateTime] + """Small possible value""" + max: ClassVar[NaiveDateTime] + """Biggest possible value""" + + # This ensures mypy's --strict-equalty works + if not TYPE_CHECKING: # pragma: no branch + + def __eq__(self, other: NaiveDateTime) -> bool: + """Compare two objects + + Example + ------- + .. code-block:: python + + assert NaiveDateTime(2020, 8, 15, 23) == NaiveDateTime(2020, 8, 15, 23) + + """ # noqa: E501 + if not isinstance(other, NaiveDateTime): + return NotImplemented + return self._py_datetime == other._py_datetime + + def __lt__(self, other: NaiveDateTime) -> bool: + """Compare two objects + + Example + ------- + .. code-block:: python + + assert NaiveDateTime(2020, 8, 15, hour=23) < NaiveDateTime(2020, 8, 16) + + """ # noqa: E501 + if not isinstance(other, NaiveDateTime): + return NotImplemented + return self._py_datetime < other._py_datetime + + def __le__(self, other: NaiveDateTime) -> bool: + if not isinstance(other, NaiveDateTime): + return NotImplemented + return self._py_datetime <= other._py_datetime + + def __gt__(self, other: NaiveDateTime) -> bool: + if not isinstance(other, NaiveDateTime): + return NotImplemented + return self._py_datetime > other._py_datetime + + def __ge__(self, other: NaiveDateTime) -> bool: + if not isinstance(other, NaiveDateTime): + return NotImplemented + return self._py_datetime >= other._py_datetime + + def __add__(self, other: timedelta) -> NaiveDateTime: + """Add a timedelta to this datetime + + Example + ------- + .. code-block:: python + + d = NaiveDateTime(2020, 8, 15, hour=23, minute=12) + assert d + timedelta(days=1, seconds=5) == NaiveDateTime( + 2020, 8, 16, hour=23, minute=12, second=5 + ) + """ + if not isinstance(other, timedelta): + return NotImplemented + new = _object_new(NaiveDateTime) + new._py_datetime = self._py_datetime + other + return new + + if TYPE_CHECKING: + + @overload + def __sub__(self, other: NaiveDateTime) -> timedelta: + ... + + @overload + def __sub__(self, other: timedelta) -> NaiveDateTime: + ... + + def __sub__( + self, other: NaiveDateTime | timedelta + ) -> NaiveDateTime | timedelta: + ... + + else: + + def __sub__( + self, other: timedelta | NaiveDateTime + ) -> NaiveDateTime | timedelta: + """Subtract another datetime or timedelta + + Example + ------- + + .. code-block:: python + + d = NaiveDateTime(2020, 8, 15, hour=23, minute=12) + assert d - timedelta(days=1, seconds=5) == NaiveDateTime( + 2020, 8, 14, hour=23, minute=11, second=55 + ) + + assert d - NaiveDateTime(2020, 8, 14) > timedelta(days=1) + """ + if isinstance(other, NaiveDateTime): + return self._py_datetime - other._py_datetime + elif isinstance(other, timedelta): + new = _object_new(NaiveDateTime) + new._py_datetime = self._py_datetime - other + return new + return NotImplemented + + def __repr__(self) -> str: + return f"whenever.NaiveDateTime({self})" + + # a custom pickle implementation with a smaller payload + def __reduce__(self) -> tuple[object, ...]: + return ( + NaiveDateTime, + self._py_datetime.timetuple()[:6] + (self.microsecond,), + ) + + +# Helpers that pre-compute/lookup as much as possible +_no_tzinfo_or_fold = {"tzinfo", "fold"}.isdisjoint +_datetime_now = _datetime.now +_object_new = object.__new__ +_match_utc_str = re.compile( + r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?Z" +).fullmatch +_match_naive_str = re.compile( + r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?" +).fullmatch + + +# Before Python 3.11, fromisoformat() is very particular +if sys.version_info > (3, 11): # pragma: no cover + _utc_fromisoformat = _naive_fromisoformat = _datetime.fromisoformat +else: # pragma: no cover + + def _utc_fromisoformat(s: str, /) -> _datetime: + return ( + _datetime.fromisoformat( + # Remove trailing Z, and ensure exactly 0 ot 6 ms digits + s[:19] + if len(s) == 20 + else s[:-1].ljust(26, "0") + ) + ).replace(tzinfo=_UTC) + + def _naive_fromisoformat(s: str, /) -> _datetime: + return _datetime.fromisoformat(s if len(s) == 19 else s.ljust(26, "0")) + + +UTCDateTime.min = UTCDateTime.from_py(_datetime.min.replace(tzinfo=_UTC)) +UTCDateTime.max = UTCDateTime.from_py(_datetime.max.replace(tzinfo=_UTC)) +NaiveDateTime.min = NaiveDateTime.from_py(_datetime.min) +NaiveDateTime.max = NaiveDateTime.from_py(_datetime.max) diff --git a/py/whenever/py.typed b/src/whenever/py.typed similarity index 100% rename from py/whenever/py.typed rename to src/whenever/py.typed diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 00000000..2fc77cfe --- /dev/null +++ b/tests/common.py @@ -0,0 +1,36 @@ +class AlwaysEqual: + def __eq__(self, other): + return True + + +class NeverEqual: + def __eq__(self, other): + return False + + +class AlwaysLarger: + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True + + +class AlwaysSmaller: + def __lt__(self, other): + return True + + def __le__(self, other): + return True + + def __gt__(self, other): + return False + + def __ge__(self, other): + return False diff --git a/tests/test_common.py b/tests/test_common.py deleted file mode 100644 index 0cd86852..00000000 --- a/tests/test_common.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest - -from whenever import Nothing, Option, Some - - -class TestSome: - def test_unwrap(self): - assert Some(42).unwrap() == 42 - assert Some(42).value == 42 - - def test_equality(self): - x: Option[int] = Some(42) - same: Option[int] = Some(42) - different: Option[int] = Some(43) - nothing: Option[int] = Nothing() - - assert x == same - assert not x == different - assert not x == nothing - assert not x == 42 # type: ignore[comparison-overlap] - - assert x != different - assert not x != same - assert x != 42 # type: ignore[comparison-overlap] - assert x != nothing - - assert hash(x) == hash(same) - assert hash(x) != hash(different) - - def test_match(self): - s = Some(42) - match s: - case Some(v): - assert v == 42 - case Nothing(): - assert False - case _: - assert False - - def test_generic(self): - s = Some[int](42) - assert s == Some(42) - - def test_inheritance(self): - s = Some(42) - assert isinstance(s, Some) - assert isinstance(s, Option) - assert not isinstance(s, Nothing) - - def test_bool(self): - assert bool(Some(42)) - assert bool(Some(0)) - - -class TestNothing: - def test_unwrap(self): - with pytest.raises(ValueError): - Nothing().unwrap() - - def test_equality(self): - x: Option[int] = Nothing() - same: Option[int] = Nothing() - - assert x == same - assert not x == 42 # type: ignore[comparison-overlap] - - assert x != 42 # type: ignore[comparison-overlap] - assert not x != same - - assert hash(x) == hash(same) - assert hash(x) != hash(42) - - def test_inheritance(self): - n = Nothing[int]() - assert isinstance(n, Nothing) - assert isinstance(n, Option) - assert not isinstance(n, Some) - - def test_repr(self): - assert repr(Nothing()) == "whenever.Nothing()" - - def test_bool(self): - assert not bool(Nothing()) - - def test_match(self): - n = Nothing[str]() - match n: - case Some(_): - assert False - case Nothing(): - assert True - case _: - assert False - - def test_generic(self): - n = Nothing[int]() - assert n == Nothing() - - def test_no_other_attributes(self): - n = Nothing[int]() - with pytest.raises(AttributeError): - n.foo = 4 # type: ignore[attr-defined] diff --git a/tests/test_naive_datetime.py b/tests/test_naive_datetime.py new file mode 100644 index 00000000..f3f9dd5d --- /dev/null +++ b/tests/test_naive_datetime.py @@ -0,0 +1,236 @@ +import pickle +import weakref +from datetime import datetime as py_datetime +from datetime import timedelta, timezone + +import pytest +from hypothesis import given +from hypothesis.strategies import text + +from whenever import NaiveDateTime + +from .common import AlwaysEqual, AlwaysLarger, AlwaysSmaller, NeverEqual + + +def test_minimal(): + d = NaiveDateTime(2020, 8, 15, 5, 12, 30, 450) + + assert d.year == 2020 + assert d.month == 8 + assert d.day == 15 + assert d.hour == 5 + assert d.minute == 12 + assert d.second == 30 + assert d.microsecond == 450 + + assert ( + NaiveDateTime(2020, 8, 15, 12) + == NaiveDateTime(2020, 8, 15, 12, 0) + == NaiveDateTime(2020, 8, 15, 12, 0, 0) + == NaiveDateTime(2020, 8, 15, 12, 0, 0, 0) + ) + + +def test_immutable(): + d = NaiveDateTime(2020, 8, 15) + with pytest.raises(AttributeError): + d.year = 2021 # type: ignore[misc] + + +class TestFromStr: + def test_valid(self): + assert NaiveDateTime.fromstr("2020-08-15T12:08:30") == NaiveDateTime( + 2020, 8, 15, 12, 8, 30 + ) + + def test_valid_fraction(self): + assert NaiveDateTime.fromstr( + "2020-08-15T12:08:30.34" + ) == NaiveDateTime(2020, 8, 15, 12, 8, 30, 340_000) + + def test_unpadded(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("2020-8-15T12:8:30") + + # TODO: more comprehensive tests + + def test_overly_precise_fraction(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("2020-08-15T12:08:30.123456789123") + + def test_trailing_z(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("2020-08-15T12:08:30Z") + + def test_no_seconds(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("2020-08-15T12:08") + + def test_empty(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("") + + def test_garbage(self): + with pytest.raises(ValueError): + NaiveDateTime.fromstr("garbage") + + @given(text()) + def test_fuzzing(self, s: str): + with pytest.raises(ValueError, match="Invalid"): + NaiveDateTime.fromstr(s) + + +def test_equality(): + d = NaiveDateTime(2020, 8, 15) + different = NaiveDateTime(2020, 8, 16) + same = NaiveDateTime(2020, 8, 15) + assert d == same + assert d != different + assert not d == different + assert not d != same + + assert hash(d) == hash(same) + assert hash(d) != hash(different) + + assert d == AlwaysEqual() + assert d != NeverEqual() + assert not d == NeverEqual() + assert not d != AlwaysEqual() + + assert d != 42 # type: ignore[comparison-overlap] + assert not d == 42 # type: ignore[comparison-overlap] + + assert NaiveDateTime(2020, 8, 15, 12, 8, 30) != NaiveDateTime( + 2020, 8, 15, 12, 8, 31 + ) + + +def test_repr(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert repr(d) == "whenever.NaiveDateTime(2020-08-15T23:12:09.987654)" + assert ( + repr(NaiveDateTime(2020, 8, 15, 23, 12)) + == "whenever.NaiveDateTime(2020-08-15T23:12:00)" + ) + + +def test_str(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert str(d) == "2020-08-15T23:12:09.987654" + + +def test_comparison(): + d = NaiveDateTime.fromstr("2020-08-15T23:12:09") + later = NaiveDateTime.fromstr("2020-08-16T00:00:00") + assert d < later + assert d <= later + assert later > d + assert later >= d + + assert d < AlwaysLarger() + assert d <= AlwaysLarger() + assert not d > AlwaysLarger() + assert not d >= AlwaysLarger() + assert not d < AlwaysSmaller() + assert not d <= AlwaysSmaller() + assert d > AlwaysSmaller() + assert d >= AlwaysSmaller() + + with pytest.raises(TypeError): + d < 42 # type: ignore[operator] + + +def test_to_py(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d.to_py() == py_datetime(2020, 8, 15, 23, 12, 9, 987_654) + + +def test_from_py(): + d = py_datetime(2020, 8, 15, 23, 12, 9, 987_654) + assert NaiveDateTime.from_py(d) == NaiveDateTime( + 2020, 8, 15, 23, 12, 9, 987_654 + ) + + with pytest.raises(ValueError, match="utc"): + NaiveDateTime.from_py( + py_datetime(2020, 8, 15, 23, 12, 9, 987_654, tzinfo=timezone.utc) + ) + + +def test_weakref(): + d = NaiveDateTime(2020, 8, 15) + ref = weakref.ref(d) + assert ref() == d + + +def test_min_max(): + assert NaiveDateTime.min == NaiveDateTime(1, 1, 1) + assert NaiveDateTime.max == NaiveDateTime( + 9999, 12, 31, 23, 59, 59, 999_999 + ) + + +def test_passthrough_datetime_attrs(): + d = NaiveDateTime(2020, 8, 15) + assert d.resolution == py_datetime.resolution + assert d.weekday() == d._py_datetime.weekday() + assert d.date() == d._py_datetime.date() + + assert d.tz() is None + + +def test_replace(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d.replace(year=2021) == NaiveDateTime( + 2021, 8, 15, 23, 12, 9, 987_654 + ) + assert d.replace(month=9) == NaiveDateTime(2020, 9, 15, 23, 12, 9, 987_654) + assert d.replace(day=16) == NaiveDateTime(2020, 8, 16, 23, 12, 9, 987_654) + assert d.replace(hour=0) == NaiveDateTime(2020, 8, 15, 0, 12, 9, 987_654) + assert d.replace(minute=0) == NaiveDateTime(2020, 8, 15, 23, 0, 9, 987_654) + assert d.replace(second=0) == NaiveDateTime( + 2020, 8, 15, 23, 12, 0, 987_654 + ) + assert d.replace(microsecond=0) == NaiveDateTime(2020, 8, 15, 23, 12, 9, 0) + + with pytest.raises(TypeError, match="tzinfo"): + d.replace(tzinfo=timezone.utc) # type: ignore[call-arg] + + +def test_add(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d + timedelta(days=1, seconds=5) == NaiveDateTime( + 2020, 8, 16, 23, 12, 14, 987_654 + ) + + +def test_add_invalid(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + with pytest.raises(TypeError, match="unsupported operand type"): + d + 42 # type: ignore[operator] + + +def test_sub(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d - timedelta(days=1, seconds=5) == NaiveDateTime( + 2020, 8, 14, 23, 12, 4, 987_654 + ) + + +def test_subtract_datetime(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + other = NaiveDateTime(2020, 8, 14, 23, 12, 4, 987_654) + assert d - other == timedelta(days=1, seconds=5) + + +def test_subtract_invalid(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + with pytest.raises(TypeError, match="unsupported operand type"): + d - 42 # type: ignore[operator] + + +def test_pickle(): + d = NaiveDateTime(2020, 8, 15, 23, 12, 9, 987_654) + dumped = pickle.dumps(d) + assert len(dumped) <= len(pickle.dumps(d.to_py())) + 15 + assert pickle.loads(pickle.dumps(d)) == d diff --git a/tests/test_utc_datetime.py b/tests/test_utc_datetime.py new file mode 100644 index 00000000..204c6667 --- /dev/null +++ b/tests/test_utc_datetime.py @@ -0,0 +1,271 @@ +import pickle +import weakref +from datetime import datetime as py_datetime +from datetime import timedelta, timezone + +import pytest +from hypothesis import given +from hypothesis.strategies import text +from pytest import approx + +from whenever import UTCDateTime + +from .common import AlwaysEqual, AlwaysLarger, AlwaysSmaller, NeverEqual + + +def test_init_and_attributes(): + d = UTCDateTime(2020, 8, 15, 5, 12, 30, 450) + + assert d.year == 2020 + assert d.month == 8 + assert d.day == 15 + assert d.hour == 5 + assert d.minute == 12 + assert d.second == 30 + assert d.microsecond == 450 + + +def test_init_optionality(): + assert ( + UTCDateTime(2020, 8, 15, 12) + == UTCDateTime(2020, 8, 15, 12, 0) + == UTCDateTime(2020, 8, 15, 12, 0, 0) + == UTCDateTime(2020, 8, 15, 12, 0, 0, 0) + ) + + +def test_init_invalid(): + with pytest.raises(ValueError, match="microsecond must"): + UTCDateTime(2020, 8, 15, 12, 8, 30, 1_000_000) + + +def test_init_named(): + d = UTCDateTime(year=2020, month=8, day=15, hour=5, minute=12, second=30) + assert d == UTCDateTime(2020, 8, 15, 5, 12, 30) + + +def test_immutable(): + d = UTCDateTime(2020, 8, 15) + with pytest.raises(AttributeError): + d.year = 2021 # type: ignore[misc] + + +class TestFromStr: + def test_valid(self): + assert UTCDateTime.fromstr("2020-08-15T12:08:30Z") == UTCDateTime( + 2020, 8, 15, 12, 8, 30 + ) + + def test_valid_fraction(self): + assert UTCDateTime.fromstr("2020-08-15T12:08:30.34Z") == UTCDateTime( + 2020, 8, 15, 12, 8, 30, 340_000 + ) + + def test_unpadded(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("2020-8-15T12:8:30Z") + + # TODO: more comprehensive tests + + def test_overly_precise_fraction(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("2020-08-15T12:08:30.123456789123Z") + + def test_invalid_lowercase_z(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("2020-08-15T12:08:30z") + + def test_no_trailing_z(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("2020-08-15T12:08:30") + + def test_no_seconds(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("2020-08-15T12:08Z") + + def test_empty(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("") + + def test_garbage(self): + with pytest.raises(ValueError): + UTCDateTime.fromstr("garbage") + + @given(text()) + def test_fuzzing(self, s: str): + with pytest.raises(ValueError, match="Invalid"): + UTCDateTime.fromstr(s) + + +def test_equality(): + d = UTCDateTime(2020, 8, 15) + different = UTCDateTime(2020, 8, 16) + same = UTCDateTime(2020, 8, 15) + assert d == same + assert d != different + assert not d == different + assert not d != same + + assert hash(d) == hash(same) + assert hash(d) != hash(different) + + assert d == AlwaysEqual() + assert d != NeverEqual() + assert not d == NeverEqual() + assert not d != AlwaysEqual() + + assert d != 42 # type: ignore[comparison-overlap] + assert not d == 42 # type: ignore[comparison-overlap] + + assert UTCDateTime(2020, 8, 15, 12, 8, 30) != UTCDateTime( + 2020, 8, 15, 12, 8, 31 + ) + + +def test_timestamp(): + assert UTCDateTime(1970, 1, 1).timestamp() == 0 + assert UTCDateTime(2020, 8, 15, 12, 8, 30, 45).timestamp() == approx( + 1_597_493_310.000045, abs=1e-6 + ) + + +def test_fromtimestamp(): + assert UTCDateTime.fromtimestamp(0) == UTCDateTime(1970, 1, 1) + assert UTCDateTime.fromtimestamp(1_597_493_310) == UTCDateTime( + 2020, 8, 15, 12, 8, 30 + ) + with pytest.raises((OSError, OverflowError)): + UTCDateTime.fromtimestamp(1_000_000_000_000_000_000) + + +def test_repr(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert repr(d) == "whenever.UTCDateTime(2020-08-15T23:12:09.987654Z)" + assert ( + repr(UTCDateTime(2020, 8, 15, 23, 12)) + == "whenever.UTCDateTime(2020-08-15T23:12:00Z)" + ) + + +def test_str(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert str(d) == "2020-08-15T23:12:09.987654Z" + + +def test_comparison(): + d = UTCDateTime.fromstr("2020-08-15T23:12:09Z") + later = UTCDateTime.fromstr("2020-08-16T00:00:00Z") + assert d < later + assert d <= later + assert later > d + assert later >= d + + assert d < AlwaysLarger() + assert d <= AlwaysLarger() + assert not d > AlwaysLarger() + assert not d >= AlwaysLarger() + assert not d < AlwaysSmaller() + assert not d <= AlwaysSmaller() + assert d > AlwaysSmaller() + assert d >= AlwaysSmaller() + + with pytest.raises(TypeError): + d < 42 # type: ignore[operator] + + +def test_to_py(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d.to_py() == py_datetime( + 2020, 8, 15, 23, 12, 9, 987_654, tzinfo=timezone.utc + ) + + +def test_from_py(): + d = py_datetime(2020, 8, 15, 23, 12, 9, 987_654, tzinfo=timezone.utc) + assert UTCDateTime.from_py(d) == UTCDateTime( + 2020, 8, 15, 23, 12, 9, 987_654 + ) + + with pytest.raises(ValueError, match="UTC.*timedelta"): + UTCDateTime.from_py(d.replace(tzinfo=timezone(-timedelta(hours=4)))) + + +def test_now(): + now = UTCDateTime.now() + py_now = py_datetime.now(timezone.utc) + assert py_now - now.to_py() < timedelta(seconds=1) + + +def test_weakref(): + d = UTCDateTime(2020, 8, 15) + ref = weakref.ref(d) + assert ref() == d + + +def test_min_max(): + assert UTCDateTime.min == UTCDateTime(1, 1, 1) + assert UTCDateTime.max == UTCDateTime(9999, 12, 31, 23, 59, 59, 999_999) + + +def test_passthrough_datetime_attrs(): + d = UTCDateTime(2020, 8, 15, 12, 43) + assert d.resolution == py_datetime.resolution + assert d.weekday() == d._py_datetime.weekday() + assert d.date() == d._py_datetime.date() + time = d.time() + assert time.tzinfo is None + assert time == d._py_datetime.time() + assert d.tz() == d._py_datetime.tzinfo == timezone.utc + + +def test_replace(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d.replace(year=2021) == UTCDateTime(2021, 8, 15, 23, 12, 9, 987_654) + assert d.replace(month=9) == UTCDateTime(2020, 9, 15, 23, 12, 9, 987_654) + assert d.replace(day=16) == UTCDateTime(2020, 8, 16, 23, 12, 9, 987_654) + assert d.replace(hour=0) == UTCDateTime(2020, 8, 15, 0, 12, 9, 987_654) + assert d.replace(minute=0) == UTCDateTime(2020, 8, 15, 23, 0, 9, 987_654) + assert d.replace(second=0) == UTCDateTime(2020, 8, 15, 23, 12, 0, 987_654) + assert d.replace(microsecond=0) == UTCDateTime(2020, 8, 15, 23, 12, 9, 0) + + with pytest.raises(TypeError, match="tzinfo"): + d.replace(tzinfo=timezone.utc) # type: ignore[call-arg] + + +def test_add(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d + timedelta(days=1, seconds=5) == UTCDateTime( + 2020, 8, 16, 23, 12, 14, 987_654 + ) + + +def test_add_invalid(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + with pytest.raises(TypeError, match="unsupported operand type"): + d + 42 # type: ignore[operator] + + +def test_sub(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + assert d - timedelta(days=1, seconds=5) == UTCDateTime( + 2020, 8, 14, 23, 12, 4, 987_654 + ) + + +def test_subtract_datetime(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + other = UTCDateTime(2020, 8, 14, 23, 12, 4, 987_654) + assert d - other == timedelta(days=1, seconds=5) + + +def test_subtract_invalid(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + with pytest.raises(TypeError, match="unsupported operand type"): + d - 42 # type: ignore[operator] + + +def test_pickle(): + d = UTCDateTime(2020, 8, 15, 23, 12, 9, 987_654) + dumped = pickle.dumps(d) + assert len(dumped) <= len(pickle.dumps(d.to_py())) + assert pickle.loads(pickle.dumps(d)) == d diff --git a/tests/utc/__init__.py b/tests/utc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/utc/test_datetime.py b/tests/utc/test_datetime.py deleted file mode 100644 index 00f04a95..00000000 --- a/tests/utc/test_datetime.py +++ /dev/null @@ -1,180 +0,0 @@ -import pickle -from datetime import datetime as py_datetime - -import pytest -from hypothesis import given -from hypothesis.strategies import text - -from whenever import Nothing -from whenever.utc import DateTime - - -def test_imports(): - from whenever import utc - - assert utc.__name__ == "whenever.utc" - - -def test_cannot_construct(): - with pytest.raises(TypeError, match="use static methods.*instead"): - DateTime() # type: ignore[call-arg] - - -def test_minimal(): - d = DateTime.new(2020, 8, 15, 5, 12, 30, 450).unwrap() - assert d.year == 2020 - assert d.month == 8 - assert d.day == 15 - assert d.hour == 5 - assert d.minute == 12 - assert d.second == 30 - assert d.nanosecond == 450 - - assert ( - DateTime.new(2020, 8, 15, 12) - == DateTime.new(2020, 8, 15, 12, 0) - == DateTime.new(2020, 8, 15, 12, 0, 0) - == DateTime.new(2020, 8, 15, 12, 0, 0, 0) - ) - - -def test_new_invalid(): - assert DateTime.new(2020, 2, 30) == Nothing() - - -def test_immutable(): - d = DateTime.new(2020, 8, 15).unwrap() - with pytest.raises(AttributeError): - d.year = 2021 # type: ignore[misc] - - -class TestParse: - def test_valid(self): - assert DateTime.parse("2020-08-15T12:08:30Z") == DateTime.new( - 2020, 8, 15, 12, 8, 30 - ) - - def test_valid_unpadded(self): - assert DateTime.parse("2020-8-15T12:8:30Z") == DateTime.new( - 2020, 8, 15, 12, 8, 30 - ) - - def test_valid_fraction(self): - assert DateTime.parse("2020-08-15T12:08:30.346Z") == DateTime.new( - 2020, 8, 15, 12, 8, 30, 346_000_000 - ) - - def test_overly_precise_fraction(self): - assert DateTime.parse( - "2020-08-15T12:08:30.123456789123Z" - ) == DateTime.new(2020, 8, 15, 12, 8, 30, 123_456_789) - - def test_invalid_lowercase_z(self): - assert not DateTime.parse("2020-08-15T12:08:30z") - - def test_no_trailing_z(self): - assert not DateTime.parse("2020-08-15T12:08:30") - - def test_no_seconds(self): - assert not DateTime.parse("2020-08-15T12:08Z") - - def test_empty(self): - assert not DateTime.parse("") - - def test_garbage(self): - assert not DateTime.parse("garbage") - - @given(text()) - def test_fuzzing(self, s: str): - DateTime.parse(s) # should not raise an exception, ever - - -def test_equality(): - d = DateTime.new(2020, 8, 15).unwrap() - different = DateTime.new(2020, 8, 16).unwrap() - same = DateTime.new(2020, 8, 15).unwrap() - assert d == same - assert d != different - assert not d == different - assert not d != same - - assert hash(d) == hash(same) - assert hash(d) != hash(different) - - assert d != 42 # type: ignore[comparison-overlap] - assert not d == 42 # type: ignore[comparison-overlap] - - assert ( - DateTime.new(2020, 8, 15, 12, 8, 30).unwrap() - != DateTime.new(2020, 8, 15, 12, 8, 31).unwrap() - ) - - -def test_timestamp(): - assert DateTime.new(1970, 1, 1).unwrap().timestamp() == 0 - assert ( - DateTime.new(2020, 8, 15, 12, 8, 30).unwrap().timestamp() - == 1_597_493_310 - ) - - -def test_from_timestamp(): - assert ( - DateTime.from_timestamp(0).unwrap() - == DateTime.new(1970, 1, 1).unwrap() - ) - assert ( - DateTime.from_timestamp(1_597_493_310).unwrap() - == DateTime.new(2020, 8, 15, 12, 8, 30).unwrap() - ) - # TODO: test out-of-bounds of i64 and datetime - - -def test_timestamp_millis(): - assert DateTime.new(1970, 1, 1).unwrap().timestamp_millis() == 0 - assert ( - DateTime.new(2020, 8, 15, 12, 8, 30, 123_456_789) - .unwrap() - .timestamp_millis() - == 1_597_493_310_123 - ) - # TODO: test out-of-bounds of i64 and datetime - - -def test_from_timestamp_millis(): - assert ( - DateTime.from_timestamp_millis(0).unwrap() - == DateTime.new(1970, 1, 1).unwrap() - ) - assert ( - DateTime.from_timestamp_millis(1_597_493_310_123).unwrap() - == DateTime.new(2020, 8, 15, 12, 8, 30, 123_000_000).unwrap() - ) - - -def test_repr(): - d = DateTime.new(2020, 8, 15, 23, 12, 9, 987_654_000).unwrap() - assert repr(d) == "whenever.utc.DateTime(2020-08-15T23:12:09.987654Z)" - - -def test_pickle(benchmark): - d = DateTime.new(2020, 8, 15, 23, 12, 9, 987_654_000).unwrap() - dumped = benchmark(pickle.dumps, d) - assert pickle.loads(dumped) == d - - -def test_comparison(): - d = DateTime.parse("2020-08-15T23:12:09Z").unwrap() - later = DateTime.parse("2020-08-16T00:00:00Z").unwrap() - assert d < later - assert d <= later - assert later > d - assert later >= d - - with pytest.raises(TypeError): - d < 42 # type: ignore[operator] - - -def test_to_py(): - d = DateTime.new(2020, 8, 15, 23, 12, 9, 987_654_000).unwrap() - assert d.to_py() == py_datetime(2020, 8, 15, 23, 12, 9, 987_654) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..e301fdc4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,75 @@ +[tox] +isolated_build = true +envlist = py{38,39,310,311,312},style,docs,typecheck,isort,slotscheck +[testenv] +allowlist_externals = + poetry +setenv= + POETRY_VIRTUALENVS_CREATE=false +commands_pre= + poetry install -n -v --no-root --only test +commands= + pytest + +[testenv:py312] +commands = + pytest -v --cov=whenever {posargs} + +[testenv:typecheck] +commands_pre= + poetry install -n -v --no-root --only typecheck --only test + +commands= + mypy --pretty src/ tests/ + +[testenv:style] +commands_pre= + poetry install -n -v --no-root --only linting +commands= + black --check --diff src/ tests/ + flake8 src/ tests/ + +[testenv:isort] +commands_pre= + poetry install -n -v --no-root --only linting +commands= + isort --check-only --diff src/ tests/ + +[testenv:slotscheck] +commands_pre= + poetry install -n -v --no-root --only linting +commands= + slotscheck -m whenever + +[testenv:docs] +basepython=python3.11 +deps= + -rdocs/requirements.txt +commands= + sphinx-build -W -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" \ + --color -bhtml + python -c 'import pathlib; print("documentation available under " \ + + (pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html").as_uri())' + +[coverage:run] +branch=True +[coverage:report] +fail_under=100 +exclude_lines= + pragma: no cover + raise NotImplementedError + def __repr__ + @overload + ^\s+\.\.\. + if TYPE_CHECKING.*: + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311, style, isort, docs + 3.12: py312, typecheck, slotscheck + +[flake8] +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,.tox diff --git a/typesafety/test_common.yml b/typesafety/test_common.yml deleted file mode 100644 index f763d79d..00000000 --- a/typesafety/test_common.yml +++ /dev/null @@ -1,16 +0,0 @@ -- case: strict_equality - regex: true - main: | - from whenever import Some, Nothing, Option - a: Some[int] - a_a: Some[int] - b: Some[str] - c: Nothing[bool] - d: Option[int] - e: Option[str] - - a == 4 # E: .*comparison.* - a == b # E: .*comparison.* - a == a_a - a == c # E: .*comparison.* - d == e # E: .*comparison.* diff --git a/typesafety/test_naive_datetime.yml b/typesafety/test_naive_datetime.yml new file mode 100644 index 00000000..1bf37cb1 --- /dev/null +++ b/typesafety/test_naive_datetime.yml @@ -0,0 +1,30 @@ +- case: ymd_arguments + regex: true + main: | + from whenever import NaiveDateTime + d = NaiveDateTime(2020, 8, 9) + d = NaiveDateTime(2020, 8, '15') # E: .*incompatible type "str".* "int" +- case: strict_equality + regex: true + main: | + from whenever import NaiveDateTime, UTCDateTime + d = NaiveDateTime(2020, 8, 9) + d == 3 # E: .*comparison.* + d == UTCDateTime(2020, 8, 9) # E: .*comparison.* +- case: addition + regex: true + main: | + from datetime import timedelta + from whenever import NaiveDateTime + d = NaiveDateTime(2020, 8, 9) + reveal_type(d + timedelta(4)) # N: .*whenever.NaiveDateTime + d + 12 # E: .*Unsupported + +- case: subtraction + regex: true + main: | + from datetime import timedelta + from whenever import NaiveDateTime + d = NaiveDateTime(2020, 8, 9) + reveal_type(d - timedelta(4)) # N: .*type is "whenever.NaiveDateTime" + reveal_type(d - d) # N: .*type is "datetime.timedelta diff --git a/typesafety/test_utc_datetime.yml b/typesafety/test_utc_datetime.yml new file mode 100644 index 00000000..385b2444 --- /dev/null +++ b/typesafety/test_utc_datetime.yml @@ -0,0 +1,35 @@ +- case: ymd_arguments + regex: true + main: | + from whenever import UTCDateTime + d = UTCDateTime(2020, 8, 9) + d = UTCDateTime(2020, 8, '15') # E: .*incompatible type "str".* "int" +- case: strict_equality + regex: true + main: | + from whenever import UTCDateTime + d = UTCDateTime(2020, 8, 9) + d == 3 # E: .*comparison.* +- case: addition + regex: true + main: | + from datetime import timedelta + from whenever import UTCDateTime + d = UTCDateTime(2020, 8, 9) + reveal_type(d + timedelta(4)) # N: .*whenever.UTCDateTime + d + 12 # E: .*Unsupported + +- case: subtraction + regex: true + main: | + from datetime import timedelta + from whenever import UTCDateTime + d = UTCDateTime(2020, 8, 9) + reveal_type(d - timedelta(4)) # N: .*type is "whenever.UTCDateTime" + reveal_type(d - d) # N: .*type is "datetime.timedelta +- case: replace + regex: true + main: | + from whenever import UTCDateTime + d = UTCDateTime(2020, 8, 9) + d.replace(tzinfo=None) # E: .*Unexpected keyword.*tzinfo.* diff --git a/typesafety/utc/test_datetime.yml b/typesafety/utc/test_datetime.yml deleted file mode 100644 index 78349f2f..00000000 --- a/typesafety/utc/test_datetime.yml +++ /dev/null @@ -1,17 +0,0 @@ -- case: ymd_arguments - regex: true - main: | - from whenever.utc import DateTime - d = DateTime.new(2020, 8, 9) - d = DateTime.new(2020, 8, '15') # E: .*incompatible type "str".* "int" -- case: no_constructor - regex: true - main: | - from whenever.utc import DateTime - d = DateTime(2020) # E: .*NoReturn -- case: strict_equality - regex: true - main: | - from whenever.utc import DateTime - d = DateTime.new(2020, 8, 9).unwrap() - d == 3 # E: .*comparison.*